OA Iview 前端开发指导

介绍

OA版本的前端是基于 Iview 开发,使用 Vue Cli 3 构建,支持 Chrome、Safari、Firefox 等现代主流浏览器,支持 IE10、IE11、Edge 浏览器。

您可以访问 Iview官网文档 去查阅基础组件API,您还可以查阅 AgileBPM基础组件API

快速开始

安装

请参考README 文件

初次设置

在开始开发前,有一些内容需要提前一次性设置,分别是:

  1. .env 文件中的 VUE_APP_TITLE 字段,设置为网站的标题,它仅用于在 public/index.html 模板中,替换对应的标题。它决定了最终入口 html 文件的 <title></title> 的内容。每页的标题是在路由配置中设置,在新增页面文档中查看。
  1. 网站 logo 图片,logo 总共有 3 个:
  • src/assets/images/logo.png:完整 Logo(深色字),用于背景为浅色时(如登录页、亮色侧边栏);
  • src/assets/images/logo-dark.png:完整 Logo(白色字),用于背景为深色时(如暗色侧边栏);
  • src/assets/images/logo-small.png:正方形 Logo,仅 icon,无文字,用于侧边栏折叠时。

目录结构

├── public                # 静态资源
│ ├── favicon.ico # favicon图标
│ └── index.html # html 模板
├── src # 源代码
│ ├── api # 所有请求(目前AgileBPM相关请求地址均配置在各个页面,该配置可忽略、也可以启用)
│ ├── assets # 图片、svg 等静态资源
│ ├── components # 公共组件,包含表单控件的组件,基础组件,列表页组件
│ ├── i18n # 多语言
│ ├── layouts # 布局
│ ├── libs # 公共方法
│ ├── menu # 菜单配置
│ ├── mixins # 通用混合
│ ├── mock # 数据模拟
│ ├── pages # 【所有页面】不同业务需要创建各自目录
│ ├── plugins # 插件
│ ├── router # 路由配置
│ ├── store # Vuex 状态管理
│ ├── styles # 样式管理
│ ├── setting.env.js # 开发配置文件
│ ├── setting.js # 业务配置文件
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── App.vue # 入口页面
├── tests # 测试管理
├── alias.config.js # 别名,仅用于配置 WebStorm 识别别名,无实际用处
├── babel.config.js # babel 配置
├── jest.config.js # jest 配置
├── package.json # package.json
└── vue.config.js # Vue CLI 3 配置

setting.js业务配置

请参考 setting.js 文中描述

新增页面

增加一个新的页面,需要 3 步:

  • 创建新的 .vue 页面文件
  • 在路由中添加新页面
  • 在菜单中配置该页面

以下是具体使用方法:

创建新的 .vue 页面文件

所有的页面文件在 src/pages 目录下管理,处于可维护性考虑,建议对页面进行目录拆分,而不是直接将 .vue 文件放置在 src/pages

在路由中添加新页面

一个页面主要分为两类:

  1. 在主框架内的页面,即包含了顶部、侧边栏、多页签等基础布局(大部分页面应当属于此类型)
  2. 在主框架外的页面,常见的有首页、登录 / 注册 / 注册结果页,它们较独立,没有其他布局

打开路由配置文件 src/router/routes.js

如果是主框架外的页面,将路由配置添加在 frameOut 中,它不含有基础布局。

如果是主框架内的页面,则将路由配置添加在 frameIn 中。

由于框架内的页面通常较多,处于可维护性考虑,建议分模块,同样以 dashboard 为例,先创建子路由文件:src/router/modules/dashboard.js ,示例内容如下:

import BasicLayout from '@/layouts/basic-layout';

const meta = {
auth: true
};

const pre = 'dashboard-';

export default {
path: '/dashboard',
name: 'dashboard',
redirect: {
name: `${pre}console`
},
meta,
component: BasicLayout,
children: [
{
path: 'console',
name: `${pre}console`,
meta: {
...meta,
title: '主控台'
},
component: () => import('@/pages/dashboard/console')
},
{
path: 'monitor',
name: `${pre}monitor`,
meta: {
...meta,
title: '监控页'
},
component: () => import('@/pages/dashboard/monitor')
},
{
path: 'workplace',
name: `${pre}workplace`,
meta: {
...meta,
title: '工作台'
},
component: () => import('@/pages/dashboard/workplace')
}
]
};

其中第一项name: 'dashboard' 并不作为页面直接访问,而是一个中间的二级页面,真正访问的是它的子页面,也就是 children 下的路由。所以在该中间页面中,有一个component: BasicLayout 的设置,它决定了子页面使用哪个布局(默认带的是典型布局,即 BasicLayout),redirect 选项为强制输入该中间路由时,重定向的页面,通常为 children下的第一个页面。

