# vue-admin-work
**Repository Path**: leungyh/vue-admin-work
## Basic Information
- **Project Name**: vue-admin-work
- **Description**: Vue2 + Element UI + Webpack(后台管理系统)
- **Primary Language**: JavaScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 1
- **Created**: 2022-02-11
- **Last Updated**: 2022-06-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Vue, webpack, Element-UI
## README
vue-admin-work说明文档
版本:v0.1.0-rc
### 演示地址
[⭐ vue-admin-work](https://leungyh.gitee.io/vue-admin-work)
[⭐ vue-admin-work(备份)](https://leungyh.gitee.io/vue-admin-work/#/login)
### 介绍
[vue-admin-work]()是一款后台前端框架,是基于 [vue](https://github.com/vuejs/vue) 和 [element-ui](https://github.com/ElemeFE/element)实现。它使用了最新的前端技术栈,内置了动态路由,权限验证,把后台常用的一些操作进行了封装,只需要简单的配置就可以实现常用的功能,同时也提供了丰富的功能组件,最大程度上满足你在后台前端开发中遇到的业务场景。
#### 优势及注意事项
```tex
vue-admin-work 有如下优势:
1. 支持前端控制路由权限和后端控制路由模式
2. 支持 mock,完全独立于后台
3. 提供了非常多的 mixin 代码块,方便集成各种功能
4. 内置了大量常用的组件,比如,上传,消息提示等
5. 支持多主题、多布局切换
使用注意事项:
1. 项目默认使用使用vscode工具进行开发,也是唯一推荐的开发工具
2. 项目默认eslint校验规范
```
#### 功能
```json
- 登录、注销
- Dashboard
- 主页
- 工作台
- 系统管理
- 部门管理
- 用户管理
- 角色管理
- 菜单管理
- 列表页面
- 表格操作
- 表格搜索
- 卡片列表
- 表单页面
- 表单操作
- 高级表单
- 分步表单
- 通知提示
- 结果页面
- 成功页面
- 失败页面
- 异常页面
- 404
- 403
- 500
- 编辑器
- 富文本
- markdown
- 其它功能
- 打印
- 外链
- 二维码
- 未完待续……
- 全局功能
- 多种动态换肤
- 动态侧边栏(支持多级路由嵌套)
- 动态面包屑
- 快捷导航(标签页)
- Svg Sprite 图标
- 本地/后端 mock 数据
- Screenfull全屏
- 消息提醒
- 自适应收缩侧边栏
- 系统配置
```
#### 项目目录
```shell
├── README.md
├── babel.config.js
├── jest.config.js
├── jsconfig.json
├── mock // 随机生成数据插件
│ ├── base.js
│ ├── index.js
│ ├── list
│ ├── router
│ └── user
├── package-lock.json // 依赖包详情
├── package.json // 依赖包
├── public // 公共文件夹
│ ├── favicon.ico
│ ├── index.html
│ └── static
├── src
│ ├── App.vue // 项目页面入口文件
│ ├── main.js // 程序入口文件,加载各种公共组件
│ ├── api // api(服务类)
│ ├── assets // 静态资源
│ ├── components // 公共组件
│ ├── directive // 全局指令
│ ├── icons // svg图标
│ ├── layouts // 布局
│ ├── mixins // 注入
│ ├── model // 封装增删查改函数
│ ├── router // 路由
│ ├── store // vuex
│ ├── styles // 公共样式
│ ├── utils // 工具
│ └── views // 页面
└── vue.config.js
```
#### layout布局目录
```shell
├── layouts
│ ├── actions // 导航栏 - 工具类
│ ├── avatar // 导航栏 - 用户类
│ ├── breadcrumb // 导航栏 - 面包屑
│ ├── footer // 底部
│ ├── header // 头部
│ ├── humburger // 侧边栏 - 收缩键
│ ├── logo // 侧边栏 - 图标
│ ├── message // 导航栏 - 工具类 - 消息通知
│ ├── navbar // 导航栏
│ ├── setting // 应用配置(主题切换)
│ ├── sidebar // 侧边栏
│ ├── store // 封装控制layouts状态函数
│ ├── svg-icon // svg复用组件
│ ├── tabbar // 标签栏
│ ├── index.js // 加载layouts组件脚本
│ ├── Layout.vue // Layout主体
│ ├── Main.vue // 内容
│ ├── MainLayout.vue // 内容布局
│ └── RouterViewLayout // 二级路由载体
```
#### views页面目录
```shell
├── views
│ ├── authority // 系统管理页
│ ├── draggable // 拖拽页
│ ├── editor // 编辑器
│ ├── excel // Excel功能页
│ ├── exception // 异常页面
│ ├── form // 表单
│ ├── index // Dashboard页
│ ├── list // 列表(表格、卡片)
│ ├── login // 登录页
│ ├── message // 通知页
│ ├── next-page // 下一页
│ ├── other // 其他功能页
│ ├── personal // 个人中心
│ ├── redirect // 刷新页
│ ├── result // 结果页
│ └── system // 系统栏(未用)
```
启动完成后会自动打开浏览器访问 [http://localhost:5566](http://localhost:5566/), 代表运行成功。
#### 开发工具
本项目开发工具是:[**vscode**](https://code.visualstudio.com/) 。也是作者推荐的开发工具,结合一些丰富的插件让你在开发过程中体验更加美好
> 推荐几款常用的开发**vue**的插件
>
> - [vetur](https://github.com/vuejs/vetur) 开发 vue 项目必备插件
> - **Auto Close Tag** 自动闭合 HTML/XML 标签
> - **Auto Rename Tag** 自动完成另一侧标签的同步修改
> - **Path Intellisense** 自动路径补全
> - **HTML CSS Support** 让 html 标签上写 class 智能提示当前项目所支持的样式
以上插件是作者常用的几款,并不代表必须只需要这些,在实际开发中根据个人情况来定。
### 布局-流程
#### Layout 布局
页面的整体布局包括:侧边栏、导航栏、快捷标签、主页面四个部分组成。基中侧边栏最上面是一个企业 logo 容器,可以根据实际情况更新 logo 和 标题。
对应的源码是:**src/layout/index.vue**。
> 注:此 layout 以后有可能会单独抽出成为一个独立的项目,以方便添加更多的布局样式。
#### SPA 工作流程
SPA 的是全称是:**Single Page Application** 中文意思是:单页面应用
结合[**Vue Router**](https://router.vuejs.org/)看一下匹配过程:
- 首先是 **App.vue**
```vue
```
App.vue 中的 ****匹配到的组件是**Layout**,这里的**Layout**就是 **src/layout/index.vue**
- 其次是 **Layout**(src/layout/index.vue),部分源码如下:
```vue
```
在**Layout**组件中包含了一下**AppMain**组件
- 最后是**AppMain**,部分源码如下:
```vue
```
可以看到**AppMain**中又包含了一个 组件,这样在**AppMain**又可以继续匹配路由了
- 举例说明一下:
在浏览器地址栏里输入以下链接是如何显示出页面的,这里忽略登录状态,假定用户已经登录,输入此链接会直接显出当前页面
http://localhost:5566/#/index/main
看一下部分路由表配置:
```js
{
path: '/index',
name: 'index',
component: Layout,
hidden: false,
meta: {
title: 'Dashboard',
icon: 'dashboard'
},
children: [
{
path: 'main',
name: 'Main',
component: () => import('@/views/index'),
meta: {
title: '主控台',
affix: true,
cacheable: true
}
}
]
},
```
当匹配到了 **'/index'** 路径是的时候,会加载对应的**Layout**组件,Layout 组件中有一个 AppMain 组件,里面又包含了一个 ,同时可以看到路由表配置还有一个 **children**属性,完全匹配到**'/index/main'**路径的时候,path 为'main'所对应的 component 组件被加载。
大体就是这么一个匹配过程,具体的可以看 **[Vue Router](https://router.vuejs.org)**官网中[**嵌套路由文档**](https://router.vuejs.org/zh/guide/essentials/nested-routes.html)
### 路由
路由和侧边栏是一个后台管理项目的重点,也是一个比较难理解的地方
#### 知识准备
- **Vue Router 知识**
- **element-ui 中的 ElMenu 组件和 ElScrollbar 组件**
- **router-link 和 router-view 组件**
#### 说明
本项目路由生成的思路如下:
1. 用户登录成功之后获取**token**和**role**
2. 通过后台接口查询该用户所对应角色的菜单列表
3. 前端处理获取到的菜单列表,按一定的规则动态生成路由表
4. 通过**vue-router**实例的**.addRoutes()**方法动态添加路由
#### 原始路由信息
以**editor**角色为例,从后台获取的原始路由信息如下:
```js
[
{
// 菜单地址
menuUrl: "/list",
// 菜单名称
menuName: "列表页面",
// 菜单图图标
icon: "list",
// 所包含的子菜单
children: [
{
menuUrl: "/list/table",
menuName: "表格操作"
},
{
menuUrl: "/list/table-with-search",
menuName: "表格搜索"
},
{
menuUrl: "/list/grid-list",
menuName: "卡片列表"
}
]
},
{
menuUrl: "/form",
menuName: "表单页面",
// 菜单提示信息
tip: "circle",
icon: "form",
children: [
{
menuUrl: "/form/base-form-view",
menuName: "表单操作",
cacheable: true
},
{
menuUrl: "/form/advance-form",
menuName: "高级表单",
cacheable: true
},
{
menuUrl: "/form/step-form",
menuName: "分步表单"
},
{
menuUrl: "/form/tip",
menuName: "通知提示"
}
]
},
{
menuUrl: "/editor",
menuName: "编辑器",
tip: "12",
icon: "editor",
children: [
{
menuUrl: "/editor/rich-text",
menuName: "富文本"
},
{
menuUrl: "/editor/markdown",
menuName: "markdown"
}
]
},
{
menuUrl: "/other",
menuName: "其它功能",
children: [
{
menuUrl: "/other/print",
menuName: "打印"
},
{
menuUrl: "http://www.baidu.com",
menuName: "外链"
},
{
menuUrl: "/other/qrcode",
menuName: "二维码"
}
]
}
];
```
#### 前端路由信息配置项
从后台获取到的原始路由信息经过如下函数处理,最终生成我们所需要的路由信息:
```js
function generatorRoutes(res) {
const tempRoutes = [];
res.forEach(it => {
const route = {
//url 信息
path: it.menuUrl,
// 设定路由的名字,建议一定要设置此name,因为有可能根据此配置跳转页面,在缓存页面的时候本项目也是采用此配置来保存的
name: getNameByUrl(it.menuUrl),
// 当设置 true 的时候该路由不会在侧边栏出现,如login 404 等页面
hidden: !!it.hidden,
// 对应的vue组件
component: isMenu(it.menuUrl) ? Layout : getComponent(it.menuUrl),
meta: {
// 路由标签名字,主要用在 快捷标签 栏和导航栏中
title: it.menuName,
// 设置为true,标识着在 快捷标签 中不会有关闭按钮
affix: !!it.affix,
// 设置为true,标识着可以被组件缓存
cacheable: !!it.cacheable,
// 路由的图标信息
icon: it.icon || "",
// 路由的提示信息,目前有三种提示方式:new、小圆点、数字,对应的 tip:new、circle、12(具体的数字)
tip: it.tip
}
};
if (it.children) {
// 子路由
route.children = generatorRoutes(it.children);
}
tempRoutes.push(route);
});
return tempRoutes;
}
```
> TIP
>
> 通过 include 缓存的时候是根据组件的 name 字段来缓存,所以最好是给每一个组件都设置一下 name 属性,而且要和 route 配置项中的 name 保持一致,因为在保存 name 的时候是根据 route.name 配置项来保存的
#### 最终路由表
经过上两步动态生成的路由表还不够,有一些页面是不需要动态生成的,也就是说是**不需要权限**的,如:**login 页面** 、**404、500**等页面,当然本项目为了演示把**主页和工作页**也做成了固定页面,在实际项目根据需要自行添加删除。
在 **src/router/index.js**文件中所有的固定路由如下:
```js
export const routes = [
{
path: "/redirect",
component: Layout,
hidden: true,
children: [
{
path: "/redirect/:path(.*)",
component: () => import("@/views/redirect/index")
}
]
},
{
path: "/login",
name: "login",
component: () => import("@/views/login"),
hidden: true
},
{
path: "/personal",
name: "personal",
component: Layout,
hidden: true,
children: [
{
path: "index",
name: "personalCenter",
component: () => import("@/views/personal"),
meta: {
title: "个人中心"
}
}
]
},
{
path: "/",
name: "root",
redirect: "/index/main",
hidden: true
},
{
path: "/index",
name: "index",
component: Layout,
hidden: false,
meta: {
title: "Dashboard",
icon: "dashboard"
},
children: [
{
path: "main",
name: "Main",
component: () => import("@/views/index"),
meta: {
title: "主控台",
affix: true,
cacheable: true
}
},
{
path: "workplace",
name: "WorkPlace",
component: () => import("@/views/index/work-place"),
meta: {
title: "工作台",
cacheable: true
}
}
]
},
{
path: "/404",
component: () => import("@/views/exception/404"),
hidden: true
},
{
path: "*",
redirect: "/404",
hidden: true
}
];
```
再把动态生成的路由信息通过 **.addRoutes()**方法添加到路由实例中就形成了本项目中所需要的所有路由信息表
```js
router.beforeEach((to, from, next) => {
NProgress.start();
if (to.name === "login") {
next();
NProgress.done();
} else {
if (!isTokenExpired()) {
next(`/login?redirect=${to.path}`);
NProgress.done();
} else {
const isEmptyRoute = store.getters["user/isEmptyRoutes"];
if (isEmptyRoute) {
// 加载路由
const accessRoutes = [];
getRoutes().then(async routes => {
accessRoutes.push(...routes);
await store.dispatch("user/saveRoutes", accessRoutes);
router.addRoutes(accessRoutes);
next({ ...to, replace: true });
});
} else {
next();
}
}
}
});
```
### 侧边栏
#### 知识准备
- **[element-ui](https://element.eleme.cn/) 框架中的 ElMenu 组件**
- **[Vuex](https://vuex.vuejs.org/)**
#### 数据来源
通过上面的分析说明,当不同用户登录成功之后,会通过**role**来动态加载菜单,从而生成路由表。然后我们把生成的路由表信息存储到 **vuex** 中
#### 嵌套路由
本框架**通过递归的方式**支持多级路由的形式,不过为了用户的体验最好是不要超过三级路由,两级路由就已经满足了大部分的需求。如果在实际开发中真的需要三级路由,请不忘记在二级的页面中加入,如:
```vue
```
#### 外链
通过配置路由项中的 **path** 属性来实现外链功能,本框架通过判断 **path** 属性值是否以 **https://** 或者 **http://** 开头,如果是以两种情况下开头,则会认为是外链,在点击菜单的时候就会打开一个新的页面打开链接
```js
{
"path": "external-link",
"component": Layout,
"children": [
{
"path": "https://leungyh.gitee.io/vue-admin-work/#/login",
}
]
}
```
#### 默认展开菜单
本框架并没有加入此功能,如果想要实现此功能,也很简单,只需要配置 **el-menu** 组件的 **default-openeds** 属性就好。具体参考 **[element-ui 中的 NavMenu 导航菜单](https://element.eleme.cn/#/zh-CN/component/menu)**
### 面包屑
#### 知识准备
- **Element-ui 中 Breadcrumb 组件**
- **Vue 中的 watch 用法**
#### 实现思路
通过 vue 组件中的 watch 监听 \$route 变化来动态生成。部分源码如下:
```js
data() {
return {
breadcrumbs: []
}
},
watch: {
$route() {
this.generateBreadcrumb()
}
},
methods: {
generateBreadcrumb() {
this.breadcrumbs = xxxxx
},
}
```
### 快捷标签
#### 知识准备
- **element-ui 中 Tabs 组件**
- **Vuex**
效果如下图:
此页面比较简单,但是所需要的技术含量还是比较多的。如下:
- **需要定制 tabs 的样式**
- **需要理解 Vuex 几个特性**
- **需要点右键弹出上下文菜单**
- **可以刷新当前页面**
- **刷新页面的时候,访问过的页面信息可以保留,也就是可以持久化**
#### 实现思路
通过监听 \$route 动态变化 把当前的路由信息保存,然后通过 tabs 展示形式显示出已经保存的页面信息
#### 右键弹出上下文菜单
如要在**PC**端弹出上下文菜单,可以通过**@contextmenu.native.prevent=""**事件来实现,代码如下:
```vue
```
> TIP
>
> @contextmenu.native.prevent 是写在 el-tabs 组件上面的,而不是写在 el-tab-pane 组件上面,如果写在子组件上不会有效果
```js
onContextMenu(item, ctx) {
const { clientX, clientY } = ctx
const { x } = this.$el.getBoundingClientRect()
const parentElementRect = document.getElementById('tagViewTab')
.getElementsByClassName('el-tabs__nav is-top')[0].getBoundingClientRect()
if (clientX < parentElementRect.x) {
return
}
if (clientX > parentElementRect.x + parentElementRect.width) {
return
}
this.selectRoute = null
this.selectRoute = this.visitedRoutes.find(it => {
const { x, width } = document.getElementById('tab-' + it.path).getBoundingClientRect()
if (x < clientX && clientX < (x + width)) {
return it
}
})
if (this.selectRoute) {
this.showLeftMenu = this.isLeftLast(this.selectRoute)
this.showRightMenu = this.isRightLast(this.selectRoute)
const screenWidth = document.body.clientWidth
this.contextMenuStyle.left = ((clientX + 130) > screenWidth ? clientX - 130 - x - 15 : clientX - x + 15) + 'px'
this.contextMenuStyle.top = clientY + 'px'
this.showContextMenu = true
}
},
```
#### 刷新当前页面
本框架采用的刷新方式是通过 **redirect** 的页面,当做中间页面,当刷新页面的时候,就加载 **redirect** 页面,
当加载完成 **redirect** 页面的时候,在 **created** 生命周期函数中再**replace** 跳转回来。
实现方法还有好多种,可以按自己的喜好实现就好,如果不想自己实现用本框架的也可以
#### 持久化路由信息
用过 vuex 的人都知道,vuex 中保存的信息是放在内存中的,当刷新浏览器的时候,内存的数据也会清空,就导致 vuex 保存的信息会丢失。体现到页面中就是已经访问过的页面,在刷新一下浏览器的时候,页面信息会丢失。
所以本框架采用的把 vuex 中的数据持久化到 **localStorage** 中,在合适的时机再把数据从 **localStorage** 中恢复出来,这样就可以实现已经访问过的页面在刷新浏览器的时候不会丢失。
```js
PERSISTENT_VISITED_ROUTES(state, rootState) {
const tempPersistendRoutes = state.visitedRoute.map(it => {
return {
fullPath: it.fullPath,
meta: it.meta,
name: it.name,
params: it.params,
path: it.path,
query: it.query
}
})
localStorage.setItem(rootState.user.userName + '_visited', JSON.stringify(tempPersistendRoutes))
},
```
#### 固定页面
有些页面是不可以删除的,如本框架中:工作台页面。这就需要在 路由配置项中的 **meta** 中配置一个属性: **affix** 设置为 **true** 就可以了
### 新增页面
在讲解这方面的知识之前,需要先明确几个概念以及它们之间的关系:
+ 浏览器地址栏路径,如 `http://xxxxx.com/#/page/index `中的 `/page/index`和路由配置项中的 `path`属性
+ 项目中文件夹及文件的命名
+ 路由配置项中的 `name` 属性
+ 页面组件中 `name`属性
如果我们要添加一个页面,需要在侧边档中显示,则需要先在`菜单管理`页面中添加对应的页面信息。再提交到后台中,再分配给不同的`角色`。最后在项目的`views`文件夹下面创建对应的目录和文件
如果我们要添加的页面,不需要在侧边栏中显示,则需要先在项目的 `router`文件夹下面的 `index.js`中的 `routes`添加路由信息。最后在项目的`views`文件夹下面创建对应的目录和文件
不管在侧边栏显示与否,都得需要在 `views`文件夹下创建对应的目录和文件。
上面几个概念的关系:
> + 如在`菜单管理`中动态添加的页面的`地址`为 `/system/role-info`,则需要项目的 `views`文件夹下面创建 `system`目录,并且创建一个名为 `role-info`的**vue**组件,如:`role-info.vue`。切记,**菜单的地址**要和文件夹和文件的命名保持一致,否则,就会找不到相应的组件。
>
> + 如在项目中的 `router/index.js`中的 `routes`属性添加的路由信息,如下:
>
> ```js
> {
> path: '/next-page',
> name: 'nextPage',
> component: Layout,
> hidden: true,
> children: [
> {
> path: 'info',
> name: 'nextPageInfo',
> component: () => import('@/views/next-page/details.vue'),
> meta: {
> title: '下一页详情'
> }
> }
> ]
> }
> ```
>
> 要访问 `nextPageInfo`页面的地址是:`/next-page/info`。因为我们是手动指定的 `component`,所以页面地址可以和文件夹及文件的名字不一样,但是为了统一管理和项目的可读性,**还是建议要把`页面地址`和`文件夹及文件`的名字一致。**
>
> + 路由配置项中的`name`属性和组件中的`name`属性,我们建议两者要保持一致。通过 `菜单管理`动态添加 的页面,在动态生成路由`name`属性的时候是用的最后一个地址,如:`/next-page/page-info`,框架已经自动把`page-info`转成了`PageInfo`,`name`属性就是`PageInfo`而组件中的`name`属性要写成`PageInfo`。所以两者也是一致的。
>
> 通过手动在项目添加的路由也要遵守这个规则,要保持一致
#### 需要在侧边栏显示
1. 在 **菜单管理** 中添加一个菜单
2. 在项目中**views**目录添加对应的 **.vue** 组件,如果添加的是一个二级页面,则只需要找到一级页面的目录,新增一个 **.vue** 文件;如果是添加的是一个一级页面,则需要在 **views** 目录下创建对应的目录,然后再在该目录 里面创建对应的 **.vue** 文件
3. 再给某个**角色**分配这个页面
#### 不需要在侧边栏显示
有些页面不需要在侧边栏显示,如 **文章详情** 页面,可以按以下步骤添加一个新页面
1. 在 **src/router/index.js**中的 **routes** 常量中添加一个路由配置,如添加**个人中心**页面
```js
{
path: '/personal',
name: 'personal',
component: Layout,
// 一定要把 hidden 属性设置成 true,否则就会在侧边栏中显示出来了
hidden: true,
children: [
{
path: 'index',
name: 'personalCenter',
component: () => import('@/views/personal'),
meta: {
title: '个人中心'
}
}
]
}
```
> TIP
>
> 1. 在不在侧边栏显示是根据 路由配置项 是的 hidden 属性来控制的
> 2. 本框架中所有的数据都是通过 mock 中来的,并没有一个真正的后台环境,所以很多情况都是模拟的,只是演示出效果。
### 其它
#### 网络请求
网络请求一直都是前后端分享项目的重中之重,真实环境下一个后台管理系统不可能离开后台接口而独自运行,否则没有实际意义。
前端对接后台接口的几个步骤:
1. 前端 UI 组件产生交互操作;
2. 发起网络请求,可以是 **ajax** 也可以是 **fetch**;
3. 获取服务端返回的数据,并处理数据;
4. 更新页面显示;
本框架采用的的请求框架是 **[axios](http://www.axios-js.com/)**,是一款非常优秀的网络请求框架,也是对原生的**XHR**的封装,支持很多特性,如:**promise**
为了更好,更方便的使用,本框架对网络请求这块做了大量的工作,对于一般的 **CRUD** 操作都做了封装,只需要简单的配置就可以,下面看一下项目的网络整体架构图:
- 最底层是**axios**的配置文件,里面封装了 **basURL、interceptors.request、interceptors.response** 等一些信息,我们所有的网络请求最终都会调用 **axios** 的方法
- 再往上一层是框架自己封装的 **http** 常用操作,包含了 **get** 和 **post** 两种请求方法,并且放在了 Vue 函数的原型链上,方便了组件的灵活调用
- 再上一层是业务逻辑方法的封装,包括 **查询、模糊查询、增加、删除、修改、更新**等操作,是以**Vue**框架中的**Mixins**的形式存在,方便注入调用
- 最上面的是平时用的组件页面,如:最常用表格页面,表单页面,这些**Vue**组件可以按需引入不同的**Mixin**,如一个表格页面只用到了查询功能,那在配置**Vue**的时候只要混入**GetDataMixin**就好,如下:
```js
import { GetDataMixin } from "@/mixins/ActionMixin";
export default {
mixins: [GetDataMixin]
};
```
#### 请求具体流程
在实际开发过程中,我们需要和后台开发人员一起配合对接接口。
1. 配置 **axios** 的 **baseURL**
2. 在 **src/api/url.js** 文件中添加请求路径,如下:
```js
export const getArticleList = "/article/getList";
```
一定要通过 export 把接口名显露出去,否则在别的文件中不能获取到
3. 在 **ArticleList.vue** 文件中添加加载数据功能,如下:
```js
import { GetDataMixin } from "@/mixins/ActionMixin";
export default {
name: "ArticleList",
mixins: [GetDataMixin],
data() {
return {
articleList: []
};
},
mounted() {
// 初始化加载请求功能
this.initGetData({
// 通过 $urlPath 获取 之前已经配置好了的 getArticleList 路径
url: this.$urlPath.getArticleList,
params: () => this.withPageInfoData(),
beforeAction: () => {
this.tableLoading = true;
},
afterAction: () => {
this.tableLoading = false;
},
onResult: res => {
this.articleList = res.list;
}
}).then(() => {
this.getData();
});
}
};
```
#### MockJs
因为本框架是一个纯前端的项目,并没有真正的对接后台接口,所以使用 [mockjs](https://github.com/nuysoft/Mock)来模拟数据。其原理如下:
**拦截了所有的请求并代理到本地,然后进行数据模拟**
##### 添加新的数据
1. 在项目的 **mock** 文件夹下面添加想要模拟的**js**文件,如:**article.js**, 里面添加要请求的地址如:
```js
Mock.mock(RegExp(getArticleList), function({ body }) {
const { page, pageSize = 10 } = JSON.parse(body);
const size = computePageSize(totalSize, page, pageSize);
return Mock.mock({
...baseData,
totalSize,
[`data|${size}`]: [
{
id: function() {
return Random.string(10);
},
image: Random.image("300x600", "#50B347", "#FFF", "vue-admin-work"),
description: function() {
return Random.csentence(50, 200);
},
"price|1000-9999.2": 100
}
]
});
});
```
2. 在 **mock** 文件下面引入刚才添加的 **article.js**文件:
```js
import "./article.js";
```
这样就可以了
##### 移除 Mock 数据
如果后台人员开完了某个接口,需要对接正式的接口了,只需要把对就的 **mock** 下对应的接口删除了即可
如果后台人员把所有的接口都开发完了,不需要本地模拟了,只需要在 **main.js** 中把对应的 **mock** 有关依赖删除了就好,如:
```js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./icons";
import "./utils";
import "@/styles/index.scss";
import "./api/http";
// 不需要 mock 只需要把下面代码注释了即可
// import '../mock'
import "@/assets/theme/blue/index.css";
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
```
#### 跨域问题
##### 产生原因
跨域问题真的是在前端开发中最常见,问的最多的问题,很多人根本不明白倒底什么是跨域。其实跨域是浏览器的一种行为,是为了保护网站的一种方式,首先肯定的一点是出于安全的角度才设计出来的这样一种同源策略。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
产生的原因也很简单,只要当一个请求 url 的**协议、域名、端口**三者之间任意一个与当前页面 url 不同即为跨域
| **当前页面 url** | **被请求页面 url** | **是否跨域** | **原因** |
| ------------------------ | ------------------------------- | ------------ | ---------- |
| http://www.xxx.com/ | http://www.xxxx.com/index.html | 否 | 同源 |
| http://www.xxx.com/ | https://www.xxxx.com/index.html | 是 | 协议不同 |
| http://www.xxx.com/ | http://www.yyy.com/ | 是 | 主机不同 |
| http://www.xxx.com/ | http://test.xxx.com/ | 是 | 子域名不同 |
| http://www.xxx.com:8080/ | http://www.xxx.com:80/ | 是 | 端口不同 |
##### 解决办法
###### cors
**cors** :全称 Cross Origin Resource Sharing(跨域资源共享),前端基本不需要做什么配置,和原来的写法基本一样,主要是后台人员得配置一些东西。更多的内容请参考 **[阮一峰的《跨域资源共享 CORS 详解》](https://www.ruanyifeng.com/blog/2016/04/cors.html)**介绍的非常清楚
###### 代理
这种方式只需要前端人员配置就好,如:webpack 中 proxy,但是这种方式只能在开发阶段使用,正式环境下不可以使用,其实本质就是在本地开启了一个代理服务器,所有的请求都转发到这个代理服务器,保持同源,从而不会产生跨域的问题。另外也可以在正式的环境下通过配置 **nginx** 来实现代理服务器的功能。两种方式的原理基本相同
个人更推荐 **cors**这种方式,无论是开发阶段还是正式环境下都可以使用,最重要的是我们前端不需要做任何东西就可以使用。
#### 常用功能
本框架中封装了很多的常用的操作,以及业务逻辑和常用的组件,正是因为有了这些小的功能单元才组成了这样复杂的逻辑
##### 表格
一个后台管理系统大部分的功能,可以说 80%的逻辑功能是由**表格和表单**支撑起来的,所以本框架也对**表格和表单**做了大量的封装。当然,如果你不喜欢作者封装的这些功能,也可以自己的方式书写,总之,能把功能实现出来就好
###### **网络(CRUD)Mixin**
- **GetDataMixin**
普通的加载接口数据,在使用时首页要通过 `import` 引入,如:
```js
import { GetDataMixin } from "@/mixins/ActionMixin";
```
然后可以在 `mounted`生命周期函数中初始化,如:
```js
mounted() {
this.initGetData({
url: this.$urlPath.getTableList,
params: () => this.withPageInfoData(),
beforeAction: () => {
this.tableLoading = true
},
afterAction: () => {
this.tableLoading = false
},
onResult: (res) => {
this.handleSuccess(res)
}
}).then(() => {
this.getData()
})
}
```
`this.initGetData()`方法在返回一个 `Promise`对象,这样做的目地是方便,在初始化配置之后可以加载接口数据。
下面分析一下 `initGetData`方法的参数信息:
```js
function initGetData({ url, method, params, beforeAction, onResult, onError, afterAction }) : Promise
```
该方法接收一个对象类型的参数,该对象可以配置的属性有:
- url:必填,否则会抛出异常,`throw new Error('please init url')`,该参数对应的是要加载的接口信息
- method:请求方法,一般是 `GET` 或者是 `POST`
- params:请求参数,该参数可以是一个对象类型,也可以是一个函数类型,这取决于要传递的参数是不是动态的。如:参数就是固定值,`{articleId: 1}`,多次请求都是一样的值,则可以写成对象类型;相反,如果每次请求的参数是动态变化的,如:分页信息,就可以写成函数类型并且一定要返回一个对象类型的数据。如下:
```js
params: () => {
return {
pageNum: this.pageNum
};
};
```
如果不需要传递任何参数,则不用写此属性
- beforeAction:函数类型,在真正发起请求之前要做的一些操作,如打开加载状态等前置操作
- onResult:函数类型,只有请求状态码返回 200 的情况下才会被调用,用于处理返回来的数据
- onError:函数类型,当请求状态码返回不是 200 的情况下会被调用,用于处理错误状态
- afterAction:函数类型,在真正发起请求之后要做的一些操作,如关闭加载状态等后置操作,但不要在这里处理请求到的数据,因为有专门处理数据的方法:onResult。这个函数无论是请求成功还是失败都会被调用
- AddItemMixin
用于添加一条数据,同样,在使用的时候需要先`import` 引入,如:
```js
import { AddItemMixin } from "@/mixins/ActionMixin";
```
> TIP
>
> 对于 **增加、删除、更新、模糊查询** 这些操作,都是需要用户点击某个按钮才能触发的操作,不像 **查询** 那样,可以一进入页面在适当的生命周期如,**created、mounted** 生命周期方法直接调用。所以,这类操作提供了两个函数,以 **AddItemMixin**为例,有:**onAddItem**,**doAddItem** 两个函数,
>
> **onAddItem** 函数可以响应按钮操作,**doAddItem** 是直接执行操作的函数
看一下 `AddItemMixin`的源码:
```js
export const AddItemMixin = {
methods: {
initAddItem({
url,
method,
params,
onAddItem,
beforeAction,
onResult,
onError,
afterAction
}) {
if (!url) {
throw new Error("please init url");
}
this.addItemModel.url = url;
this.addItemModel.method = method;
this.addItemModel.params = params;
this.addItemModel.onResult = onResult;
this.addItemModel.onError = onError;
this.addItemModel.beforeAction = beforeAction;
this.addItemModel.afterAction = afterAction;
this.addItemModel.onAddItem = onAddItem;
this.addItemModel.init = true;
},
onAddItem() {
if (!this.addItemModel.onAddItem) {
throw new Error("please init onAddItem");
}
if (!(this.addItemModel.onAddItem instanceof Function)) {
throw new Error("onAddItem must be Function");
}
this.addItemModel.onAddItem();
},
doAddItem() {
if (!this.addItemModel.init) {
throw new Error("please init addItemModel first");
}
const data = checkParams(this.addItemModel);
if (!data) {
throw new Error("please set add param");
}
addItem
.call(this, {
url: this.addItemModel.url,
method: this.addItemModel.method || "post",
data
})
.then(res => {
handleResult.call(this, this.addItemModel, res);
})
.catch(error => {
handleError.call(this, this.addItemModel, error);
});
}
}
};
```
值得说一下 `initAddItem`方法参数中的 `onAddItem`属性,可以看做是 点击 某个按钮时的响应函数。
其它的参数都与 `GetDateMixin` 中的 `initGetData`函数参数一样。
- **UpdateItemMixin**
用于更新一条记录,同样,在使用的时候需要先`import` 引入,如:
```js
import { UpdateItemMixin } from "@/mixins/ActionMixin";
```
用法与 `AddItemMixin` 一样,`initUpdateItem`方法参数也与 `AddItemMixin` 的 `initAddItem` 方法参数一样
- **DeleteItemsMixin**
用于删除一条或者多条记录,同样,在使用的时候需要先`import` 引入,如:
```js
import { UpdateItemMixin } from "@/mixins/ActionMixin";
```
与其它操作不同的是:删除操作可以针对一条记录,也可以针对多条记录,即:批量删除。因此在初始化的时候就分成了两种情况。先看下与其它不一样的初始化参数:
- url:必填,否则会抛出异常,`throw new Error('please init url')`,该参数对应的是要加载的接口信息
- method:请求方法,一般是 `GET` 或者是 `POST`
- params:请求参数,该参数可以是一个对象类型,也可以是一个函数类型,如果是函数类型则需要返回一个对象类型的数据, **这个属性用于删除单条记录**
- multiParams:请求参数,该参数可以是一个对象类型,也可以是一个函数类型,如果是函数类型则需要返回一个对象类型的数据, **这个属性用于删除多条记录**
- onDeleteItem:**当单击删除按钮时的回调函数**,接收一个参数,就是要删除的记录对象
- onDeleteMultiItem:**当单击 批量删除 按钮时的回调函数**
- beforeAction:函数类型,在真正发起请求之前要做的一些操作,如打开加载状态等前置操作
- onResult:函数类型,只有请求状态码返回 200 的情况下才会被调用,用于处理返回来的数据
- onError:函数类型,当请求状态码返回不是 200 的情况下会被调用,用于处理错误状态
- afterAction:函数类型,在真正发起请求之后要做的一些操作,如关闭加载状态等后置操作,但不要在这里处理请求到的数据,因为有专门处理数据的方法:onResult。这个函数无论是请求成功还是失败都会被调用
> TIP
>
> 本框架是一个纯属前端的项目,并没有真正对接后台接口,所有的数据和行为都是通过 MockJs 模拟或者本地模拟,所以在实际开发环境下一定要对接真正的后台接口
真实场景开发步骤如下:
1. 引入 `import { DeleteItemsMixin } from '@/mixins/ActionMixin'`
2. 配置组件的 `mixins`选项,如下:
```js
export default {
name: "UserList",
mixins: [DeleteItemsMixin]
};
```
3. 在 `mounted`生命周期函数中初始化操作,如下:
```js
export default {
name: 'UserList',
mixins: [ DeleteItemsMixin ],
mounted() {
this.initDelteItem({
url: xxxxx,
// 当删除一条记录的时候调用,以方便生成接口所需要的参数
params: () => {
// 返回的参数要结合后台所提供的接口情况而定,这里只是演示
return {
ids: this.tempItem.id
}
},
// 当批量删除多条记录的时候调用,以方便生成接口所需要的参数
multiParams: () => {
// 返回的参数要结合后台所提供的接口情况而定,这里只是演示
return {
ids: this.selectedItems.map(it=> it.id).join(',)
}
},
// 当要删除某一条记录的时候,执行此方法
onDeleteItem: (item) => {
this.tempItem = item
this.$showConfirmDialog('确定要删除此信息吗?').then((_) => {
// 调用真正执行删除的操作,'single'参数是方便区分是删除单条记录还是删除多条记录
this.doDeleteItem('sinlge')
})
},
// 当要批量删除多条记录的时候,执行此方法
onDeleteMultiItem: () => {
this.$showConfirmDialog('确定要删除这些信息吗?').then((_) => {
// 调用真正执行删除的操作,'multi'参数是方便区分是删除单条记录还是删除多条记录
this.doDeleteItem('multi')
})
},
// 操作执行成功之后的回调方法
onResult: () => { },
// 操作执行失败之后的回调方法
onError: () => { }
})
}
}
```
4. 在``视图中给`删除按钮`添加监听事件,如下:
```vue
// 删除多条记录
批量删除
{{ scope.$index + 1 }}
......
编辑
// 删除单条记录
删除
```
- **LikeSearchMixin**
这个功能是单单为 **表格类型的页面** 模糊搜索而制定的,局限性比较大,而且还得配合其它的功能才能实现。所以在项目中如果不需要,可以不引入。详细文档放到后面和其它功能一起讲解
- **PageModelMixin**
该功能主要是为了表格的分页功能,先看一下源码:
```js
export const PageModelMixin = {
data() {
return {
// 分页模型
pageModel: {
// 当前页数,从1开始
currentPage: 1,
// 每页的条数,默认是10条
pageSize: 10,
// 总页数
totalSize: 0
}
};
},
methods: {
// 当 每页的条数 改变的时候,回调的方法
pageSizeChanged(pageSize) {
this.pageModel.pageSize = pageSize;
this.pageModel.currentPage = 1;
this.publishEvent("pageChanged", this.pageModel);
},
// 当 当前页数 改变的时候,回调的方法
currentChanged(currentPage) {
this.pageModel.currentPage = currentPage;
this.publishEvent("pageChanged", this.pageModel);
},
// 用于把分页信息和其它参数一起封装成一个对象,传给后台接口
withPageInfoData(otherParams = {}) {
return {
...otherParams,
page: this.pageModel.currentPage,
pageSize: this.pageModel.pageSize
};
}
},
created() {
this.registeEvent(pageEvents);
}
};
```
###### 组件
先来看一下本框架设计的表格页面的组成部分:
- **TableHeader.vue**
表头主要放置的信息是标题,常用操作按钮,如添加,删除,可以根据自己的业务逻辑添加相关的按钮。
一个重要的功能就是:**模糊搜索**,有些表格页面是带有模糊搜索功能的,有的没有此功能。如果想要在添加**搜索**功能,则需要设置几个属性:
- **canCollapsed**:此属性默认值是 **false**,请设置成 **true**
- **searchModel**:搜索模型,(后面会具体讲到详细用法)**一定要设置成一个非空的数组**
- **defaultCollapsedState**:默认展开状态,**true** 为展开,默认是 **true**,可以不用设置,除非想设置成默认是关闭状态
说这里,就详细说一下上面一个没有细说的 Mixin-----`LikeSearchMixin`,这个模型就是专门为此功能而开发,讲解一下源码:
```js
export const LikeSearchMixin = {
data() {
return {
// 搜索模型
likeSearchModel: {
init: false,
// 搜索的表单项集合
conditionItems: []
}
};
},
methods: {
// 初始化搜索功能,重点讲一下几个参数的
initLikeSearch({
url,
method,
conditionItems,
extraParams,
beforeAction,
onResult,
onError,
afterAction
}) {
if (!url) {
throw new Error("please init url");
}
if (!onResult) {
throw new Error("please init onSearchResult function");
}
if (!(onResult instanceof Function)) {
throw new Error("onSearchResult must be Function type");
}
this.likeSearchModel.url = url;
this.likeSearchModel.method = method;
this.likeSearchModel.conditionItems = conditionItems;
this.likeSearchModel.extraParams = extraParams;
this.likeSearchModel.onResult = onResult;
this.likeSearchModel.onError = onError;
this.likeSearchModel.beforeAction = beforeAction;
this.likeSearchModel.afterAction = afterAction;
this.likeSearchModel.init = true;
},
// 执行搜索的方法
doSearch() {
if (!this.likeSearchModel.init) {
throw new Error("please init likeSearchModel first");
}
let searchParams = this.generatorSearchParams();
if (isOjbect(this.likeSearchModel.extraParams)) {
searchParams = {
...searchParams,
...this.likeSearchModel.extraParams
};
} else if (isFunction(this.likeSearchModel.extraParams)) {
searchParams = {
...searchParams,
...this.likeSearchModel.extraParams()
};
}
likeSearch
.call(this, {
url: this.likeSearchModel.url,
method: this.likeSearchModel.method || "post",
data: searchParams
})
.then(res => {
handleResult.call(this, this.likeSearchModel, res);
})
.catch(error => {
handleError.call(this, this.likeSearchModel, error);
});
},
// 重置搜索表单项
resetSearch() {
this.likeSearchModel.conditionItems &&
this.likeSearchModel.conditionItems.forEach(it => {
it.value = "";
});
},
// 判断表单项是否有值
hasSearchParams() {
return this.likeSearchModel.conditionItems.some(it => it.value !== "");
},
// 把表单项转成普通对象
generatorSearchParams() {
if (
this.likeSearchModel.conditionItems &&
this.likeSearchModel.conditionItems.length !== 0
) {
return this.likeSearchModel.conditionItems.reduce((pre, cur) => {
pre[cur.name] = cur.value;
return pre;
}, {});
}
return {};
}
}
};
```
重点看一下`initLikeSearch`方法中的几个函数:
- **conditionItems:**表单项数组,这个参数的用处就是最后在执行搜索的时候,收集表单项中的值,然后生成,提交到后台接口的参数。后面会详细讲解表单项的生成过程和一些属性
- **extraParams:**额外的参数,**可以是一个对象类型,也可以是函数类型**,**函数类型要返回一个对象类型的值**。这个参数的意义是,当表单项数组中的数据不满足后台接口需要的参数的时候,就通过这个参数的值和表单项的值组合在一起,最终生成后台接口所需要的参数。**最常见的例子就是:额外的添加分页信息。**如果表单项中的数据完全满足后台接口所需要的参数,那么这个参数可以不指定。
**表单项:**
目前框架支持的表单类型有:`input`、`select`、`date-range`、`date`、`datetime`、`time`。以后可能会添加更多的表单类型。看一个表单项的配置:
```js
conditionItems: [
{
name: 'name', // 必填,当前表单项的名称,和后台接口的参数名对应
label: '用户姓名',// 必填,当前表单项的标题
value: '', // 必填,当前表单项的值
type: 'input', // 必填,当前表单项的类型
placeholder: '请输入用户姓名', // 选填,input 类型的默认提示语
span: 8 // 选填,当前表单项在一行中所占权重,默认就是 8
},
{
name: 'sex',
label: '用户姓别',
value: '',
type: 'select', // 必填,当前表单项的类型
placeholder: '请选择用户姓别',
selectOptions: [ // 必填,当前表单项的类型是 `select`的时候,选项的数据
{
label: '男', // 选项标题
value: 0 // 选项值
},
{
label: '女',
value: 1
}
],
span: 8
}
]
**事件:**
**TableHeader**中有两个事件:**doSearch,resetSearch**,分别对应着,**搜索和重置**两个按钮的点击事件
```
- **TableBody.vue**
具体的表格内容,很简单,源码如下:
```vue
```
在页面中使用 `TableBody`的时候,因为要动态计算表格的高度,所以必须要设置两个地方的 `ref`,如下:
```vue
{{ scope.$index + 1 }}
{{ scope.row.gender === 0 ? '男' : '女' }}
```
如果没有设置两处的 `ref` 可能会导致页面显示不正常,如果页面显示不正常看看这两个地方是否设置了 `ref`属性,且名称是否正确!!!!
详情可以看具体的源码。
- **TableFooter.vue**
表尾主要的功能是:分页和刷新。分页没什么好讲的,和 `element-ui`加的 `el-pagination`用法一致。如不清楚, 可以看一下 **element-ui** 文档。
这里需要注意一个地方就是:**刷新**
因为表格页面有两个加载数据的地方:普通加载 和 模糊搜索。当点击刷新按钮的时候容易出现混乱,所以框架规定了 模糊搜索 优先,即,当表头的搜索表单项有值的时候,就优先执行 模糊搜索,如果没有就执行 普通的加载 方法。框架也提供了一个 `mixin`来应对这种场景:
```js
export const RefreshActionMixin = {
methods: {
doRefresh() {
if (this.isInited("likeSearchModel")) {
if (this.hasSearchParams()) {
// 搜索有值,优先执行模糊搜索的方法
this.doSearch();
} else {
// 执行普通的列表查询
this.getData();
}
} else if (this.isInited("getDataModel")) {
// 执行普通的列表查询
this.getData();
} else {
// 如果都没有设置就报错
throw new Error("can`t exec doRefresh function");
}
}
}
};
```
当引入 `PageModelMixin`的时候, 就自动引入了 `RefreshActionMixin`
##### 表单
###### 封装的用法
关于表单的功能,本框架提供了提供了一个`BaseForm.vue`简单组件,和 **模糊搜索** 的表单项功能差不多,也是由表单项生成相应的表单组件。看一下源码:
```vue
{}
"
>
{{ optionItem.label }}
{{ optionItem.label }}
```
同样的,该组件也提供了几种表单类型:
- input
- select
- date-range
- date
- datetime
- time
- radio-group
- check-group
另外,该组件还支持 `slot`功能,以防止在以上类型不够用的时候,可以自己定义不同的类型,如上传图片功能
看一下表单项的属性:
```js
data() {
return {
formItems: [
{
label: '会议名称:',
type: 'input',
name: 'name',
value: '',
maxLength: 50,
inputType: 'text',
placeholder: '请输入会议名称',
// 表单校验规
validator: ({ value, placeholder }) => {
if (!value) {
this.$errorMsg(placeholder)
return false
}
return true
}
},
{
label: '会议内容:',
type: 'input',
name: 'content',
value: '',
maxLength: 10,
inputType: 'text',
placeholder: '请输入会议内容',
// 表单校验规
validator: ({ value, placeholder }) => {
if (!value) {
this.$errorMsg(placeholder)
return false
}
return true
}
},
{
label: '起止时间:',
type: 'date-range',
name: 'startEndTime',
placeholder: '请选择会议起止时间',
value: '',
validator: ({ value, placeholder }) => {
if (!value) {
this.$errorMsg(placeholder)
return false
}
return true
}
},
{
label: '起止地点:',
type: 'select',
name: 'address',
value: '',
placeholder: '请选择会议地点',
selectOptions: [
{
label: '会议一室',
value: 1
},
{
label: '会议二室',
value: 2
},
{
label: '会议三室',
value: 3
},
{
label: '会议四室',
value: 4
}
],
validator: ({ value, placeholder }) => {
if (!value) {
this.$errorMsg(placeholder)
return false
}
return true
}
}
]
}
}
```
> TIP
>
> 重要提醒:如果表单数据依赖外部的数据或者说表单有默认的值,如在编辑某一个条数据的时候,需要把原始的数据回显出来。当遇到这种场景,要把表单项写在 `computed`中,这样可以保证数据刷新。如下:
>
> ```js
> computed: {
> formItems() {
> return formBuilder()
> .formItem({
> label: '用户名称',
> type: 'input',
> name: 'nickName',
> value: this.userModel.nickName,
> maxLength: 50,
> inputType: 'text',
> placeholder: '请输入用户名称',
> // 关联属性
> associatedOption: 'address',
> validator: ({ value, placeholder }, { value: assValue }) => {
> if (!value) {
> this.$errorMsg(placeholder)
> return false
> }
> if (!assValue) {
> this.$errorMsg('地址不行')
> return false
> }
> return true
> }
> })
> .formItem({
> label: '用户性别',
> type: 'radio-group',
> name: 'gender',
> style: 'button',
> value: this.userModel.gender,
> radioOptions: [
> {
> label: '男',
> value: 0
> },
> {
> label: '女',
> value: 1
> }
> ]
> })
> .formItem({
> label: '联系地址',
> type: 'input',
> name: 'address',
> value: this.userModel.address,
> maxLength: 50,
> inputType: 'textarea',
> row: 5,
> placeholder: '请输入联系地址'
> })
> .formItem({
> label: '用户状态',
> type: 'radio-group',
> name: 'status',
> value: this.userModel.status,
> radioOptions: [
> {
> label: '正常',
> value: 1
> },
> {
> label: '禁用',
> value: 0
> }
> ]
> })
> .build().formItems
> }
> }
> ```
>
> 可以通过一个`构造模式` 动态生成表单项。
>
> 对于表单构造项,需要说明几个地方:
>
> - 关联属性,在一些特定的场景下我们需要把表单之间进行关联,来处理一些业务逻辑。
>
> 需要设置:`associatedOption`属性为要关联表单项的`name`的属性值。
>
> - 校验器:`validator`校验器是提供该表单的校验功能,如,非空,长度,特殊字符等,具体的校验规则需要自己去实现,接收两个参数:当前表单项对象和关联表单项对象(如果有设置`associatedOption`属性,没有设置该属性就是 `undefined`),如果校验成功,则需要返回一个 `true`,否则返回 `false`
>
> - 针对 `select`、 `radio`、`checkbox` 类型的 `onChange`事件,在一些特定的业务场景下,如,某个表单项的值需要根据另一个表单的值变化而变化,如先选择省,再选择市。
>
> - 对于 `插槽` 功能,和构造出来的的表单完全独立,**没有任何关系**, 在构造完成表单之后,才会加载 `插槽`中的表单或者其它元素。因为 `插槽`中的表单不是通过表单项构造出来, 所以不要和构造表单项产生联系,它们之间没有关联。请一定要记住这点
具体用法,如下:
```vue
```