Vue CLI3搭建Antd-vue項目

Vue CLI3搭建Antd-vue項目

Vue CLI 3快速建立項目

  • 輸入命令 vue create ant-design-vue-pro建立項目
  • 按上下鍵選擇Manually select features(手動選擇功能)項,default (babel, eslint)(默認安裝)

default(babel,eslint):默認設置(直接enter,沒有帶任何輔助功能的 npm包javascript

Manually select features:手動配置是咱們項目所須要的npm包css

  • 按上下鍵選擇要安裝的功能,按空格鍵肯定,選擇完成以後回車進行下一步

  • 按上下鍵和回車鍵選擇要安裝的功能;

自定義Webpack和Babel配置

  • antd按需引入組件依賴,輸入命令npm i --save-dev babel-plugin-import安裝babel-plugin-import,而且修改文件babel.config.js以下
{
  plugins: [
    ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }] // `style: true` 會加載 less 文件
  ]
}
複製代碼
  • 輸入命令 npm i ant-design-vue安裝組antd-vue件庫
  • main 文件引入antd-vue,引入後以下
import Vue from "vue";
import { Button } from "ant-design-vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

Vue.config.productionTip = false;

Vue.use(Button);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");
複製代碼
  • npm run serve運行項目,若出現less問題

解決辦法配置vue.config.jshtml

css: {
    loaderOptions: {
      less: {
        javascriptEnabled: true,
      }
    }
  }
複製代碼

如何設計一個高擴展的路由

新建用戶登錄註冊組建及配置路由

  • src/router.js配置用戶登陸註冊路由
{
      path: "/user",
      children: [
        {
          path: "/user/login",
          name: "login",
          component: () => import(/* webpackChunkName: "user"*/ "./views/user/Login.vue")
        },
        {
          path: "/user/register",
          name: "register",
          component: () => import(/* webpackChunkName: "user"*/ "./views/user/Register.vue")
        },

      ]
}
複製代碼
  • src/views下新建一個user用戶目錄存放用戶相關的組件,新建用戶組件Login.vueRegister.vue

佈局組件(routerView)

  • 用戶組件已經建立完成,但訪問路由時並不會加載到咱們的組件,咱們須要一個routerView佔位符,當匹配到組件時把它掛載到routerView位置上,能夠寫一個routerView組件引入,不過通常用render方法比較簡便
{
      path: "/user",
      component: { render: h=> h("router-view") },
      children: [
        {
          path: "/user/login",
          name: "login",
          component: () => import(/* webpackChunkName: "user"*/ "./views/user/Login.vue")
        },
        {
          path: "/user/register",
          name: "register",
          component: () => import(/* webpackChunkName: "user"*/ "./views/user/Register.vue")
        },

      ]
    }
複製代碼
  • 設計佈局組件,並在佈局裏面提供routerView掛載項,在src新建layouts目錄用來存放UserLayout及BasicLayout;在src新建dashboard目錄存放Analysis分析頁面

UserLayout:抽離出用於登錄註冊頁面的通用佈局vue

<template>
  <div>
    <div class="desc">Ant Desigin Vue Pro</div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {

}
</script>

<style>

</style>


複製代碼
  • src/router引入UserLayout佈局組件,當訪問user重定向到/user/login頁面
{
      path: "/user",
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/UserLayout"),
      children: [
        {
          path: "/user",
          redirect: "/user/login"
        },
        {
          path: "/user/login",
          name: "login",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/user/Login")
        },
        {
          path: "/user/register",
          name: "register",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/user/Register")
        }
      ]
    }
複製代碼

BasicLayout:基礎頁面佈局,包含了頭部導航,底部信息,側邊欄和內容部分java

<template>
  <div>
    <Header />
    <SiderMenu />
    <router-view></router-view>
    <Footer />
  </div>
</template>

<script>
import Header from "./Header";
import Footer from "./Footer";
import SiderMenu from "./SiderMenu";
export default {
  components: {
    Header,
    Footer,
    SiderMenu
  }
};
</script>

<style></style>
複製代碼
  • src/router引入BasicLayout佈局組件,當地址欄路徑爲/時,重定向到分析頁
{
      path: "/",
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/BasicLayout"),
      children: [
        // dashboard
        {
          path: "/",
          redirect: "/dashboard/analysis"
        },
        {
          path: "/dashboard",
          name: "dashboard",
          meta: { icon: "dashboard", title: "儀表盤" },
          component: { render: h => h("router-view") },
          children: [
            {
              path: "/dashboard/analysis",
              name: "analysis",
              meta: { title: "分析頁" },
              component: () =>
                import(
                  /* webpackChunkName: "dashboard" */ "./views/Dashboard/Analysis"
                )
            }
          ]
        }
      ]
    }
複製代碼

實現一個可動態改變的頁面佈局

  • 打開vue antd官網https://vue.ant.design,找到layout佈局組件,找到相似pro的模板,以下圖

  • 打開代碼賦值到咱們項目中src/layouts/BasicLayout.vue

BasicLayout.vuewebpack

<template>
  <div>
    <a-layout id="components-layout-demo-side" style="min-height: 100vh">
    <a-layout-sider
      collapsible
      v-model="collapsed"
    >
      <div class="logo" />
      <SiderMenu />
    </a-layout-sider>
    <a-layout>
        <a-layout-header style="background: #fff; padding: 0" >
          <Header />
        </a-layout-header>
        <a-layout-content style="margin: 0 16px">
          <router-view></router-view>
        </a-layout-content>
        <a-layout-footer style="text-align: center">
          <Footer />
        </a-layout-footer>
      </a-layout>
    </a-layout>
  </div>
</template>

<script>
import Header from "./Header";
import Footer from "./Footer";
import SiderMenu from "./SiderMenu";
export default {
  data() {
    return {
      collapsed: false,
    }
  },
  components: {
    Header,
    Footer,
    SiderMenu
  }
};
</script>

<style></style>

複製代碼
  • 菜單切換trigger與antd pro有些差別須要咱們本身去自定義;

首先隱藏trigger原始圖標,Layout.Sider 有個trigger屬性,自定義 trigger,設置爲 null 時隱藏 triggerios

而後自定義a-icon 圖片及位置git

最後圖片添加click事件控制菜單切換github

BasicLayout.vueweb

<template>
  <div>
    <a-layout id="components-layout-demo-side" style="min-height: 100vh">
    <a-layout-sider
      collapsible
      v-model="collapsed"
      :trigger="null"
    >
      <div class="logo" />
      <SiderMenu />
    </a-layout-sider>
    <a-layout>
        <a-layout-header style="background: #fff; padding: 0" >
          <a-icon 
            class="trigger" 
            :type="collapsed ? 'menu-unfold' : 'menu-fold'" 
            @click="collapsed = !collapsed"
          ></a-icon>
          <Header />
        </a-layout-header>
        <a-layout-content style="margin: 0 16px">
          <router-view></router-view>
        </a-layout-content>
        <a-layout-footer style="text-align: center">
          <Footer />
        </a-layout-footer>
      </a-layout>
    </a-layout>
  </div>