创建好子路由后,在把它设置在顶级路由中即可:

src/router/routes.js

import dashboard from './modules/dashboard';
const frameIn = [
{
path: '/',
redirect: {
name: 'dashboard-console'
},
component: BasicLayout,
children: [
{
path: 'index',
name: 'index',
redirect: {
name: 'dashboard-console'
}
},
{
path: 'log',
name: 'log',
meta: {
title: '前端日志',
auth: true
},
component: () => import('@/pages/system/log')
},
// 刷新页面 必须保留
{
path: 'refresh',
name: 'refresh',
hidden: true,
component: {
beforeRouteEnter (to, from, next) {
next(instance => instance.$router.replace(from.fullPath));
},
render: h => h()
}
},
// 页面重定向 必须保留
{
path: 'redirect/:route*',
name: 'redirect',
hidden: true,
component: {
beforeRouteEnter (to, from, next) {
next(instance => instance.$router.replace(JSON.parse(from.params.route)));
},
render: h => h()
}
}
]
},
dashboard
];

路由的各选项说明

路由中的配置都是 vue-router 的标准配置,除了 meta 选项。

meta 中主要有几个可选项:

  • title:当前页面的标题,最终更改文件在 src/libs/util.js 中完成,而调用 util.js 中 修改标题 方法是在 src/router/index.jsrouter.afterEach 中。util.title 方法还支持另一个选项 count,设置后,标题最终会渲染形如:(3条消息)页面标题 - 标题后缀
    可以根据需求在 util.js 中适当修改文案描述和逻辑。
  • auth:路由级别的鉴权依据,详见文档鉴权篇-路由鉴权。

  • cache:当前页面是否缓存,如果设置为 true,该页面会在 <keep-alive> 中被缓存

  • closable 2.1.0:设置为 false,则该页签不可关闭,仅对首页有效。

路由常见问题

  • 设置 cache 后缓存无效
    设置缓存,需要设置 name,包括路由的 name 和 .vue 页面的 name。

在菜单中配置该页面

在 设计开发平台 -系统配置-资源配置中 新增菜单资源

  1. 资源树切换平台至 “门户平台-OA”,在资源树中 右键 点击“添加资源”
  2. 输入资源信息
    • 资源别名:唯一的CODE
    • 资源名:菜单的名字(国际化版本需要在 内容管理-翻译管理 中定义国际化 别名为menu.资源别名)
    • 请求地址:为路由地址
    • 默认打开: 菜单默认会以展开形式展现
    • 类型: 选择菜单

新增样式

样式是在目录 src/styles 下进行管理的,且使用 less 进行预处理。

入口的 less 文件在src/styles/index.less 内,这里可以导入需要的样式文件。初始时会导入所需的默认配置:

default/index.less:主题修改在这里。
font/iconfont.css:系统所需的第三方 icon
setting.less:样式的全局配置,已在业务配置章节介绍
common.less:通用样式,见下节
layout/index.less:引入布局样式,默认引入的是自带的典型布局样式
pages/account.less:具体的页面样式,如有很多,可以拆分 modules

对于额外的页面样式,有两种方法集成,一种是统一放置在src/styles 里,通过配置 index.less 引入,另一种是直接写在具体的 .vue 页面文件的 <style></style> 里。

由于 less 的特性,建议您这样命名:

  • 对于页面,以 .page- 开头,比如 .page-dashboard-monitor-xxxxxx
  • 对于通用组件,以 .i- 开头,比如 .i-copyright

如何使用第三方 iconfont

如果 iView 自带的 700+ 图标无法满足业务需求,或需要自定义图标,建议使用 iconfont 管理您的图标

引入外部模块

vue-grid-layout 为例

安装依赖

在终端安装 vue-grid-layout:

$ npm install vue-grid-layout --save

使用

全局注册
src/main.js 中注册并使用组件:

// src/main.js
// 导入组件
import { GridLayout, GridItem } from 'vue-grid-layout';

// 使用组件
Vue.component('i-grid-layout', GridLayout);
Vue.component('i-grid-item', GridItem);

全局注册后,任何地方都可以使用 <i-grid-layout><i-grid-item> 两个组件了。

局部注册
只在需要的页面或组件中注册:

<template>
<i-grid-layout>
<i-grid-item />
</i-grid-layout>
</template>
<script>
import { GridLayout, GridItem } from 'vue-grid-layout';
export default {
components: { GridLayout, GridItem },
data () {
return {

}
}
}
</script>

