Vue CLI3搭建Antd-vue项目
Vue CLI 3快速创建项目
- 输入命令 vue create ant-design-vue-pro创建项目
- 按上下键选择Manually select features(手动选择功能)项,default (babel, eslint)(默认安装)
default(babel,eslint):默认设置(直接enter,没有带任何辅助功能的 npm包
Manually select features:手动配置是我们项目所需要的npm包
- 按上下键选择要安装的功能,按空格键确定,选择完成之后回车进行下一步
- 按上下键和回车键选择要安装的功能;
自定义Webpack和Babel配置
- antd按需引入组件依赖,输入命令npm i --save-dev babel-plugin-import安装
babel-plugin-import
,并且修改文件babel.config.js
如下
module.exports = {
presets: ["@vue/app"],
+ plugins: [
+ [
+ "import",
+ { libraryName: "ant-design-vue", libraryDirectory: "es", style: true }
+ ]
+ ]
};
- 输入命令 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.js
module.exports = {
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.vue
和Register.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:抽离出用于登陆注册页面的通用布局
<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:基础页面布局,包含了头部导航,底部信息,侧边栏和内容部分
<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.vue
<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 时隐藏 trigger
然后自定义a-icon 图片及位置
最后图片添加click事件控制菜单切换
BasicLayout.vue
<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等其他第三方库
- 输入命令安装echarts
npm 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>
...
- 动态输入时验证,只要把
validateStatus
和help
改为动态即可
<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>
创建一个分布表单
结合
vuex
和view-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
找到自己喜欢图标加入购物车,添加至项目中 - 查看在线链接也可以下载到本地,使用
symbol
svg模式,另外两项都是字体
- 引入到项目
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.config.js
配置我们想要的主题
- 设置的变量可以找到如下图
- 社区提供antd-主题的WebPack-插件,此webpack插件用于生成特定颜色的less / css并注入到您的index.html文件中,以便您可以在浏览器中更改Ant Design特定颜色主题
https://github.com/mzohaibqc/antd-theme-webpack-plugin
vue.config
const path = require("path");
const webpack = require("webpack");
const AntDesignThemePlugin = require("antd-theme-webpack-plugin");
const { createMockMiddleware } = require("umi-mock-middleware");
const options = {
antDir: path.join(__dirname, "./node_modules/ant-design-vue"),
stylesDir: path.join(__dirname, "./src"),
varFile: path.join(
__dirname,
"./node_modules/ant-design-vue/lib/style/themes/default.less"
),
mainLessFile: "",
themeVariables: ["@primary-color"],
generateOnce: false
};
const themePlugin = new AntDesignThemePlugin(options);
module.exports = {
lintOnSave: false,
css: {
loaderOptions: {
less: {
modifyVars: {
"primary-color": "#1DA57A"
},
javascriptEnabled: true
}
}
},
configureWebpack: {
plugins: [themePlugin, new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)]
},
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"
}
}
}
};
public\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>ant-design-vue-pro</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<link rel="stylesheet/less" type="text/css" href="/color.less" />
<script>
window.less = {
async: false,
env: 'production',
javascriptEnabled: true,
modifyVars: {
"primary-color": "#1DA57A"
},
};
</script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>
</body>
</html>
国际化
ant-design-vue 目前的默认文案是英文,如果需要使用其他语言,可以参考下面的方案。
LocaleProvider
首先LocaleProvider组件注册到项目
LocaleProvider使用,引入中英文语言包,日历组件
header 头部添加中英文切换按钮,现在日历就可以中英文切换了,但这是moment库提供;自己组件国际化的需要安装
vue-i18n
库 main.js引用,新建语言包,Analysis.vue中使用
App.vue
<template>
<div id="app">
<a-locale-provider :locale="locale">
<router-view />
</a-locale-provider>
</div>
</template>
<script>
import zhCN from "ant-design-vue/lib/locale-provider/zh_CN";
import enUS from "ant-design-vue/lib/locale-provider/en_US";
import moment from "moment";
import "moment/locale/zh-cn";
export default {
data() {
return {
locale: zhCN
};
},
watch: {
"$route.query.locale": function(val) {
this.locale = val === "enUS" ? enUS : zhCN;
moment.locale(val === "enUS" ? "en" : "zh-cn");
}
}
};
</script>
<style lang="less">
#app {
height: 100%;
}
</style>
Header.vue
<template>
<div class="header">
<notice-icon-view />
<a-dropdown>
<a-icon type="global" />
<a-menu
slot="overlay"
@click="localeChange"
:selectedKeys="[$route.query.locale || 'zhCN']"
>
<a-menu-item key="zhCN">
中文
</a-menu-item>
<a-menu-item key="enUS">
English
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</template>
<script>
export default {
methods: {
localeChange({ key }) {
this.$router.push({ query: { ...this.$route.query, locale: key } });
this.$i18n.locale = key;
}
}
};
</script>
<style scoped>
.header {
float: right;
margin-right: 30px;
}
</style>
Analysis.vue
<template>
<div>
{{ $t("message")["app.dashboard.analysis.timeLabel"] }} :
<a-date-picker></a-date-picker>
<Chart :option="chartOption" style="height: 400px" />
</div>
</template>
main
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store/index.js";
import VueI18n from "vue-i18n";
import enUS from "./locale/enUS";
import zhCN from "./locale/zhCN";
import queryString from "query-string";
// 引入 按需组件的统一引入文件
import "./core/lazy_use";
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: queryString.parse(location.search).locale || "zhCN",
messages: {
zhCN: { message: zhCN },
enUS: { message: enUS }
}
});
Vue.config.productionTip = false;
new Vue({
i18n,
router,
store,
render: h => h(App)
}).$mount("#app");
高效地构建打包发布
执行
npm run build -- --report