Appearance
PC界面渲染包 mas-explorer
PC界面渲染包是专门为PC浏览器访问系统而设计的界面渲染功能,引入组件库 Element UI
、路由管理器 Vue Router
,注册全局UI组件、全局路由,定义主题机制,汇总菜单、主题等信息,启动界面渲染,并提供默认组件封装、默认主题、默认路由、默认菜单和服务接口错误提示功能。
为平台扩展的能力
- 渲染PC Web界面
- PC浏览器前端组件库(Element UI)
- 路由管理(Vue Router)与定义
- 菜单定义
- 全局UI组件注册能力
- 主题机制与主题切换能力
- 主题开发与定义能力
- Ajax调用平台级错误显示
引入的框架
Element UI 组件库
Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,当前版本为 2.15.2
,具体组件信息,详见官网:https://element.eleme.cn/#/zh-CN/component/installation
Vue Router 路由管理器
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。当前版本为 3.6.5
,详见官网:https://v3.router.vuejs.org/zh/
封装的对象
主题
解析主题配置信息,提供显示主题区域和激活主题的功能方法
js
class Theme {
constructor(ctx, app, options) {},
// 编号:<app_name>.<theme_name>
id,
// 所属应用对象
app,
// 上下文
ctx,
// 是否激活
actived,
// options 中的属性
name,
title,
author,
image,
description,
main,
login,
lockscreen,
css,
// 显示主题区域(main/login/lockscreen)
show(section) {},
// 激活主题(成为当前主题)
active() {}
}
其中,options
定义如下:
js
{
// 名称
name: 'default',
// 标题
title: '政务蓝'
// 作者,
author: '中科天翔',
// 预览图
image: "/images/themeImg1.png",
// 描述信息
description: '蓝色稳重,简洁大气,作为默认主题,通用性高',
// 主界面
main: () => import(/* webpackChunkName: "main" */ './theme/Main.vue'),
// 纯工作区
page: () => import(/* webpackChunkName: "main" */ './theme/Page.vue'),
// 登录界面
login: () => import(/* webpackChunkName: "main" */ './theme/Login.vue'),
// 锁屏页面
lockscreen: () => import(/* webpackChunkName: "main" */ './theme/LockScreen.vue'),
//引入的css文件列表
css: ["/theme/index.css"]
}
其中,image
、css
中的地址如果不以 http://
或 https://
开头的,必须相对于应用的静态文件夹(默认为:public
)目录地址,如:
/theme/index.css => public/theme/index.css
theme/index.css => public/theme/index.css
/index.css => public/index.css
说明
默认主题为 mas-explorer
应用提供的 default
,其定义如前文所述。其余主题定义过程中,如其中部分属性没有定义,则使用默认主题中相应属性替代
封装的UI 组件
页头
- Logo,
- LogoImg,
- LogoTitle,
- Header,
菜单
- Menu,
- SysMenu,
- LeftSubMenu,
- MenuItem,
- SubNavBar,
- SubNavBarLogo,
- SubNavbarIsHide,
- SidebarFirst,
- HeaderNoMenu,
主题
- Theme,
- LoginMask,
表格
- Title,
- TableNoData
- Pagination,
窗口大小拖拽
- Container,
- Splitter,
其他
- SysPanel,
- GuideMask,
- Label,
- Badge,
对微应用能力的扩展
全局 UI 组件
按Vue框架制作的全局UI组件,可以通过components属性,在微应用定义中声明,平台会自动注册为全局组建
js
{
...
components: [
// 数组定义方式:[tagName, 组件]
["uiblock", UIBlock],
// 仅组件,使用组件定义中的name属性值作为tagName
Container
]
}
开发主题
各应用可以自行开发并定义主题,主题包括:基本信息、页面和样式信息等,页面又包含登录页、锁屏页、主页面和纯工作区,分别以 login
、lockscreen
、main
和page
定义。应用对外定义主题时,在应用定义对象中,通过 themes
属性设置,具体定义如下:
js
themes: [
{
// 名称
name: 'default',
// 标题
title: '政务蓝'
// 作者,
author: '中科天翔',
// 预览图
image: "/images/themeImg1.png",
// 描述信息
description: '蓝色稳重,简洁大气,作为默认主题,通用性高',
// 主界面
main: () => import(/* webpackChunkName: "main" */ './theme/Main.vue'),
// 登录界面
login: () => import(/* webpackChunkName: "main" */ './theme/Login.vue'),
// 锁屏页面
lockscreen: () => import(/* webpackChunkName: "main" */ './theme/LockScreen.vue'),
// 纯工作区
page: () => import(/* webpackChunkName: "main" */ './theme/Page.vue'),
//引入的css文件列表
css: ["/theme/index.css"]
}
]
主界面
作为登录后的框架页面,一般包含菜单和工作区,至少包含一个 router-view
节点作为工作区,举例如下:
vue
<template>
<el-container ref="container" class="main">
<el-header style="height:64px;">
<NextHeader ref="header" />
</el-header>
<el-main ref="mainContent">
<transition name="component-fade" mode="out-in">
<router-view class="main-content" />
</transition>
</el-main>
<GuideMask/>
</el-container>
</template>
<style scoped>
.el-container {
height: calc(100vh);
font-family: 'Source Han Sans CN Regular';
}
.el-header {
background-color: #1A81FF;
color: white;
padding: 0;
}
.el-main {
height: calc(100vh - 64px);
background: #f0f2f5;
padding: 24px;
position: relative;
width: 100%;
overflow: auto;
overflow-x: hidden;
}
.main-content{
height: 100%;
width: 100%;
box-sizing: border-box;
/* background: #fff; */
border-radius: 12px
}
<style>
纯工作区
当页面进入编辑、预览模式,即 this.$app.ctx.mode
为 page
或 mobild-page
时,主页面的菜单等附加部件需要隐藏,便于页面编辑。
为确保页面编辑、预览和实际使用过程中样式一致,纯工作区模式的主界面除隐藏菜单等部件外,工作区的显示样式要保持一致
登录页
用于系统登录,需要包含用户登录区,实现用户登录逻辑,举例如下:
查看代码
vue
<template>
<div>
<img class="login-image" :src="loginImage" :onerror="defaultImage"/>
<div class="login-card">
<div class="left"></div>
<div class="right">
<span @click="handleQrCodeClick" v-show="showQrCode" class="iconfont1 qrCode"></span>
<span @click="handleQrCodeClick" v-show="!showQrCode" class="iconfont1"></span>
<div slot="header" class="header">
<NextLogo />
</div>
<div class="form-div" v-if="!showQrCode">
<el-form ref="form" :model="form" :rules="rules" labelPosition="top">
<el-form-item label="登录名" prop="username">
<el-input
placeholder="请输入登录名"
v-model="form.username"
@keyup.native.enter.stop.prevent="$refs.password.select"
ref="username"
></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
placeholder="请输入密码"
v-model="form.password"
@keyup.native.enter.stop.prevent="onSubmit"
:type="secretFlag == false ? 'password' : 'text'"
ref="password"
>
<i slot="suffix" :class="secretFlag == false ? 'iconfont icon-in_biyan' : 'el-icon-view'" @click="changeIconState('secretFlag')"></i>
</el-input>
</el-form-item>
<el-alert
:title="error"
v-if="error && !showQrCode"
:closable="false"
type="error"
show-icon
></el-alert>
<el-form-item>
<div class="group-btn-block">
<el-button type="info" plain @click="resetForm" >重置</el-button>
<el-button type="primary" @click="onSubmit">登录</el-button>
</div>
</el-form-item>
</el-form>
</div>
<div class="hide-mask" v-if="showQrCode"><div class='div-qrcode-desc'>浙政钉扫码登录</div></div>
<iframe v-if="showQrCode" :src="iframeSrc" scrolling="auto" frameborder="0" id="iframe" width="100%" height="100%"></iframe>
<div :style="{display: showQrCode && error && error!='' ? '' : 'none'}" class="qrMsg">{{error && error !='' ? error : '登录失败'}}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
secretFlag:false,
showQrCode: false,
iframeSrc: this.$store.getters.setting.qrCodeUrl ? this.$store.getters.setting.qrCodeUrl: 'https://login.dg-work.cn/oauth2/auth.htm?response_type=code&client_id=scanTestSn_dingoa&scope=get_user_info&authType=QRCODE&embedMode=true&redirect_uri=http://localhost:8081/#/mas-panel/index',
form: {
username: this.$store.getters.user.username,
password: "",
},
rules: {
username: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入用户名"));
} else {
callback();
}
},
trigger: "blur",
},
{
pattern: /^[a-zA-Z]+[0-9a-zA-Z-_]*$/,
message: "登录名必须字母开头,仅包含字母、数字和下划线",
},
],
password: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入密码"));
} else {
callback();
}
},
trigger: "blur",
},
],
},
};
},
mounted() {
this.$refs.username.select();
let that = this
window.addEventListener('message', function(event) {
if (event.data && event.data.code) {
that.$store.commit("error");
that.$store.dispatch("qrLogin", {qrCode: event.data.code}).then(() => {
that.$router.push(that.$store.getters.home);
});
}
});
},
computed: {
error() {
return this.$store.getters.error;
},
loginImage() {
return this.$store.getters.setting.loginImage;
},
defaultImage() {
return 'this.src="' + require('../assets/login.png') + '"';
}
},
methods: {
changeIconState(){
this.secretFlag = !this.secretFlag
},
onSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.$store.commit("error");
this.$store.dispatch("login", this.form).then(() => {
this.$router.push(this.$store.getters.home);
});
} else {
return false;
}
});
},
resetForm() {
this.$store.commit("error");
this.$refs.form.resetFields();
},
handleQrCodeClick() {
this.showQrCode = !this.showQrCode
}
},
};
</script>
锁屏页
用于锁屏后界面的显示,并且可以解锁,需要实现解锁逻辑,举例如下:
查看代码
vue
<template>
<div>
<img class="login-image" :src="loginImage" :onerror="defaultImage"/>
<div class="login-card">
<div class="left"></div>
<div class="right">
<div slot="header" class="header">
<NextLogo />
</div>
<div class="form-div">
<el-alert
:title="`已被${$store.getters.user.name}(${$store.getters.user.username})锁定,请解锁`"
:closable="false"
type="info"
show-icon
></el-alert>
<el-form ref="form" :model="form" :rules="rules" labelPosition="top" @submit.native.prevent>
<el-form-item label="密码:" prop="password">
<el-input
placeholder="请输入密码"
v-model="form.password"
@keyup.native.enter.stop.prevent="onSubmit"
:type="secretFlag == false ? 'password' : 'text'"
ref="password"
>
<i slot="suffix" :class="secretFlag == false ? 'iconfont icon-in_biyan' : 'el-icon-view'" @click="changeIconState('secretFlag')"></i>
</el-input>
</el-form-item>
<el-alert
:title="error"
v-if="error"
:closable="false"
type="error"
show-icon
></el-alert>
<el-form-item>
<div class="group-btn-block">
<el-button type="info" plain @click="resetForm">重置</el-button>
<el-button type="primary" @click="onSubmit">解锁</el-button>
</div>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LockScreen",
data() {
return {
secretFlag:false,
form: {
username: this.$store.getters.user.username,
password: "",
},
rules: {
password: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入密码"));
} else {
callback();
}
},
trigger: "blur",
},
],
},
};
},
mounted() {
this.$nextTick(() => {
this.$refs["password"].select();
});
},
computed: {
error() {
return this.$store.getters.error;
},
loginImage() {
return this.$store.getters.setting.loginImage;
},
defaultImage() {
return 'this.src="' + require('../assets/login.png') + '"';
}
},
methods: {
changeIconState(){
this.secretFlag = !this.secretFlag
},
onSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.$store.dispatch("unlockscreen", this.form);
} else {
return false;
}
});
},
resetForm() {
this.$store.commit("error");
this.$refs.form.resetFields();
},
},
};
</script>
样式表
前端主要以Element UI组件库为主,因此样式表文件中必须包含其样式文件
默认主题
此包提供默认主题,确保每个站点都有主题可以正常显示,效果如下:
定义路由
全局路由定义主页面、默认工作区页面、切换主题和登录页4个,并提供 root
和 main
插槽,各应用可以定义全屏页面路由到 root
插槽下,实现全屏页面(无主题菜单区域);也可以定义工作区页面路由到 main
插槽下,实现菜单切换工作区页面的功能。
此包对 Vue Router
进行扩展,增加了应用相对路径和插槽机制,所有相对路径路由均以 /<app_name>
为前缀改为绝对路径,其中 <app_name>
为当前应用的 name
js
{
routes: [{
path: 'users',
slot: "main", // 默认,可不写
component: () => import(/* webpackChunkName: "main" */ './pages/Users.vue'),
children: [{
path: 'report',
component: () => import(/* webpackChunkName: "main" */ './pages/UsersReport.vue'),
}]
}]
}
路径规则
path
定义路径匹配规则,支持绝对路径、相对路径和动态路由等
绝对路径
以
/
开头的固定路径,如/user-profile
,匹配路径/user-profile
相对路径
不以
/
开头的固定路径,如users/info
,匹配路径/<app_name>/users/info
动态路由
动态路由是在绝对路径或相对路径之上,把路径的一部分区域改成变量,如
users/:id
,匹配路径/<app_name>/users/123
,此时页面中可通过this.$route.params.id
获取到路径中的123
;也可以用users-*
,匹配以/<app_name>/users-
开头的任意路径,此时页面中可通过this.$route.params.pathMatch
获取到路径中的users-
之后的所有字符串。动态匹配规则可参见
Vue Router
官网说明:https://v3.router.vuejs.org/zh/guide/essentials/dynamic-matching.html
路由区域
slot
定义路由组件显示区域,有效值为 root
和 main
,main
为默认值
root 全屏页面
表示路由组件显示成全屏页面,也就是主题不现实菜单等区域。
main 工作区页面
表示路由组件显示在主题的工作区,也就是主题显示菜单区域等,工作区显示路由组件。
路由组件
component
定义路由组件,一般以() => import(/* webpackChunkName: "main" */ './pages/Users.vue')
定义异步加载组件,也支持同步组件。
子路由
children
定义子路由,实现路由层级嵌套。
应用内路由跳转
应用内定义的路由,在注册到平台路由时,会按规则增加应用 name
属性作为前缀,所以使用默认的路由跳转,path
参数上需要添加 name
属性,不方便,也不利于后期 name
属性的变更,因此在应用原型对象上扩展 router
属性,提供 push
方法,用于应用内路由跳转,可通过 this.$app.router.push(url)
调用。
菜单
默认定义了主菜单和系统菜单两个,并提供 main
插槽作为主菜单区,system
插槽作为系统菜单区,profile
插槽作为系统菜单/用户菜单区,console
插槽作为系统菜单/用户菜单/系统管理区
菜单定义格式如下:
json
{
navs: [{
title: "业务菜单组A",
slot: "main", // 放到主菜单区
children: [{
title: '业务A1',
auth: 'r.user',
path: 'users',
}, {
title: '业务A2',
icon: 'el-icon-lock',
path: 'roles',
}]
},{
title: '系统管理',
icon: 'el-icon-setting',
auth: 'r.admin || r.platform',
slot: "console", // 放到系统管理区
path: '/system',
children: [{
title: '系统管理1',
icon: 'el-icon-lock',
path: 'roles',
},{
title: '系统管理2',
icon: 'el-icon-news',
handler: function (vm) {
...
// 弹出窗口等,不切换页面
}
}]
}]
}
菜单项名称
title
定义菜单项名称
菜单项图标
icon
定义菜单项图标,值必须是已加载的图标库的图标样式
路径
path
定义菜单路径,支持相对路径和绝对路径,如同路由定义中的 path
,相对路径也会自动增加 /<app_name>
前缀
非页面切换菜单项
系统菜单中,还有些如“退出系统”等特殊操作,不是切换工作区页面,而取消执行脚本的菜单项。此时可以使用 handler
定义一个方法执行特殊逻辑,而不定义 path
属性。
菜单访问权限
auth
定义菜单访问权限,值为权限表达式。权限表达式可参考:权限验证
跨应用打开窗口
应用定义 action
提供给其他应用调用,而此 action
逻辑中如果涉及到弹窗的话,使用 this.$dialog
方法,会使弹窗页面内的逻辑调用 this.$app
时,出现混乱。因此在应用上定义 $dialog
方法,避免此情况的产生。
对上下文对象的扩展
业务菜单数据
在上下文对象中定义 navs
数组,其值是通过对所有微应用定义的菜单汇总,并通过权限校验剔除没有权限的菜单项后获得。数据格式如下:
json
[
{
"title": "首页",
"icon": "el-icon-s-home",
"auth": "r.default",
"path": "/mas-panel/index"
},
{
"path": "/lc-system/project-manager",
"isDyn": "1",
"auth": "",
"icon": "el-icon-suitcase",
"title": "项目管理"
},
{
"path": "/lc-system/project-workspace",
"isDyn": "1",
"auth": "",
"icon": "el-icon-suitcase",
"title": "项目空间"
},
...
]
系统菜单数据
在上下文对象中定义 sysNavs
数组,其值是通过对所有微应用定义的菜单汇总,并通过权限校验剔除没有权限的菜单项后获得。数据格式如下:
json
[
{
"title": "登录",
"auth": "!r.default"
},
{
"title": "锁定屏幕",
"icon": "el-icon-lock"
},
{
"type": "user",
"path": "/user",
"auth": "r.default",
"children": [
{
"title": "切换主题",
"icon": "el-icon-star-off",
"auth": "r.default",
"path": "/mas-explorer/themes"
},
...
{
"title": "退出系统",
"icon": "el-icon-news"
}
]
}
]
主题列表
通过合并所有微应用定义的主题并实例化成 Theme
对象后,以 <app_name>.<theme_name>
为属性名,保存在上下文的 themes
对象上。
js
{
"mas-explorer.default": Theme{...}
}
路由对象
上下文中以 router
保存了对全局路由对象的引用
对Vue原型的扩展
弹出窗口
主页面打开弹出窗口显示其他页面,用法如下:
js
$dialog({
width: "60%", // 窗口宽度
top: "47px", // 窗口顶部位置
title: "Json编辑器", // 窗口标题
appendToBody: true, // 是否添加在body上,不加在body上可能会被其他元素遮挡
closeOnClickModal: false, // 是否可以在弹窗外单击鼠标关闭弹窗
... // 此处可以定义el-dialog的其他属性,参见Element UI官网
noBindOptions: true // 必填参数,解决兼容性问题
}, "UIBlock", {
code: "page-code", // 需要在弹窗中显示的页面代码
... // 此处可以定义code指定的页面所需要的参数
}).then(data=>{
... // 此处代码为弹出窗口被关闭时执行,其中data为弹出窗口放回的数据
})
弹出窗口中的页面需要主动关闭弹出窗口时,用法如下:
js
$emit("close", data) // 其中data为需要返回给主页面的数据
$emit("cancel") // 关闭窗口,不返回任何数据
弹出抽屉面板
主页面弹出抽屉面板显示其他页面,用法如下:
js
$drawer({
size: "60%", // 抽屉大小
title: "Json编辑器", // 窗口标题
appendToBody: true, // 是否添加在body上,不加在body上可能会被其他元素遮挡
... // 此处可以定义el-dialog的其他属性,参见Element UI官网
}, "UIBlock", {
code: "page-code", // 需要在弹窗中显示的页面代码
... // 此处可以定义code指定的页面所需要的参数
}).then(data=>{
... // 此处代码为弹出窗口被关闭时执行,其中data为弹出窗口放回的数据
})
弹出窗口中的页面需要主动关闭弹出窗口,用法如下:
js
$emit("close", data) // 其中data为需要返回给主页面的数据
$emit("cancel") // 关闭抽屉,不返回任何数据