</template>

<script>
import Header from "./Header";
import Footer from "./Footer";
import SiderMenu from "./SiderMenu";
export default {
  data() {
    return {
      collapsed: false,
    }
  },
  components: {
    Header,
    Footer,
    SiderMenu
  }
};
</script>

<style lang="less" scoped>
.trigger{
  padding: 0 20px;
  line-height: 64px;
  font-size: 20px;
}
.trigger:hover{
  background-color: #eeeeee;
}
</style>

</style>

複製代碼
  • antd pro右側抽屜動態改變頁面佈局,在src/components/SettingDrawer/Index.vue新建設置主題組件,找到官網Drawer 基礎抽屜組件

Index.vue

<template>
  <div>
    <a-button type="primary" @click="showDrawer">
      Open
    </a-button>
    <a-drawer
      title="Basic Drawer"
      placement="right"
      :closable="false"
      @close="onClose"
      :visible="visible"
    >
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
    </a-drawer>
  </div>
</template>
<script>
export default {
  data() {
    return {
      visible: false,
    }
  },
  methods: {
    showDrawer() {
      this.visible = true
    },
    onClose() {
      this.visible = false
    },
  },
}
</script>
複製代碼

-自定義抽屜按鈕及抽屜樣式

刪除原始按鈕及展現事件;

把圖標定位到右側及樣式,綁定展開收縮事件;

動態佈局設置項樣式

爲了能動態改變頁面佈局,咱們暫時先把設置參數放到router中,而後BasicLayout經過computed計算屬性從router中讀取來動態修改佈局

Index.vue

<template>
  <div>
    <a-drawer
      placement="right"
      :closable="false"
      @close="onClose"
      :visible="visible"
      width="300px"
    >
      <template v-slot:handle>
        <div class="handle" @click="visible = !visible">
          <a-icon :type="visible ? 'close' : 'setting'"></a-icon>
        </div>
      </template>
      <div>
        <h2>總體風格定製</h2>
        <a-radio-group
          :value="$route.query.navTheme || 'dark'"
          @change="e => handleSettingChange('navTheme', e.target.value)"
        >
          <a-radio value="dark">黑色</a-radio>
          <a-radio value="light">白色</a-radio>
        </a-radio-group>
        <h2>導航模式</h2>
        <a-radio-group
          :value="$route.query.navLayout || 'left'"
          @change="e => handleSettingChange('navLayout', e.target.value)"
        >
          <a-radio value="left">左側</a-radio>
          <a-radio value="lighn">頂部</a-radio>
        </a-radio-group>
      </div>
    </a-drawer>
  </div>
</template>
<script>
export default {
  data() {
    return {
      visible: false
    };
  },
  methods: {
    onClose() {
      this.visible = false;
    },
    handleSettingChange(type, value) {
      this.$router.push({ query: { ...this.$route.query, [type]: value } });
    }
  }
};
</script>
<style type="less" scoped>
.handle {
  position: absolute;
  top: 240px;
  right: 300px;
  width: 48px;
  height: 48px;
  background: #1890ff;
  color: #fff;
  font-size: 20px;
  text-align: center;
  line-height: 48px;
  border-radius: 3px 0 0 3px;
}
</style>


複製代碼

BasicLayout.vue

<template>
  <div :class="[`nav-theme-${navTheme}`, `nav-layout-${navLayout}`]">
    <a-layout id="components-layout-demo-side" style="min-height: 100vh">
      <a-layout-sider
        v-if="navLayout === 'left'"
        :theme="navTheme"
        :trigger="null"
        collapsible
        v-model="collapsed"
        width="256px"
      >
        <div class="logo">
          <h1>Ant Design Pro</h1>
        </div>
        <SiderMenu />
      </a-layout-sider>
      <a-layout>
        <a-layout-header style="background: #fff; padding: 0">
          <a-icon
            class="trigger"
            :type="collapsed ? 'menu-unfold' : 'menu-fold'"
            @click="collapsed = !collapsed"
          ></a-icon>
          <Header />
        </a-layout-header>
        <a-layout-content style="margin: 0 16px">
          <router-view></router-view>
        </a-layout-content>
        <a-layout-footer style="text-align: center">
          <Footer />
        </a-layout-footer>
      </a-layout>
    </a-layout>
    <setting-drawer />
  </div>
</template>

<script>
import Header from "./Header";
import Footer from "./Footer";
import SiderMenu from "./SiderMenu";
import SettingDrawer from "../components/SettingDrawer/Index";
export default {
  data() {
    return {
      collapsed: false
    };
  },
  computed: {
    navTheme() {
      return this.$route.query.navTheme || "dark";
    },
    navLayout() {
      return this.$route.query.navLayout || "left";
    }
  },
  components: {
    Header,
    Footer,
    SiderMenu,
    SettingDrawer
  }
};
</script>

<style lang="less" scoped>
.trigger {
  padding: 0 20px;
  line-height: 64px;
  font-size: 20px;
  &:hover {
    background: #eeeeee;
  }
}
.logo {
  position: relative;
  height: 64px;
  padding-left: 24px;
  overflow: hidden;
  line-height: 64px;
  svg {
    width: 32px;
    height: 32px;
    display: inline-block;
    vertical-align: middle;
  }
  h1 {
    display: inline-block;
    margin: 0 0 0 12px;
    font-size: 20px;
    font-family: Avenir, "Helvetica Neue", Arial, Helvetica, sans-serif;
    font-weight: 600;
    vertical-align: middle;
  }
}
.nav-theme-dark {
  /deep/ .logo {
    h1 {
      color: #ffffff;
    }
  }
}
</style>

複製代碼

如何將菜單與路由結合

  • 菜單咱們將採用官網單文件遞歸菜單示例

  • 直接複製示例代碼來修改咱們SiderMenu.vue文件,因爲須要遞歸須要建立一個SubMenu.vue文件,兩種方式:①函數式組件的形式 ②普通組件,推薦是函數式組件
* recommend SubMenu.vue
 https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu.vue 
 * SubMenu1.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu1.vue
複製代碼
  • 切換主題菜單是沒變化的,咱們須要獲取到設置的主題參數,能夠再BasicLayout.vue經過父子傳參的方式傳到SiderMenu.vue中

BasicLayout.vue

<SiderMenu :theme="navTheme" :collapsed="collapsed" />
複製代碼

SiderMenu.vue

<template>
  <div style="width: 256px">
    <a-menu
      :defaultSelectedKeys="['1']"
      :defaultOpenKeys="['2']"
      mode="inline"
      :theme="theme"
      :inlineCollapsed="collapsed"
    >
      <template v-for="item in list">
        <a-menu-item v-if="!item.children" :key="item.key">
          <a-icon type="pie-chart" />
          <span>{{ item.title }}</span>
        </a-menu-item>
        <sub-menu v-else :menu-info="item" :key="item.key" />
      </template>
    </a-menu>
  </div>
</template>

<script>
/*
 * recommend SubMenu.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu.vue
 * SubMenu1.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu1.vue
 * */
