前言
本次是搭建大致的外观框架(基本仿照微信),并不是一成不变以后可能会有更改。有很多资源可以用现成开源产品,可以直接利用起来,为自己的项目服务。
-
element-ui 一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。vue 项目最常用的前端组件库,拥有丰富的组件可以使用。
-
iconfont 阿里妈妈 MUX 倾力打造的矢量图标管理、交流平台。 设计师将图标上传到 Iconfont 平台,用户可以自定义下载多种格式的 icon,平台也可将图标转换为字体,便于前端工程师自由调整与调用。
-
中国色 中国传统颜色的配色网站。
1. 集成 element-ui
package.json 中 dependencies 依赖中加入 element-ui
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0",
"element-ui": "^2.13.2"
}
控制台项目根目录下运行 yarn 命令安装。由于是桌面端软件,所以不考虑按需引用,直接全部引用即可。在 main.js 中加入:
// 引入element
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 引入element
Vue.use(ElementUI)
2. 集成 remote 模块
在 main.js 加入:
// 获取远程调用
const remote = require('electron').remote
Vue.prototype.$remote = remote
3. 窗口阴影
桌面端程序的窗口怎么缺少掉阴影,但是 electron 去掉自带窗口框架之后自定义窗口阴影却需要一点小技巧。
首先在 background.js 中设置窗口背景透明:
win = new BrowserWindow({
transparent: true // 窗口透明
})
之后在 App.vue 当中让 #app 绝对布局,并背景透明,子 div 也绝对布局并距离外部 div 4px 并设置阴影。
注意:如果全屏就不能出现窗口阴影了,要不然会有缝隙。这个时候就需要判断是否全屏来决定是否启用阴影。这个在之后讲到顶部工具栏按钮(最小化,最大化,关闭)的时候会再次说到这个判断全屏。
<template>
<!-- 禁止鼠标拖动图片 -->
<div id="app" ondragstart="return false;">
<div class="outer-radius" :class="this.$store.getters.getFullScreenChange ? 'no-app-shadow' : 'app-shadow'">
<router-view />
</div>
</div>
</template>
<script>
export default {}
</script>
<style>
@font-face {
font-family: Apr;
src: url('./assets/font/Alibaba-PuHuiTi-Regular.otf');
}
#app {
font-family: Apr, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: transparent;
}
.outer-radius {
border-radius: 2px;
}
.no-app-shadow {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.app-shadow {
position: absolute;
top: 4px;
bottom: 4px;
left: 4px;
right: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}
input,
select,
option,
textarea {
outline: none;
}
/* 显示一行,省略号 */
.text-elip {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* el-scrollbar全局设置100%高度 */
.el-scrollbar {
height: 100%;
}
/* el-scrollbar去除x轴滑动条 */
.el-scrollbar__wrap {
overflow-x: hidden;
}
/* 修复element-ui 下拉框问题 */
.el-select-dropdown .el-scrollbar {
padding-bottom: 17px;
}
/* 过度动画-渐隐渐显 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>
4. 侧边菜单栏
在 element-ui 的 icon 组件中选取合适图标,注册图标点击事件来传递点击图标的序号.
<template>
<div class="side-bar">
<div class="user-icon">
<img src="../assets/img/icon.svg" />
</div>
<div title="聊天" class="side-btn my-chat" :class="tabIndex === 0 ? 'tab-this' : 'tab-other'" @click="_btnTab(0)">
<i class="el-icon-chat-dot-round"></i>
</div>
<div title="通讯录" class="side-btn my-address" :class="tabIndex === 1 ? 'tab-this' : 'tab-other'" @click="_btnTab(1)">
<i class="el-icon-user"></i>
</div>
<div title="文件夹" class="side-btn my-file" :class="tabIndex === 2 ? 'tab-this' : 'tab-other'" @click="_btnTab(2)">
<i class="el-icon-document"></i>
</div>
<div title="设置" class="side-btn my-setting" :class="tabIndex === 3 ? 'tab-this' : 'tab-other'" @click="_btnTab(3)">
<i class="el-icon-setting"></i>
</div>
</div>
</template>
<script>
export default {
data() {
return {
tabIndex: 0
}
},
methods: {
_btnTab: function(index) {
if (this.tabIndex === index) {
return
}
this.tabIndex = index
// 注册点击事件
this.$emit('tabClick', index)
}
}
}
</script>
<style scoped>
.side-bar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 60px;
background-color: #1f2623;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
-webkit-app-region: drag;
-webkit-user-select: none;
}
.user-icon {
position: absolute;
top: 20px;
left: 10px;
right: 10px;
height: 40px;
-webkit-app-region: no-drag;
}
.user-icon img {
height: 100%;
width: 100%;
}
.side-btn {
height: 30px;
width: 30px;
text-align: center;
line-height: 30px;
font-size: 28px;
-webkit-app-region: no-drag;
cursor: pointer;
}
.tab-other {
color: #dcdfe6;
}
.tab-this {
color: aquamarine;
}
.tab-other:hover {
color: #f2f6fc;
}
.my-chat {
position: absolute;
top: 100px;
left: 15px;
}
.my-address {
position: absolute;
top: 160px;
left: 15px;
}
.my-file {
position: absolute;
top: 220px;
left: 15px;
}
.my-setting {
position: absolute;
bottom: 20px;
left: 15px;
}
</style>
5. 顶部功能栏
顶部只负责窗口拖动和最大化、最小化、关闭、置顶等功能。这个时候就用到了 electron 的 remote 模块。由于在 main.js 入口函数中就已经注册了 remote 模块,在 vue 中直接$remote 就可以引用。
5.1 此处问题
- electron 窗口最大化被禁用
windows 设置了 frame: true,然后再有这个配置,最大化无效,置灰。
- win.isMaximized() 始终返回 false
windows 设置了 frame: false,然后再有这个配置,win.isMaximized() 会始终返回 false。
程序中默认进入非全屏,在状态管理中自定义全屏状态。
- electron9.x 退出程序 app.quit() 无效
暂时未找到原因,只能使用 app.exit() 代替。
app.quit()和 app.exit()区别:app.quit()是如果此时所有窗口已经关闭,直接触发 quit 事件;否则 Electron 会首先触发 before-quit,然后开始关闭所有的窗口,然后触发 will-quit 事件,注意在这种情况下 window-all-closed 事件不会被触发,所以你可以放心在 window-all-closed 里使用 app.quit(),而不用担心会出现无限递归。app.exit()是直接关闭进程,强制退出。
注意:右上角 X 号关闭的应该是窗口剩下托盘而不是退出程序,托盘中才能真正的退出程序。托盘功能在之后的文章讲解,这里暂时 X 号就是退出程序。
5.2 监听全屏和退出全屏
- 在状态管理 store 中放入全屏事件状态的计算属性监听:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
isFullScreen: false
},
mutations: {
// 改变全屏状态
switchFullScreen(state, payload) {
state.isFullScreen = payload
}
},
getters: {
// 获取全屏状态计算属性
getFullScreenChange(state) {
return state.isFullScreen
}
},
actions: {},
modules: {}
})
- 在需要的地方使用$store.getters.getFullScreenChange 来表示全屏状态,比如全屏时取消窗口边框阴影:
<div class="outer-radius" :class="this.$store.getters.getFullScreenChange ? 'no-app-shadow' : 'app-shadow'">
<router-view />
</div>
5.3 顶部功能栏 topBar.vue 代码
<template>
<div class="top-bar">
<div class="left"></div>
<div class="right">
<div class="top-icon close" title="关闭" @click="btnClick('close')">
<i class="el-icon-close"></i>
</div>
<div class="top-icon max" title="最大化" @click="btnClick('max')">
<i v-if="!isFull" class="el-icon-full-screen"></i>
<i v-if="isFull" class="el-icon-copy-document"></i>
</div>
<div class="top-icon min" title="最小化" @click="btnClick('min')">
<i class="el-icon-minus"></i>
</div>
<div class="top-icon top" :class="isAlwaysOnTop ? 'always-top' : ''" title="置顶" @click="btnClick('top')">
<i class="el-icon-paperclip"></i>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isFull: false,
isAlwaysOnTop: false
}
},
methods: {
btnClick: function(key) {
let vm = this
switch (key) {
case 'close':
vm.$remote.app.exit()
break
case 'max':
if (vm.isFull) {
vm.$remote.getCurrentWindow().unmaximize()
vm.isFull = false
} else {
vm.$remote.getCurrentWindow().maximize()
vm.isFull = true
}
vm.$store.commit('switchFullScreen', vm.isFull)
break
case 'min':
vm.$remote.getCurrentWindow().minimize()
break
case 'top':
vm.$remote.getCurrentWindow().setAlwaysOnTop(!vm.isAlwaysOnTop)
vm.isAlwaysOnTop = !vm.isAlwaysOnTop
break
}
}
}
}
</script>
<style scoped>
.top-bar {
position: absolute;
top: 0;
left: 60px;
right: 0;
height: 26px;
border-top-right-radius: 4px;
-webkit-app-region: drag;
-webkit-user-select: none;
}
.top-bar .left {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 250px;
background-color: #c3d7df;
}
.top-bar .right {
position: absolute;
top: 0;
left: 250px;
height: 100%;
right: 0;
background-color: rgba(198, 223, 200);
border-top-right-radius: 2px;
}
.top-icon {
position: absolute;
-webkit-app-region: no-drag;
top: 0;
height: 26px;
width: 30px;
line-height: 26px;
font-size: 14px;
text-align: center;
cursor: pointer;
color: #909399;
}
.top-icon:hover {
background-color: #f2f6fc;
}
.close {
right: 0;
border-top-right-radius: 2px;
}
.close:hover {
background-color: #f56c6c;
color: #ffffff;
}
.max {
right: 30px;
}
.min {
right: 60px;
}
.top {
right: 90px;
}
.always-top {
color: #000000;
}
</style>
6. 总结
已经完成大致 UI 框架,剩下的就是需要补充各个二级界面。
未完待续。。。