Vue3项目实战(六):通用功能开发(二)
screenfull
对于 screenfull
和之前一样 ,我们还是先分析它的原理,然后在制定对应的方案实现
原理:
对于 screenfull
而言,浏览器本身已经提供了对用的 API
,点击这里即可查看,这个 API
中,主要提供了两个方法:
Document.exitFullscreen()
:该方法用于请求从全屏模式切换到窗口模式Element.requestFullscreen()
:该方法用于请求浏览器(user agent)将特定元素(甚至延伸到它的后代元素)置为全屏模式- 比如我们可以通过
document.getElementById('app').requestFullscreen()
在获取id=app
的DOM
之后,把该区域置为全屏
- 比如我们可以通过
但是该方法存在一定的小问题,兼容性也不是那么好。
所以通常情况下我们不会直接使用该 API
来去实现全屏效果,而是会使用它的包装库 screenfull
整体的方案实现分为两步:
- 封装
screenfull
组件- 展示切换按钮
- 基于 screenfull 实现切换功能
- 在
navbar
中引入该组件
明确好了方案之后,接下来我们就落地该方案
封装 screenfull
组件:
- 下来依赖包 screenfull
1 |
|
- 创建
components/Screenfull/index
1 |
|
在 navbar
中引入该组件:
1 |
|
headerSeach
所谓
headerSearch
一般是指 页面搜索
原理:
headerSearch
是复杂后台系统中非常常见的一个功能,它可以:在指定搜索框中对当前应用中所有页面进行检索,以 select
的形式展示出被检索的页面,以达到快速进入的目的
那么明确好了 headerSearch
的作用之后,接下来我们来看一下对应的实现原理
根据前面的目的我们可以发现,整个 headerSearch
其实可以分为三个核心的功能点:
- 根据指定内容对所有页面进行检索
- 以
select
形式展示检索出的页面 - 通过检索页面可快速进入对应页面
那么围绕着这三个核心的功能点,我们想要分析它的原理就非常简单了:根据指定内容检索所有页面,把检索出的页面以 select
展示,点击对应 option
可进入到指定页面
方案:
对照着三个核心功能点和原理,想要指定对应的实现方案是非常简单的一件事情了
- 创建
headerSearch
组件,用作样式展示和用户输入内容获取 - 获取所有的页面数据,用作被检索的数据源
- 根据用户输入内容在数据源中进行模糊搜索(https://fusejs.io/)
- 把搜索到的内容以
select
进行展示 - 监听
select
的change
事件,完成对应跳转
headerSearch 组件
创建 components/headerSearch/index
组件,当点击搜索图标时,通过 transition
动画,将其长度展示出来,并且自动聚焦 focus()
:
1 |
|
在 navbar
中导入该组件
1 |
|
获取数据源
在有了 headerSearch
之后,接下来就可以来处理对应的 检索数据源了
检索数据源 表示:有哪些页面希望检索
那么对于我们当前的业务而言,我们希望被检索的页面其实就是左侧菜单中的页面,那么我们检索数据源即为:左侧菜单对应的数据源
根据以上原理,我们可以得出以下代码:
1 |
|
模糊搜索Fuse.js
Fuse.js is a powerful, lightweight fuzzy-search library, with zero dependencies.
如果我们想要进行 模糊搜索 的话,那么需要依赖一个第三方的库 fuse.js
它是0️⃣依赖的,专门处理模糊搜索的库。
Why should I use it?
- 使用 Fuse.js,您不需要仅仅为了处理搜索而设置专用的后端。
- 简单性和性能是开发这个库的主要标准。
- 安装 fuse.js
1 |
|
- 初始化
Fuse
,更多初始化配置项 可点击这里
1 |
|
- 参考 Fuse Demo 与 最终效果,可以得出,我们最终期望得到如下的检索数据源结构
1 |
|
- 所以我们之前处理了的数据源并不符合我们的需要,所以我们需要对数据源进行重新处理
数据源重处理,生成 searchPool
我们明确了最终我们期望得到数据源结构,那么接下来我们就对重新计算数据源,生成对应的 searchPoll
创建 components/HeaderSearch/FuseData.js
1 |
|
这样,我们就通过 generateRoutes
方法,根据咱们的路由表,生成了符合 fuse.js
的数据。
在 headerSearch
中导入 generateRoutes
1 |
|
通过 querySearch
测试搜索结果
1 |
|
渲染检索数据
数据源处理完成之后,最后我们就只需要完成:
- 渲染检索出的数据
- 完成对应跳转
那么下面我们按照步骤进行实现:
- 渲染检索出的数据
1 |
|
- 完成对应跳转
1 |
|
剩余问题处理
这里我们的 headerSearch
功能基本上就已经处理完成了,但是还存在一些小 bug
,那么最后这一小节我们就处理下这些剩余的 bug
- 在
search
打开时,点击body
关闭search
- 在
search
关闭时,清理searchOptions
headerSearch
应该具备国际化能力
明确好问题之后,接下来我们进行处理
首先我们先处理前前面两个问题:
1 |
|
接下来是国际化的问题,想要处理这个问题非常简单,我们只需要:监听语言变化,重新计算数据源初始化 fuse
即可
- 在
utils/i18n
下,新建方法watchSwitchLang
1 |
|
- 在
headerSearch
监听变化,重新赋值
1 |
|
headerSearch 方案总结
那么到这里整个的 headerSearch
我们就已经全部处理完成了,整个 headerSearch
我们只需要把握住三个核心的关键点
- 根据指定内容对所有页面进行检索
- 以
select
形式展示检索出的页面 - 通过检索页面可快速进入对应页面
保证大方向没有错误,那么具体的细节处理我们具体分析就可以了。
关于细节的处理,可能比较复杂的地方有两个:
- 模糊搜索
- 检索数据源
对于这两块,我们依赖于 fuse.js
进行了实现,大大简化了我们的业务处理流程。
tagsView 原理及方案分析
所谓 tagsView
可以分成两部分来去看:
- tags
- view
好像和废话一样是吧。那怎么分开看呢?
首先我们先来看 tags
:
所谓 tgas
指的是:位于 appmain
之上的标签
那么现在我们忽略掉 view
,现在只有一个要求:
在
view
之上渲染这个tag
仅看这一个要求,很简单吧。
views:
明确好了 tags
之后,我们来看 views
。
脱离了 tags
只看 views
就更简单了,所谓 views
:指的就是一个用来渲染组件的位置,就像我们之前的 Appmain
一样,只不过这里的 views
可能稍微复杂一点,因为它需要在渲染的基础上增加:
- 动画
- 缓存
这两个额外的功能。
加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个问题
所以 单看 views
也是一个很简单的功能。
那么接下来我们需要做的就是把 tags
和 view
合并起来而已。
那么明确好了原理之后,我们就来看 实现方案:
- 创建
tagsView
组件:用来处理tags
的展示 - 处理基于路由的动态过渡,在
AppMain
中进行:用于处理view
的部分
整个的方案就是这么两大部,但是其中我们还需要处理一些细节相关的,完整的方案为:
- 监听路由变化,组成用于渲染
tags
的数据源 - 创建
tags
组件,根据数据源渲染tag
,渲染出来的tags
需要同时具备- 国际化
title
- 路由跳转
- 国际化
- 处理鼠标右键效果,根据右键处理对应数据源
- 处理基于路由的动态过渡
那么明确好了方案之后,接下来我们根据方案进行处理即可。
创建 tags 数据源
tags
的数据源分为两部分:
- 保存数据:
appmain
组件中进行 - 展示数据:
tags
组件中进行
所以 tags
的数据我们最好把它保存到 vuex
中。
在
constant
中新建常量1
2// tags
export const TAGS_VIEW = 'tagsView'在
store/app
中创建tagsViewList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import { LANG, TAGS_VIEW } from '@/constant'
import { getItem, setItem } from '@/utils/storage'
export default {
namespaced: true,
state: () => ({
...
tagsViewList: getItem(TAGS_VIEW) || []
}),
mutations: {
...
/**
* 添加 tags
*/
addTagsViewList(state, tag) {
const isFind = state.tagsViewList.find(item => {
return item.path === tag.path
})
// 处理重复
if (!isFind) {
state.tagsViewList.push(tag)
setItem(TAGS_VIEW, state.tagsViewList)
}
}
},
actions: {}
}在
appmain
中监听路由的变化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50<script setup>
import { watch } from 'vue'
import { isTags } from '@/utils/tags'
import { generateTitle } from '@/utils/i18n'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
const route = useRoute()
/**
* 生成 title
*/
const getTitle = route => {
let title = ''
if (!route.meta) {
// 处理无 meta 的路由
const pathArr = route.path.split('/')
title = pathArr[pathArr.length - 1]
} else {
title = generateTitle(route.meta.title)
}
return title
}
/**
* 监听路由变化
*/
const store = useStore()
watch(
route,
(to, from) => {
if (!isTags(to.path)) return
const { fullPath, meta, name, params, path, query } = to
store.commit('app/addTagsViewList', {
fullPath,
meta,
name,
params,
path,
query,
title: getTitle(to)
})
},
{
immediate: true
}
)
</script>创建
utils/tags
1
2
3
4
5
6
7
8
9
10
11const whiteList = ['/login', '/import', '/404', '/401']
/**
* path 是否需要被缓存
* @param {*} path
* @returns
*/
export function isTags(path) {
return !whiteList.includes(path)
}
生成 tagsView
目前数据已经被保存到 store
中,那么接下来我们就依赖数据渲染 tags
创建
store/app
中tagsViewList
的快捷访问1
tagsViewList: state => state.app.tagsViewList
创建
components/tagsview
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103<template>
<div class="tags-view-container">
<router-link
class="tags-view-item"
:class="isActive(tag) ? 'active' : ''"
:style="{
backgroundColor: isActive(tag) ? $store.getters.cssVar.menuBg : '',
borderColor: isActive(tag) ? $store.getters.cssVar.menuBg : ''
}"
v-for="(tag, index) in $store.getters.tagsViewList"
:key="tag.fullPath"
:to="{ path: tag.fullPath }"
>
{{ tag.title }}
<i
v-show="!isActive(tag)"
class="el-icon-close"
@click.prevent.stop="onCloseClick(index)"
/>
</router-link>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
/**
* 是否被选中
*/
const isActive = tag => {
return tag.path === route.path
}
/**
* 关闭 tag 的点击事件
*/
const onCloseClick = index => {}
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
color: #fff;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 4px;
}
}
// close 按钮
.el-icon-close {
width: 16px;
height: 16px;
line-height: 10px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>在
layout/index
中导入1
2
3
4
5
6
7
8
9<div class="fixed-header">
<!-- 顶部的 navbar -->
<navbar />
<!-- tags -->
<tags-view></tags-view>
</div>
import TagsView from '@/components/TagsView'
tagsView 国际化处理
tagsView
的国际化处理可以理解为修改现有 tags
的 title
。
所以我们只需要:
- 监听到语言变化
- 国际化对应的
title
即可
根据方案,可生成如下代码:
在
store/app
中,创建修改ttile
的mutations
1
2
3
4
5
6
7/**
* 为指定的 tag 修改 title
*/
changeTagsView(state, { index, tag }) {
state.tagsViewList[index] = tag
setItem(TAGS_VIEW, state.tagsViewList)
}
在
appmain
中监听语言变化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { generateTitle, watchSwitchLang } from '@/utils/i18n'
/**
* 国际化 tags
*/
watchSwitchLang(() => {
store.getters.tagsViewList.forEach((route, index) => {
store.commit('app/changeTagsView', {
index,
tag: {
...route,
title: getTitle(route)
}
})
})
})
contextMenu 展示处理
contextMenu 为 鼠标右键事件
contextMenu 事件的处理分为两部分:
contextMenu
的展示- 右键项对应逻辑处理
那么这一小节我们先处理第一部分:contextMenu
的展示:
创建
components/TagsView/ContextMenu
组件,作为右键展示部分1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53<template>
<ul class="context-menu-container">
<li @click="onRefreshClick">
{{ $t('msg.tagsView.refresh') }}
</li>
<li @click="onCloseRightClick">
{{ $t('msg.tagsView.closeRight') }}
</li>
<li @click="onCloseOtherClick">
{{ $t('msg.tagsView.closeOther') }}
</li>
</ul>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
index: {
type: Number,
required: true
}
})
const onRefreshClick = () => {}
const onCloseRightClick = () => {}
const onCloseOtherClick = () => {}
</script>
<style lang="scss" scoped>
.context-menu-container {
position: fixed;
background: #fff;
z-index: 3000;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
</style>在
tagsview
中控制contextMenu
的展示1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43<template>
<div class="tags-view-container">
<el-scrollbar class="tags-view-wrapper">
<router-link
...
@contextmenu.prevent="openMenu($event, index)"
>
...
</el-scrollbar>
<context-menu
v-show="visible"
:style="menuStyle"
:index="selectIndex"
></context-menu>
</div>
</template>
<script setup>
import ContextMenu from './ContextMenu.vue'
import { ref, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
...
// contextMenu 相关
const selectIndex = ref(0)
const visible = ref(false)
const menuStyle = reactive({
left: 0,
top: 0
})
/**
* 展示 menu
*/
const openMenu = (e, index) => {
const { x, y } = e
menuStyle.left = x + 'px'
menuStyle.top = y + 'px'
selectIndex.value = index
visible.value = true
}
</script>
contextMenu 事件处理
对于 contextMenu
的事件一共分为三个:
- 刷新
- 关闭右侧
- 关闭所有
但是不要忘记,我们之前 关闭单个 tags
的事件还没有进行处理,所以这一小节我们一共需要处理 4 个对应的事件
刷新事件
1
2
3
4const router = useRouter()
const onRefreshClick = () => {
router.go(0)
}在
store/app
中,创建删除tags
的mutations
,该mutations
需要同时具备以下三个能力:- 删除 “右侧”
- 删除 “其他”
- 删除 “当前”
根据以上理论得出以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 删除 tag
* @param {type: 'other'||'right'||'index', index: index} payload
*/
removeTagsView(state, payload) {
if (payload.type === 'index') {
state.tagsViewList.splice(payload.index, 1)
return
} else if (payload.type === 'other') {
state.tagsViewList.splice(
payload.index + 1,
state.tagsViewList.length - payload.index + 1
)
state.tagsViewList.splice(0, payload.index)
} else if (payload.type === 'right') {
state.tagsViewList.splice(
payload.index + 1,
state.tagsViewList.length - payload.index + 1
)
}
setItem(TAGS_VIEW, state.tagsViewList)
},关闭右侧事件
1
2
3
4
5
6
7const store = useStore()
const onCloseRightClick = () => {
store.commit('app/removeTagsView', {
type: 'right',
index: props.index
})
}关闭其他
1
2
3
4
5
6const onCloseOtherClick = () => {
store.commit('app/removeTagsView', {
type: 'other',
index: props.index
})
}关闭当前(
tagsview
)1
2
3
4
5
6
7
8
9
10/**
* 关闭 tag 的点击事件
*/
const store = useStore()
const onCloseClick = index => {
store.commit('app/removeTagsView', {
type: 'index',
index: index
})
}
处理 contextMenu 的关闭行为
1 |
|
处理基于路由的动态过渡
处理基于路由的动态过渡 官方已经给出了示例代码,结合 router-view
和 transition
我们可以非常方便的实现这个功能
在
appmain
中处理对应代码逻辑1
2
3
4
5
6
7
8
9
10
11<template>
<div class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive>
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>
</div>
</template>增加了
tags
之后,app-main
的位置需要进行以下处理1
2
3
4
5
6
7
8<style lang="scss" scoped>
.app-main {
min-height: calc(100vh - 50px - 43px);
...
padding: 104px 20px 20px 20px;
...
}
</style>在
styles/transition
中增加动画渲染1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
tagsView 方案总结
那么到这里关于 tagsView
的内容我们就已经处理完成了。
整个 tagsView
就像我们之前说的,拆开来看之后,会显得明确很多。
整个 tagsView
整体来看就是三块大的内容:
tags
:tagsView
组件contextMenu
:contextMenu
组件view
:appmain
组件
再加上一部分的数据处理即可。
最后关于 tags
的国际化部分,其实处理的方案有非常多,大家也可以在后面的 讨论题 中探讨一下关于 此处国家化 的实现,相信会有很多新的思路被打开的。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!