import SubMenu from "./SubMenu";
export default {
  props: {
    theme: {
      type: String,
      default: "dark"
    }
  },
  components: {
    "sub-menu": SubMenu
  },
  data() {
    return {
      collapsed: false,
      list: [
        {
          key: "1",
          title: "Option 1"
        },
        {
          key: "2",
          title: "Navigation 2",
          children: [
            {
              key: "2.1",
              title: "Navigation 3",
              children: [{ key: "2.1.1", title: "Option 2.1.1" }]
            }
          ]
        }
      ]
    };
  },
  methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed;
    }
  }
};
</script>
複製代碼
  • 經過路由的配置來生成咱們的菜單數據項,有些路由是不須要在菜單欄顯示的,因此在 vue-router 的配置中咱們增長了一些參數,如 hideChildrenInMenu,hideInMenu,meta.title,meta.icon來輔助生成菜單

hideChildrenInMenu 用於隱藏不須要在菜單中展現的子路由。

hideInMenu 能夠在菜單中不展現這個路由,包括子路由。

meta.title 和 meta.icon分別表明生成菜單項的文本和圖標。

router.js

import Vue from "vue";
import Router from "vue-router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";

Vue.use(Router);

const router = new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/user",
      hideInMenu: true,
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/UserLayout"),
      children: [
        {
          path: "/user",
          redirect: "/user/login"
        },
        {
          path: "/user/login",
          name: "login",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/user/Login")
        },
        {
          path: "/user/register",
          name: "register",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/user/Register")
        }
      ]
    },
    {
      path: "/",
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/BasicLayout"),
      children: [
        // dashboard
        {
          path: "/",
          redirect: "/dashboard/analysis"
        },
        {
          path: "/dashboard",
          name: "dashboard",
          meta: { icon: "dashboard", title: "儀表盤" },
          component: { render: h => h("router-view") },
          children: [
            {
              path: "/dashboard/analysis",
              name: "analysis",
              meta: { title: "分析頁" },
              component: () =>
                import(
                  /* webpackChunkName: "dashboard" */ "./views/Dashboard/Analysis"
                )
            }
          ]
        },
        // form
        {
          path: "/form",
          name: "form",
          component: { render: h => h("router-view") },
          meta: { icon: "form", title: "表單" },
          children: [
            {
              path: "/form/basic-form",
              name: "basicform",
              meta: { title: "基礎表單" },
              component: () =>
                import(/* webpackChunkName: "form" */ "./views/Forms/BasicForm")
            },
            {
              path: "/form/step-form",
              name: "stepform",
              hideChildrenInMenu: true,
              meta: { title: "分佈表單" },
              component: () =>
                import(
                  /* webpackChunkName: "form" */ "./views/Forms/StepForm/Index"
                ),
              children: [
                {
                  path: "/form/step-form",
                  redirect: "/form/step-form/info"
                },
                {
                  path: "/form/step-form/info",
                  name: "info",
                  component: () =>
                    import(
                      /* webpackChunkName: "form" */ "./views/Forms/StepForm/Step1"
                    )
                },
                {
                  path: "/form/step-form/confirm",
                  name: "confirm",
                  component: () =>
                    import(
                      /* webpackChunkName: "form" */ "./views/Forms/StepForm/Step2"
                    )
                },
                {
                  path: "/form/step-form/result",
                  name: "result",
                  component: () =>
                    import(
                      /* webpackChunkName: "form" */ "./views/Forms/StepForm/Step3"
                    )
                }
              ]
            }
          ]
        }
      ]
    },
    {
      path: "*",
      name: "404",
      hideInMenu: true,
      component: () =>
        import(/* webpackChunkName: "exception" */ "@/views/Exception/404")
    }
  ]
});

router.beforeEach((to, from, next) => {
  NProgress.start();
  next();
});

router.afterEach(() => {
  NProgress.done();
});

export default router;

複製代碼
  • 動態讀取router路由配置,根據以上配置處理參數,生成菜單

SiderMenu

<template>
  <div style="width: 256px">
    <a-menu
      :selectedKeys="selectedKeys"
      :openKeys.sync="openKeys"
      mode="inline"
      :theme="theme"
    >
      <template v-for="item in menuData">
        <a-menu-item
          v-if="!item.children"
          :key="item.path"
          @click="() => $router.push({ path: item.path, query: $route.query })"
        >
          <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
          <span>{{ item.meta.title }}</span>
        </a-menu-item>
        <sub-menu v-else :menu-info="item" :key="item.path" />
      </template>
    </a-menu>
  </div>
</template>

<script>
/*
 * recommend SubMenu.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu.vue
 * SubMenu1.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu1.vue
 * */
import SubMenu from "./SubMenu";
export default {
  props: {
    theme: {
      type: String,
      default: "dark"
    },
    collapsed: {
      type: Boolean,
      default: false
    }
  },
  components: {
    "sub-menu": SubMenu
  },
  watch: {
    "$route.path": function(val) {
      this.selectedKeys = this.selectedKeysMap[val];
      this.openKeys = this.collapsed ? [] : this.openKeysMap[val];
    },
    collapsed(val) {
      if (val) {
        this.cacheOpenKeys = this.openKeys;
        this.openKeys = [];
      } else {
        this.openKeys = this.cacheOpenKeys;
      }
    }
  },
  data() {
    this.selectedKeysMap = {};
    this.openKeysMap = {};
    const menuData = this.getMenuData(this.$router.options.routes);
    return {
      menuData,
      selectedKeys: this.selectedKeysMap[this.$route.path],
      openKeys: this.collapsed ? [] : this.openKeysMap[this.$route.path]
    };
  },
  methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed;
    },
    getMenuData(routes = [], parentKeys = [], selectedKey) {
      const menuData = [];
      for (let item of routes) {
        if (item.name && !item.hideInMenu) {
          this.openKeysMap[item.path] = parentKeys;
          this.selectedKeysMap[item.path] = [selectedKey || item.path];
          const newItem = { ...item };
          delete newItem.children;
          if (item.children && !item.hideChildrenInMenu) {
            newItem.children = this.getMenuData(item.children, [
              ...parentKeys,
              item.path
            ]);
          } else {
            this.getMenuData(
              item.children,
              selectedKey ? parentKeys : [...parentKeys, item.path],
              selectedKey || item.path
            );
          }
          menuData.push(newItem);
        } else if (
          !item.hideInMenu &&
          !item.hideChildrenInMenu &&
          item.children
        ) {
          menuData.push(
            ...this.getMenuData(item.children, [...parentKeys, item.path])
          );
        }
      }
      return menuData;
    }
  }
};
</script>


複製代碼

SubMenu.vue

<template functional>
  <a-sub-menu :key="props.menuInfo.path">
    <span slot="title">
      <a-icon
        v-if="props.menuInfo.meta.icon"
        :type="props.menuInfo.meta.icon"
      /><span>{{ props.menuInfo.meta.title }}</span>
    </span>
    <template v-for="item in props.menuInfo.children">
      <a-menu-item
        v-if="!item.children"
        :key="item.path"
        @click=" () => parent.$router.push({ path: item.path, query: parent.$route.query }) "
      >
        <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
        <span>{{ item.meta.title }}</span>
      </a-menu-item>
      <sub-menu v-else :key="item.meta.path" :menu-info="item" />
    </template>
  </a-sub-menu>