自己封装组件

如果是自己封装的组件,可以放置在src/components 目录内,使用方法不变。

构建和发布

构建

在终端执行命令进行项目打包

$ npm run build

构建打包成功之后,默认会在根目录生成 dist 文件夹,里面就是构建打包好的文件,通常是 **.js*.cssindex.html 等静态文件。

发布

前端是典型的 SPA 类型的工程,打包后的文件分为 index.html 和其它静态资源,整个项目只有 index.html 这一个入口文件,其它都是 Webpack 来管理的了。

所以,发布一个 SPA 项目,核心就是渲染这个 index.html 以及静态资源的位置。

一般来说,你可能使用 Nginx、Apache 等渲染这个入口文件 index.html,也可以使用 CDN 的服务,比如七牛。
更多资料可以查阅 vue官方文档

发布失败常见问题

  1. 服务端是否支持 history 路由模式。框架默认为 history 模式,需服务端处理 404 情况,否则可以改为 hash 模式。

  2. 静态资源地址 publicPath 是否修改正确,可以修改为绝对地址,避免出错。

使用 Mock 数据模拟

使用方法

OA-UI 是 基于 Mock.js 做的数据模拟

对于 Mock.js 的基本用法,请直接阅读 Mock.js 的文档。

下面介绍如何对一个接口返回内容做模拟。

我们已经使用 Mock.js 做了一些上下文处理,使用者只需要关心自己的接口和返回内容即可。

以登录接口为例:

创建文件 src/mock/api/account.js:

const userDB = [
{
username: 'admin',
password: 'admin',
uuid: 'admin-uuid',
info: {
name: 'Aresn',
avatar: 'https://dev-file.iviewui.com/userinfoPDvn9gKWYihR24SpgC319vXY8qniCqj4/avatar',
access: ['admin']
}
}
];

export default [
{
path: '/api/login',
method: 'post',
handle ({ body }) {
const user = userDB.find(e => e.username === body.username && e.password === body.password);
if (user) {
return {
code: 0,
msg: '登录成功',
data: {
...user,
token: 'A68NUPaXVBJYRStwvd9frcUn8rlf30h6'
}
}
} else {
return {
code: 401,
msg: '用户名或密码错误',
data: {}
}
}
}
}
]

示例中默认导出的数组就是一系列的模拟请求,其中每项配置如下:

  • path:请求地址,一般要与真实地址区分,比如真实地址是/login,所以模拟请求地址加了前缀 /api/login
  • method:请求方法。
  • handle:处理逻辑,这里返回模拟的数据。
    定义好了模拟返回数据,使用起来与调用真实接口类似,只用在请求数据时,将请求的真实接口 url 改为模拟数据的 url 即可。

比如登录接口:

// src/api/account.js

import request from '@/plugins/request';

export function AccountLogin (data) {
return request({
url: '/api/login',
method: 'post',
data
});
}

这里的 url 写的就是模拟的 path /api/login,当测试完成,服务上线后,只用改成真实的 /login 即可。

关闭数据模拟

如果你不需要数据模拟,直接在配置文件 src/setting.env.js 中将 isMock 设置为 false 即可。

或者你可以彻底移除跟 mock 相关的功能,步骤如下:

  1. npm remove mockjs --save

  2. 删除 src/mock

  3. 删除 vue.config.js 中相关代码:

// 判断是否需要加入模拟数据
const entry = config.entry('app');
if (Setting.isMock) {
entry
.add('@/mock')
.end()
}

Vuex

Account

account 模块负责实现用户的注册、登录、注销等功能。
文件位置在 src/store/modules/admin/modules/account.js。

User

user 模块负责实现设置和读取用户的登录信息。

文件位置在 src/store/modules/admin/modules/user.js

可以获取当前用户信息如:

//引入
import { mapState, mapActions } from 'vuex';

computed: {
...mapState('admin/user', [
'info'
]),
...mapState('admin/layout', [
'isMobile',
'logoutConfirm'
])
},

{{info.name}}

设置登录用户信息

this.$store.dispatch("admin/user/set", user);

DB 前端持久化存储

概念介绍

db 模块负责实现持久化存储。

文件位置在 src/store/modules/admin/modules/db.js

持久化存储,通常是指前端将一些数据长期存储在浏览器中,不随着刷新页面、重启浏览器等行为而消失。常见的办法有 Cookies 存储和 LocalStorage 存储
db模块 数据持久化存储方案依赖浏览器的 LocalStorage,基于 lowdb 进行进一步封装,使用体验接近 Vue.js 的模式,并更加简单、高可维护、可扩展。