</template>
<script>
export default {
  props: ["menuInfo"]
};
</script>

複製代碼

如何使用路由管理用戶權限

-在src\utils新建auth.js模擬獲取當前用戶權限及驗證權限

auth.js

const currentAuth = ["admin"];
export { currentAuth };

export function getCurrentAuthority() {
  return currentAuth;
}

export function check(authority) {
  const current = getCurrentAuthority();
  return current.some(item => authority.includes(item));
}

export function isLogin() {
  const current = getCurrentAuthority();
  return current && current[0] !== "guest";
}

複製代碼
  • router.js meta中設置用戶權限

router.js

import Vue from "vue";
import Router from "vue-router";
import findLast from "lodash/findLast";
import { notification } from "ant-design-vue";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { check, isLogin } from "./utils/auth";

Vue.use(Router);

const router = new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/user",
      hideInMenu: true,
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/UserLayout"),
      children: [
        {
          path: "/user",
          redirect: "/user/login"
        },
        {
          path: "/user/login",
          name: "login",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/user/Login")
        },
        {
          path: "/user/register",
          name: "register",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/user/Register")
        }
      ]
    },
    {
      path: "/",
      meta: { authority: ["user", "admin"] },
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/BasicLayout"),
      children: [
        // dashboard
        {
          path: "/",
          redirect: "/dashboard/analysis"
        },
        {
          path: "/dashboard",
          name: "dashboard",
          meta: { icon: "dashboard", title: "儀表盤" },
          component: { render: h => h("router-view") },
          children: [
            {
              path: "/dashboard/analysis",
              name: "analysis",
              meta: { title: "分析頁" },
              component: () =>
                import(
                  /* webpackChunkName: "dashboard" */ "./views/Dashboard/Analysis"
                )
            }
          ]
        },
        // form
        {
          path: "/form",
          name: "form",
          component: { render: h => h("router-view") },
          meta: { icon: "form", title: "表單", authority: ["admin"] },
          children: [
            {
              path: "/form/basic-form",
              name: "basicform",
              meta: { title: "基礎表單" },
              component: () =>
                import(/* webpackChunkName: "form" */ "./views/Forms/BasicForm")
            },
            {
              path: "/form/step-form",
              name: "stepform",
              hideChildrenInMenu: true,
              meta: { title: "分佈表單" },
              component: () =>
                import(
                  /* webpackChunkName: "form" */ "./views/Forms/StepForm/Index"
                ),
              children: [
                {
                  path: "/form/step-form",
                  redirect: "/form/step-form/info"
                },
                {
                  path: "/form/step-form/info",
                  name: "info",
                  component: () =>
                    import(
                      /* webpackChunkName: "form" */ "./views/Forms/StepForm/Step1"
                    )
                },
                {
                  path: "/form/step-form/confirm",
                  name: "confirm",
                  component: () =>
                    import(
                      /* webpackChunkName: "form" */ "./views/Forms/StepForm/Step2"
                    )
                },
                {
                  path: "/form/step-form/result",
                  name: "result",
                  component: () =>
                    import(
                      /* webpackChunkName: "form" */ "./views/Forms/StepForm/Step3"
                    )
                }
              ]
            }
          ]
        }
      ]
    },
    {
      path: "/403",
      name: "403",
      hideInMenu: true,
      component: () =>
        import(/* webpackChunkName: "exception" */ "@/views/Exception/403")
    },
    {
      path: "*",
      name: "404",
      hideInMenu: true,
      component: () =>
        import(/* webpackChunkName: "exception" */ "@/views/Exception/404")
    }
  ]
});

router.beforeEach((to, from, next) => {
  if (to.path !== from.path) {
    NProgress.start();
  }
  const record = findLast(to.matched, record => record.meta.authority);
  if (record && !check(record.meta.authority)) {
    if (!isLogin() && to.path !== "/user/login") {
      next({
        path: "/user/login"
      });
    } else if (to.path !== "/403") {
      notification.error({
        message: "403",
        description: "你沒有權限訪問,請聯繫管理員諮詢。"
      });
      next({
        path: "/403"
      });
    }
    NProgress.done();
  }
  next();
});

router.afterEach(() => {
  NProgress.done();
});

export default router;

複製代碼

-在菜單中過濾掉沒有權限的路由

SiderMenu.vue

<template>
  <div style="width: 256px">
    <a-menu
      :selectedKeys="selectedKeys"
      :openKeys.sync="openKeys"
      mode="inline"
      :theme="theme"
    >
      <template v-for="item in menuData">
        <a-menu-item
          v-if="!item.children"
          :key="item.path"
          @click="() => $router.push({ path: item.path, query: $route.query })"
        >
          <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
          <span>{{ item.meta.title }}</span>
        </a-menu-item>
        <sub-menu v-else :menu-info="item" :key="item.path" />
      </template>
    </a-menu>
  </div>
</template>

<script>
/*
 * recommend SubMenu.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu.vue
 * SubMenu1.vue https://github.com/vueComponent/ant-design-vue/blob/master/components/menu/demo/SubMenu1.vue
 * */
import SubMenu from "./SubMenu";
import { check } from "../utils/auth";
export default {
  props: {
    theme: {
      type: String,
      default: "dark"
    },
    collapsed: {
      type: Boolean,
      default: false
    }
  },
  components: {
    "sub-menu": SubMenu
  },
  watch: {
    "$route.path": function(val) {
      this.selectedKeys = this.selectedKeysMap[val];
      this.openKeys = this.collapsed ? [] : this.openKeysMap[val];
    },
    collapsed(val) {
      if (val) {
        this.cacheOpenKeys = this.openKeys;
        this.openKeys = [];
      } else {
        this.openKeys = this.cacheOpenKeys;
      }
    }
  },
  data() {
    this.selectedKeysMap = {};
    this.openKeysMap = {};
    const menuData = this.getMenuData(this.$router.options.routes);
    return {
      menuData,
      selectedKeys: this.selectedKeysMap[this.$route.path],
      openKeys: this.collapsed ? [] : this.openKeysMap[this.$route.path]
    };
  },
  methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed;
    },
    getMenuData(routes = [], parentKeys = [], selectedKey) {
      const menuData = [];
      for (let item of routes) {
        if (item.meta && item.meta.authority && !check(item.meta.authority)) {
          continue;
        }
        if (item.name && !item.hideInMenu) {
          this.openKeysMap[item.path] = parentKeys;
          this.selectedKeysMap[item.path] = [selectedKey || item.path];
          const newItem = { ...item };
          delete newItem.children;
          if (item.children && !item.hideChildrenInMenu) {
            newItem.children = this.getMenuData(item.children, [
              ...parentKeys,
              item.path
            ]);
          } else {
            this.getMenuData(
              item.children,
              selectedKey ? parentKeys : [...parentKeys, item.path],
              selectedKey || item.path
            );
          }
          menuData.push(newItem);
        } else if (
          !item.hideInMenu &&
          !item.hideChildrenInMenu &&
          item.children
        ) {
          menuData.push(
            ...this.getMenuData(item.children, [...parentKeys, item.path])
          );
        }
      }
      return menuData;
    }
  }
};
</script>

複製代碼

更加精細化的權限設計(權限組件、權限指令)

若想控制table的增刪改查的功能,咱們須要控制單個按鈕的功能

組件式權限控制

  • components中新建權限組件Authorized.vue組件

Authorized.vue

<script>
import { check } from "../utils/auth";
export default {
    // 函數式組件
  functional: true,
  props: {
    authority: {
      type: Array,
      required: true
    }
  },
  render(h, context) {
    const { props, scopedSlots } = context;
    return check(props.authority) ? scopedSlots.default() : null;
  }
};
</script>
複製代碼
  • 權限組件在各個組件使用到,註冊成爲全局組建,在按需組件的統一引入文件src/core/lazy_use,在main.js中引入import "./core/lazy_use";

lazy_use.js

import Vue from "vue";
import Authorized from "@/components/Authorized";

Vue.component("Authorized", Authorized);
複製代碼
  • src/layouts/BasicLayout.vue引入權限組件
<template>
  <div :class="[`nav-theme-${navTheme}`, `nav-layout-${navLayout}`]">
    <a-layout id="components-layout-demo-side" style="min-height: 100vh">
      <a-layout-sider
        v-if="navLayout === 'left'"
        :theme="navTheme"
        :trigger="null"
        collapsible
        v-model="collapsed"
        width="256px"
      >
        <div class="logo">
          <h1>Ant Design Pro</h1>
        </div>
        <SiderMenu :theme="navTheme" :collapsed="collapsed" />
      </a-layout-sider>
      <a-layout>
        <a-layout-header style="background: #fff; padding: 0">
          <a-icon
            class="trigger"
            :type="collapsed ? 'menu-unfold' : 'menu-fold'"
            @click="collapsed = !collapsed"
          ></a-icon>
          <Header />
        </a-layout-header>
        <a-layout-content style="margin: 0 16px">
          <router-view></router-view>
        </a-layout-content>
        <a-layout-footer style="text-align: center">
          <Footer />
        </a-layout-footer>
      </a-layout>
    </a-layout>
    <Authorized :authority="['admin']">
      <SettingDrawer />
    </Authorized>
  </div>
</template>
複製代碼
  • 當前用戶權限爲admin時能夠看到動態佈局按鈕,其餘權限將看不到

指令式權限

  • 新建存放指令的文件src/directives/auth.js

auth.js

import { check } from "../utils/auth";

function install(Vue, options = {}) {
  Vue.directive(options.name || "auth", {
    inserted(el, binding) {
      if (!check(binding.value)) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    }
  });
}

export default { install };
複製代碼
<a-icon
        v-auth="['admin']"
        class="trigger"
></a-icon>
複製代碼

如何在組件中使用Echarts、Antv等其餘第三方庫

  • 輸入命令安裝echartsnpm i echarts,在src/components新建Chart.vue組件

Chart.vue

<template>
  <div ref="chartDom"></div>
</template>

<script>
import echarts from "echarts/lib/echarts";
import "echarts/lib/chart/bar";
import "echarts/lib/component/title";

import debounce from "lodash/debounce";
import { addListener, removeListener } from "resize-detector";
export default {
  props: {
    option: {
      type: Object,
      default: () => {}
    }
  },
  watch: {
    option(val) {
      this.chart.setOption(val);
    }
  },
  created() {
    this.resize = debounce(this.resize, 300);//防抖
  },
  mounted() {
    this.renderChart();
    addListener(this.$refs.chartDom, this.resize);
  },
  beforeDestroy() {
    removeListener(this.$refs.chartDom, this.resize);
    this.chart.dispose();
    this.chart = null;
  },
  methods: {
    resize() {
      console.log("resize");
      this.chart.resize();
    },
    renderChart() {
      // 基於準備好的dom,初始化echarts實例
      this.chart = echarts.init(this.$refs.chartDom);
      this.chart.setOption(this.option);
    }
  }
};
</script>

<style></style>

複製代碼
  • src\views\Dashboard\Analysis.vue引入Chart.vue組件

父子傳值把chartOption傳給組件

新增定時器並引入random函數,每隔3秒數據變化

數據變化後,給chartOption賦新值,組件監聽數據變化則更新組件,不須要深度監聽

beforeDestroy時銷燬定時器

Analysis.vue

<template>
  <div>
    <Chart :option="chartOption" style="height: 400px" />
  </div>
</template>

<script>
import random from "lodash/random";
import Chart from "../../components/Chart";
export default {
  data() {
    return {
      chartOption: {
        title: {
          text: "ECharts 入門示例"
        },
        tooltip: {},
        xAxis: {
          data: ["襯衫", "羊毛衫", "雪紡衫", "褲子", "高跟鞋", "襪子"]
        },
        yAxis: {},
        series: [
          {
            name: "銷量",
            type: "bar",
            data: [5, 20, 36, 10, 10, 20]
          }
        ]
      }
    };
  },
  mounted() {
    this.interval = setInterval(() => {
      this.chartOption.series[0].data = this.chartOption.series[0].data.map(
        () => random(100)
      );
      this.chartOption = { ...this.chartOption };
    }, 3000);
  },
  beforeDestroy() {
    clearInterval(this.interval);
  },
  components: {
    Chart
  }
};
</script>

<style></style>

複製代碼

如何高效地使用Mock數據進行開發

  • 安裝axios,因引入axios,添加axios請求

Analysis.vue

<template>
  <div>
    <Chart :option="chartOption" style="height: 400px" />
  </div>
</template>

<script>
import axios from "axios";
import Chart from "../../components/Chart";
export default {
  data() {
    return {
      chartOption: {}
    };
  },
  mounted() {
    this.getChartData();
    this.interval = setInterval(() => {
      this.getChartData();
    }, 3000);
  },
  methods: {
    getChartData() {
      axios
        .get("/api/dashboard/chart", { params: { ID: 12345 } })
        .then(response => {
          this.chartOption = {
            title: {
              text: "ECharts 入門示例"
            },
            tooltip: {},
            xAxis: {
              data: ["襯衫", "羊毛衫", "雪紡衫", "褲子", "高跟鞋", "襪子"]
            },
            yAxis: {},
            series: [
              {
                name: "銷量",
                type: "bar",
                data: response.data
              }
            ]
          };
        });
    }
  },
  beforeDestroy() {
    clearInterval(this.interval);
  },
  components: {
    Chart
  }
};
</script>

<style></style>

複製代碼
  • mock\dashboard_chart.js編寫接口文件

dashboard_chart.js

function chart(method) {
  let res = null;
  switch (method) {
    case "GET":
      res = [20, 40, 79, 10, 30, 48];
      break;
    default:
      res = null;
  }
  return res;
}

module.exports = chart;

複製代碼
  • 添加代理

vue.config.js