db模块持久化存储,更像是一个小型数据库,具有以下特征,而这些是直接使用 LocalStorage 所不具备的:

  • 可根据不同登录用户划分存储区域,不同的登录(或未登录)用户,所存储的空间是不同的,而不是共享一个存储空间(可开关)。
  • 可根据路由划分存储区域,同一个 key,可配置在不同的路由下进行存储,不同路由获取的 value 可不同(可开关,可同时再区分用户)。
  • 可快速保存/读取当前 Vue 实例的所有 data(可区分用户)。

使用方法

  • 一:保存任意数据 - 区分用户 - 区分页面

写入数据:

const db = await this.$store.dispatch('admin/db/databasePage', {
user: true
});
db.set('keyName', 'value').write();

读取数据:

const db = await this.$store.dispatch('admin/db/databasePage', {
user: true
});
const value = db.get('keyName').value();
  • 二:保存任意数据 - 区分用户 - 不区分页面

写入数据:

const db = await this.$store.dispatch('admin/db/database', {
user: true
});
db.set('keyName', 'value').write();

读取数据:

const db = await this.$store.dispatch('admin/db/database', {
user: true
});
const value = db.get('keyName').value();
  • 三:保存任意数据 - 不区分用户 - 区分页面

写入数据:

const db = await this.$store.dispatch('admin/db/databasePage');
db.set('keyName', 'value').write();

读取数据:

const db = await this.$store.dispatch('admin/db/databasePage');
const value = db.get('keyName').value();
  • 四:保存任意数据 - 不区分用户 - 不区分页面

写入数据:

const db = await this.$store.dispatch('admin/db/database');
db.set('keyName', 'value').write();

读取数据:

const db = await this.$store.dispatch('admin/db/database');
const value = db.get('keyName').value();
  • 五:保存整页数据 $data - 区分用户

写入数据:

this.$store.dispatch('admin/db/pageSet', {
instance: this,
user: true
});

读取数据:

// 获取数据
const data = await this.$store.dispatch('admin/db/pageGet', {
instance: this,
user: true
});
// 将数据还原到页面
for (const key in data) {
if (data.hasOwnProperty(key)) this[key] = data[key];
}
  • 六:保存整页数据 $data - 不区分用户

写入数据:

this.$store.dispatch('admin/db/pageSet', {
instance: this
});

读取数据:

// 获取数据
const data = await this.$store.dispatch('admin/db/pageGet', {
instance: this
});
// 将数据还原到页面
for (const key in data) {
if (data.hasOwnProperty(key)) this[key] = data[key];
}
  • 七:Root 级别存储 - 区分用户

    提示:该方法应该是开发系统模块所使用,业务中不应该直接使用。
    写入数据:

this.$store.dispatch('admin/db/set', {
dbName: 'database',
path: 'config.version',
value: '1.0.0',
user: true
});

读取数据:

const value = await this.$store.dispatch('admin/db/get', {
dbName: 'database',
path: 'config.version',
defaultValue: '1.0.0',
user: true
});
  • 八:Root 级别存储 - 不区分用户

写入数据:

this.$store.dispatch('admin/db/set', {
dbName: 'database',
path: 'config.version',
value: '1.0.0'
});

读取数据:

const value = await this.$store.dispatch('admin/db/get', {
dbName: 'database',
path: 'config.version',
defaultValue: '1.0.0'
});

清空存储实例

  • 清空持久化数据对象

不区分用户清空:

this.$store.dispatch('admin/db/databaseClear');

区分用户清空:

this.$store.dispatch('admin/db/databaseClear', {
user: true
});

如果您想进行后续操作,可以接受返回值,返回值为可以直接操作的 db 对象:

const db = await this.$store.dispatch('admin/db/databaseClear');
db.set('keyName', 'value').write();

清空持久化数据对象 [ 区分页面 ]

不区分用户清空:

this.$store.dispatch('admin/db/databasePageClear');

区分用户清空:

this.$store.dispatch('admin/db/databasePageClear', {
user: true
});

如果您想进行后续操作,可以接受返回值,返回值为可以直接操作的 db 对象:

const db = await this.$store.dispatch('admin/db/databasePageClear');
db.set('keyName', 'value').write();

清空页面快照

不区分用户清空:

this.$store.dispatch('admin/db/pageClear');

区分用户清空:

this.$store.dispatch('admin/db/pageClear', {
user: true
});

如果您想进行后续操作,可以接受返回值,返回值为可以直接操作的 db 对象:

const db = await this.$store.dispatch('admin/db/pageClear');
db.set('keyName', 'value').write();