module.exports = {
  lintOnSave: false,
  css: {
    loaderOptions: {
      less: {
        javascriptEnabled: true
      }
    }
  },
  devServer: {
    port: 8000,
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        bypass: function(req, res) {
          if (req.headers.accept.indexOf("html") !== -1) {
            console.log("Skipping proxy for browser request.");
            return "/index.html";
          } else {
            console.log(req.path);
            const name = req.path
              .split("/api/")[1]
              .split("/")
              .join("_");
            const mock = require(`./mock/${name}`);
            const result = mock(req.method);
            delete require.cache[require.resolve(`./mock/${name}`)];
            console.dir(result);
            return res.send(result);
          }
        }
      }
    }
  }
};

複製代碼
  • mock.js

以上都是本身寫的mock請求,下面使用第三方mock.js

安裝mock.js umi-mock-middleware,umi-mock-middleware中間件當修改mock數據的時候,中間件會自動刷新

首先註冊中間件,修改vue.config.js,若是沒有在項目根文件夾下新建一個,關鍵代碼以下

const { createMockMiddleware } = require("umi-mock-middleware");
  devServer: {
    port: 8000,
    open: true,
    // 解析body,對接真實服務端環境須要註釋掉
    before: function(app) {
      // var bodyParser = require("body-parser");
      // app.use(bodyParser.json());
      if (process.env.MOCK !== "none") {
        app.use(createMockMiddleware());
      }
    },
    proxy: {
      "/api": {
        target: "http://localhost:3000"
      }
    }
  }
複製代碼

/mock/chart.js

module.exports = { 
  `GET /api/dashboard/chart`: (req, res) {
    let result = null;
    switch (req.method) {
      case "GET":
        result = [100, 40, 78, 10, 30, 50];
        break;
      default:
        result = null;
    }
    // 返回你的mock數據。好比:
    res.json(result);
  }
};
複製代碼

如何與服務端進行交互

開發完後與後端聯調

  • 須要一個簡單方式來區分mock 環境與聯調環境,在環境變量package.json新增命令

window下須要安裝一個cross-env的包才能使用

修改vue.config.js 配置

package.json

// mac
"scripts": {
    "serve": "vue-cli-service serve",
    "serve:no-mock":"MOCK=none vue-cli-service serve"
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit"
  }
  //window
  "scripts": {
    "serve": "vue-cli-service serve",
    "serve:no-mock":"cross-env MOCK=none vue-cli-service serve"
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit"
  }
複製代碼

vue.config.js

const { createMockMiddleware } = require("umi-mock-middleware");

module.exports = {
  css: {
    loaderOptions: {
      less: {
        javascriptEnabled: true
      }
    }
  },
  devServer: {
    open: true,
    // 解析body,對接真實服務端環境須要註釋掉
    before: function(app) {
      // var bodyParser = require("body-parser");
      // app.use(bodyParser.json());
      if (process.env.MOCK !== "none") {
        app.use(createMockMiddleware());
      }
    },
    proxy: {
      "/api": {
        target: "http://localhost:3000"
      }
    }
  }
};

複製代碼

封裝axios

通常狀況下,不會直接axios請求會進行一個二次封裝

  • src/utils/request.js進行二次封裝

request.js

import axios from "axios";
import { notification } from "ant-design-vue";

function request(options) {
  return axios(options)
    .then(res => {
      return res;
    })
    .catch(error => {
      const {
        response: { status, statusText }
      } = error;
      notification.error({
        // eslint-disable-next-line no-unused-vars
        message:status,
        description: statusText
      });
      return Promise.reject(error);
    });
}

export default request;

複製代碼
  • src\views\Dashboard\Analysis.vue中把封裝好的axios替換掉以前的axios
<template>
  <div>
    <Chart :option="chartOption" style="height: 400px" />
  </div>
</template>

<script>
import request from "../../utils/request";
import Chart from "../../components/Chart";
export default {
  data() {
    return {
      chartOption: {}
    };
  },
  mounted() {
    this.getChartData();
    this.interval = setInterval(() => {
      this.getChartData();
    }, 3000);
  },
  methods: {
    getChartData() {
      request({
        url: "/api/dashboard/chart",
        method: "get",
        params: { ID: 12345 }
      }).then(response => {
        this.chartOption = {
          title: {
            text: "ECharts 入門示例"
          },
          tooltip: {},
          xAxis: {
            data: ["襯衫", "羊毛衫", "雪紡衫", "褲子", "高跟鞋", "襪子"]
          },
          yAxis: {},
          series: [
            {
              name: "銷量",
              type: "bar",
              data: response.data
            }
          ]
        };
      });
    }
  },
  beforeDestroy() {
    clearInterval(this.interval);
  },
  components: {
    Chart
  }
};
</script>

<style></style>

複製代碼
  • 錯誤提示添加一些樣式,在utils.js文件沒法寫單文件組件,可使用render函數,render函數仍是比較繁瑣的,另外一種時jsx;下面須要把jsx 配置到項目中

安裝babel支持jsx 插件 @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

配置babel

babel.config.js

module.exports = {
  presets: [
    "@vue/app",
    [
      "@vue/babel-preset-jsx",
      {
        injectH: false
      }
    ]
  ],
  plugins: [
    [
      "import",
      { libraryName: "ant-design-vue", libraryDirectory: "es", style: true }
    ] // `style: true` 會加載 less 文件
  ]
};
複製代碼

request.js

import axios from "axios";
import { notification } from "ant-design-vue";

function request(options) {
  return axios(options)
    .then(res => {
      return res;
    })
    .catch(error => {
      const {
        response: { status, statusText }
      } = error;
      notification.error({
        // eslint-disable-next-line no-unused-vars
        message: h => (
          <div>
            請求錯誤 <span style="color: red">{status}</span> : {options.url}
          </div>
        ),
        description: statusText
      });
      return Promise.reject(error);
    });
}

export default request;
複製代碼

建立一個普通表單

在antd vue 官網找到一個基本表單帶佈局

  • 複製代碼至src/view/Forms/BasicForm.vue,這樣一個基本表單就完成了

BasicForm.vue

<template>
  <div>
    <a-form :layout="formLayout">
      <a-form-item
        label="Form Layout"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-radio-group
          default-value="horizontal"
          @change="handleFormLayoutChange"
        >
          <a-radio-button value="horizontal">
            Horizontal
          </a-radio-button>
          <a-radio-button value="vertical">
            Vertical
          </a-radio-button>
          <a-radio-button value="inline">
            Inline
          </a-radio-button>
        </a-radio-group>
      </a-form-item>
      <a-form-item
        label="Field A"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input placeholder="input placeholder" />
      </a-form-item>
      <a-form-item
        label="Field B"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input placeholder="input placeholder" />
      </a-form-item>
      <a-form-item
        :wrapper-col="buttonItemLayout.wrapperCol"
      >
        <a-button type="primary">
          Submit
        </a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

<script>
export default {
  data () {
    return {
      formLayout: 'horizontal',
    };
  },
  computed: {
    formItemLayout () {
      const { formLayout } = this;
      return formLayout === 'horizontal' ? {
        labelCol: { span: 4 },
        wrapperCol: { span: 14 },
      } : {};
    },
    buttonItemLayout () {
      const { formLayout } = this;
      return formLayout === 'horizontal' ? {
        wrapperCol: { span: 14, offset: 4 },
      } : {};
    },
  },
  methods: {
    handleFormLayoutChange  (e) {
      this.formLayout = e.target.value;
    },
  },
};
</script>
複製代碼
  • 表單驗證,antd vue 提供了 validateStatus help hasFeedback 等屬性,你能夠不須要使用 Form.create 和 getFieldDecorator,本身定義校驗的時機和內容。

validateStatus: 校驗狀態,可選 ‘success’, ‘warning’, ‘error’, ‘validating’。 hasFeedback:用於給輸入框添加反饋圖標。 help:設置校驗文案。

BasicForm.vue

...
<a-form-item
        label="Field A"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
        validateStatus="error"
        help="必須大於5個字符!"
      >
        <a-input v-model="FieldA" placeholder="input placeholder" />
</a-form-item>
...
複製代碼
  • 動態輸入時驗證,只要把validateStatushelp改成動態便可
<template>
  <div>
    <a-form :layout="formLayout">
      <a-form-item
        label="Form Layout"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-radio-group
          default-value="horizontal"
          @change="handleFormLayoutChange"
        >
          <a-radio-button value="horizontal">
            Horizontal
          </a-radio-button>
          <a-radio-button value="vertical">
            Vertical
          </a-radio-button>
          <a-radio-button value="inline">
            Inline
          </a-radio-button>
        </a-radio-group>
      </a-form-item>
      <a-form-item
        label="Field A"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
        :validateStatus="FieldAStatus"
        :help="FieldAHelp"
      >
        <a-input v-model="FieldA" placeholder="input placeholder" />
      </a-form-item>
      <a-form-item
        label="Field B"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input v-model="FieldB" placeholder="input placeholder" />
      </a-form-item>
      <a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
        <a-button type="primary" @click="handleSubmit">
          Submit
        </a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formLayout: "horizontal",
      FieldA: "",
      FieldB: "",
      FieldAStatus: "",
      FieldAHelp: ""
    };
  },
  watch: {
    FieldA(val) {
      if (val.length <= 5) {
        this.FieldAStatus = "error";
        this.FieldAHelp = "必須大於5個字符!";
      } else {
        this.FieldAStatus = "";
        this.FieldAHelp = "";
      }
    }
  },
  computed: {
    formItemLayout() {
      const { formLayout } = this;
      return formLayout === "horizontal"
        ? {
            labelCol: { span: 4 },
            wrapperCol: { span: 14 }
          }
        : {};
    },
    buttonItemLayout() {
      const { formLayout } = this;
      return formLayout === "horizontal"
        ? {
            wrapperCol: { span: 14, offset: 4 }
          }
        : {};
    }
  },
  methods: {
    handleFormLayoutChange(e) {
      this.formLayout = e.target.value;
    },
    handleSubmit() {
      if (this.FieldA.length <= 5) {
        this.FieldAStatus = "error";
        this.FieldAHelp = "必須大於5個字符!";
      } else {
        console.dir({ FieldA: this.FieldA, FieldB: this.FieldB });
      }
    }
  }
};
</script>


複製代碼

初始數據、自動校驗、動態賦值

  • this.form = this.$form.createForm(this);註冊表單
  • v-decorator屬性設置組件名稱、初始化值initialValue、表單驗證rules
  • this.form.setFieldsValue({ fieldA: "hello world" }) 動態賦值
<template>
  <div>
    <a-form :layout="formLayout" :form="form">
      <a-form-item
        label="Form Layout"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-radio-group
          default-value="horizontal"
          @change="handleFormLayoutChange"
        >
          <a-radio-button value="horizontal">
            Horizontal
          </a-radio-button>
          <a-radio-button value="vertical">
            Vertical
          </a-radio-button>
          <a-radio-button value="inline">
            Inline
          </a-radio-button>
        </a-radio-group>
      </a-form-item>
      <a-form-item
        label="Field A"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input
          v-decorator="[ 'fieldA', { initialValue: fieldA, rules: [{ required: true, min: 6, message: '必須大於5個字符' }] } ]"
          placeholder="input placeholder"
        />
      </a-form-item>
      <a-form-item
        label="Field B"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input v-decorator="['fieldB']" placeholder="input placeholder" />
      </a-form-item>
      <a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
        <a-button type="primary" @click="handleSubmit">
          Submit
        </a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

<script>
export default {
  data() {
    this.form = this.$form.createForm(this);
    return {
      formLayout: "horizontal",
      fieldA: "hello",
      fieldB: ""
    };
  },
  mounted() {
    setTimeout(() => {
      this.form.setFieldsValue({ fieldA: "hello world" });
    }, 3000);
  },
  computed: {
    formItemLayout() {
      const { formLayout } = this;
      return formLayout === "horizontal"
        ? {
            labelCol: { span: 4 },
            wrapperCol: { span: 14 }
          }
        : {};
    },
    buttonItemLayout() {
      const { formLayout } = this;
      return formLayout === "horizontal"
        ? {
            wrapperCol: { span: 14, offset: 4 }
          }
        : {};
    }
  },
  methods: {
    handleFormLayoutChange(e) {
      this.formLayout = e.target.value;
    },
    handleSubmit() {
      this.form.validateFields((err, values) => {
        if (!err) {
          console.log(values);
          Object.assign(this, values);
        }
      });
    }
  }
};
</script>


複製代碼

建立一個分佈表單

結合vuexview-router作一個比較複雜的表單,第一步保存付款帳戶、收款帳戶信息,第二步請求服務並保存付款帳戶和密碼信息及收款帳戶信息,第三步提示操做信息

  • 建立表單store,src\store\modules\form.js

step狀態保存付款帳戶、收款帳戶信息

submitStepForm action 表單提交請求服務並保存付款帳戶和密碼信息及收款帳戶信

saveStepFormData mutations保存第一步付款帳戶、收款帳戶信息

form.js

import router from "../../router";
import request from "../../utils/request";

const state = {
  step: {
    payAccount: "123456",
    receiverAccount: {
      type: "alipay",
      number: ""
    }
  }
};

const actions = {
  async submitStepForm({ commit }, { payload }) {
    await request({
      url: "/api/forms",
      method: "POST",
      data: payload
    });
    commit("saveStepFormData", { payload });
    router.push("/form/step-form/result");
  }
};

const mutations = {
  saveStepFormData(state, { payload }) {
    state.step = {
      ...state.step,
      ...payload
    };
  }
};

export default {
  namespaced: true,
  state,
  actions,
  mutations
};

複製代碼
  • store 導出,src/store/index.js

index.js

import Vue from "vue";
import Vuex from "vuex";
import form from "./modules/form";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  modules: {
    form
  }
});

複製代碼
  • main.js引入
...
import store from "./store/index.js";
...
複製代碼
  • 編寫第一步src\views\Forms\StepForm\Step1.vue

Step1.vue

<template>
  <div>
    <a-form layout="horizontal" :form="form">
      <a-form-item
        label="付款帳戶"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input
          v-decorator="[ 'payAccount', { initialValue: step.payAccount, rules: [{ required: true, message: '請輸入付款帳號' }] } ]"
          placeholder="請輸入付款帳號"
        />
      </a-form-item>
      <a-form-item
        label="收款帳戶"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input
          v-decorator="[ 'receiverAccount', { initialValue: step.receiverAccount, rules: [{ required: true, message: '請輸入收款帳號' }] } ]"
          placeholder="請輸入收款帳號"
      </a-form-item>
      <a-form-item>
        <a-button type="primary" @click="handleSubmit">下一步</a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

<script>
export default {
  data() {
    this.form = this.$form.createForm(this);
    return {
      formItemLayout: {
        labelCol: { span: 4 },
        wrapperCol: { span: 14 }
      }
    };
  },
  computed: {
    step() {
      return this.$store.state.form.step;
    }
  },
  methods: {
    handleSubmit() {
      const { form, $router, $store } = this;
      form.validateFields((err, values) => {
        if (!err) {
          $store.commit({
            type: "form/saveStepFormData",
            payload: values
          });
          $router.push("/form/step-form/confirm");
        }
      });
    }
  }
};
</script>

<style></style>

複製代碼
  • 第二步src\views\Forms\StepForm\Step2.vue

Step2.vue

<template>
  <div>
    <a-form layout="horizontal" :form="form">
      <a-form-item
        label="付款帳戶"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        {{ step.payAccount }}
      </a-form-item>
      <a-form-item
        label="密碼"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <a-input
          v-decorator="[ 'password', { initialValue: step.payAccount, rules: [{ required: true, message: '請輸入密碼' }] } ]"
          type="password"
          placeholder="請輸入付款密碼"
        />
      </a-form-item>
      <a-form-item>
        <a-button type="primary" @click="handleSubmit">提交</a-button>
        <a-button style="margin-left: 8px" @click="onPrev">上一步</a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

<script>
export default {
  data() {
    this.form = this.$form.createForm(this);
    return {
      formItemLayout: {
        labelCol: { span: 4 },
        wrapperCol: { span: 14 }
      }
    };
  },
  computed: {
    step() {
      return this.$store.state.form.step;
    }
  },
  methods: {
    handleSubmit() {
      const { form, $store, step } = this;
      form.validateFields((err, values) => {
        if (!err) {
          $store.dispatch({
            type: "form/submitStepForm",
            payload: { ...step, ...values }
          });
        }
      });
    },
    onPrev() {
      this.$router.push("/form/step-form/info");
    }
  }
};
</script>

<style></style>

複製代碼
  • 第三步src\views\Forms\StepForm\Step3.vue

Step3.vue

<template>
  <div>操做成功,預計兩小時到帳</div>
</template>

<script>
export default {};
</script>

<style></style>

複製代碼

封裝一個自動校驗的表單項

自定義或第三方的表單控件,也能夠與 Form 組件一塊兒使用。只要該組件遵循如下的約定:

提供受控屬性 value 或其它與 valuePropName-參數) 的值同名的屬性。

提供 onChange 事件或 trigger-參數) 的值同名的事件。

不能是函數式組件。

  • 新建表單項組件src\components\ReceiverAccount.vue

ReceiverAccount.vue

<template>
  <a-input-group compact>
    <a-select v-model="type" style="width: 130px" @change="handleTypeChange">
      <a-select-option value="alipay">支付寶</a-select-option>
      <a-select-option value="bank">銀行帳戶</a-select-option>
    </a-select>
    <a-input
      style="width: calc(100% - 130px)"
      v-model="number"
      @change="handleNumberChange"
    />
  </a-input-group>
</template>

<script>
export default {
  props: {
    value: {
      type: Object
    }
  },
  watch: {
    value(val) {
      Object.assign(this, val);
    }
  },
  data() {
    const { type, number } = this.value || {};
    return {
      type: type || "alipay",
      number: number || ""
    };
  },
  methods: {
    handleTypeChange(val) {
      this.$emit("change", { ...this.value, type: val });
    },
    handleNumberChange(e) {
      this.$emit("change", { ...this.value, number: e.target.value });
    }
  }
};
</script>

<style></style>

複製代碼
  • 引入組件添加驗證

Step1.vue

···
 <a-form-item
        label="收款帳戶"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >
        <ReceiverAccount
          v-decorator="[ 'receiverAccount', { initialValue: step.receiverAccount, rules: [ { required: true, message: '請輸入收款帳號', validator: (rule, value, callback) => { if (value && value.number) { callback(); } else { callback(false); } } } ] } ]"
        />
      </a-form-item>
 ···
複製代碼

管理系統中使用的圖標

iconfont.cn使用

  • 搜索圖標404找到本身喜歡圖標加入購物車,添加至項目中
  • 查看在線連接也能夠下載到本地,使用symbolsvg模式,另外兩項都是字體

  • 引入到項目

src\core\lazy_use.js

lazy_use.js

在 1.2.0 以後,咱們提供了一個 createFromIconfontCN 方法,方便開發者調用在 iconfont.cn 上自行管理的圖標。

const IconFont = Icon.createFromIconfontCN({
  scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成
});
Vue.component("IconFont", IconFont);
複製代碼
  • 使用
<IconFont type="icon-icon-404" style="font-size:100px"></IconFont>
複製代碼

自設計的logo

  • 自設計logo能夠上傳圖片到iconfont.cn項目中,也能夠放到vue項目中引用
<template>
 <div><img :src="logo"></div>
</template>
import logo from "@/assets/logo.svg";
<script>
    data() {
        return {
         logo
        };
      }
</script>
複製代碼
  • 也能夠以組件的形式引入svg圖片

首先安裝包vue-svg-loader

配置vue.config.js 使用

<template>
 <div><logo></logo></div>
</template>
import Logo from "@/assets/logo.svg";
<script>
 components: {
    Logo
  }
</script>
複製代碼

vue.config.js

const { createMockMiddleware } = require("umi-mock-middleware");

module.exports = {
  lintOnSave: false,
  css: {
    loaderOptions: {
      less: {
        javascriptEnabled: true
      }
    }
  },
  chainWebpack: config => {
    const svgRule = config.module.rule("svg");

    // 清除已有的全部 loader。
    // 若是你不這樣作,接下來的 loader 會附加在該規則現有的 loader 以後。
    svgRule.uses.clear();

    // 添加要替換的 loader
    svgRule.use("vue-svg-loader").loader("vue-svg-loader");
  },
  devServer: {
    port: 8000,
    open: true,
    // 解析body,對接真實服務端環境須要註釋掉
    before: function(app) {
      // var bodyParser = require("body-parser");
      // app.use(bodyParser.json());
      if (process.env.MOCK !== "none") {
        app.use(createMockMiddleware());
      }
    },
    proxy: {
      "/api": {
        target: "http://localhost:3000"
      }
    }
  }
};

複製代碼
  • 導出配置vue inspect >output.js
相關文章
相關標籤/搜索