现在有资料,只差介面了。
虽然每个视窗功能都不同,但是视窗外框功能都一样,所以我们建立 base-window.vue
组件透过 slot
保留弹性,其他特定功能的卡片只要引入 base-window.vue
并透过 slot
就可以加入不同的功能。
预期长这样:
base-window.vue
功能需求:
title bar
视窗内容可以任意抽换。
使用 slot
实现。
建立 src\components\base-window.vue
。
src\components\base-window.vue <template lang="pug">
.base-window(
@click.stop='handleClick',
:style='style',
:class='classes',
@touchstart.stop,
@contextmenu.stop
)
q-bar.base-window-header-bar(v-touch-pan.prevent.mouse='handleMove')
q-icon(:name='headerIcon', :color='color')
q-space
.base-window-title.text-shadow {{ title }}
q-space
q-btn(
@click='handleClose',
icon='r_close',
dense,
flat,
rounded,
color='grey-5'
)
.base-window-body(:class='bodyClass')
slot
样式部分预期设计 Focus 效果,利用阴影呈现高低落差,所以在 quasar.variables.sass
建立阴影样式变数。
src\styles\quasar.variables.sass
// ...
$unfocus-shadow: 0 0px 20px rgba(#000, 0.05), 0 2.8px 2.2px -30px rgba(0, 0, 0, 0.02),0 6.7px 5.3px -30px rgba(0, 0, 0, 0.028),0 12.5px 10px -30px rgba(0, 0, 0, 0.035),0 22.3px 17.9px -30px rgba(0, 0, 0, 0.042),0 41.8px 33.4px -30px rgba(0, 0, 0, 0.05),0 100px 80px -30px rgba(0, 0, 0, 0.07)
$focus-shadow: 0 0px 20px rgba(#000, 0.05),0 2.8px 2.2px rgba(0, 0, 0, 0.02),0 6.7px 5.3px rgba(0, 0, 0, 0.028),0 12.5px 10px rgba(0, 0, 0, 0.035),0 22.3px 17.9px rgba(0, 0, 0, 0.042),0 41.8px 33.4px rgba(0, 0, 0, 0.05),0 100px 80px rgba(0, 0, 0, 0.07)
@import '~quasar-variables-styl'
src\components\base-window.vue <style scoped lang="sass">
@import '@/styles/quasar.variables.sass'
.base-window
position: fixed
min-width: 200px
min-height: 100px
overflow: hidden
transition-duration: 0.5s
transform: translateZ(0px)
transition-timing-function: cubic-bezier(0.83, 0, 0.17, 1)
box-shadow: $unfocus-shadow
border-radius: $border-radius-m
background: rgba(white, 0.8)
backdrop-filter: blur(4px)
&.moving
transition: top 0s, left 0s, transform 0.5s, box-shadow 0.5s
.base-window-header-bar
height: auto
padding: 20px
padding-bottom: 14px
cursor: move
background: none
color: $grey-8
.base-window-title
font-size: 14px
user-select: none
margin: 0px
position: relative
font-weight: 900
transition-duration: 0.4s
letter-spacing: 1px
.base-window-body
position: relative
src\components\base-window.vue <script>
export default {
name: 'BaseWindow',
components: {},
props: {
// 视窗起始位置
pos: {
type: Object,
default() {
return {
x: 0,
y: 0,
};
},
},
// 可以额外增加 class
bodyClass: {
type: String,
default: '',
},
// title bar 文字
title: {
type: String,
default: 'title',
},
// title bar icon 名称
headerIcon: {
type: String,
default: 'r_dashboard',
},
// title bar icon 颜色
headerIconColor: {
type: String,
default: 'blue-grey-4',
},
},
data() {
return {
// 目前视窗移动量
offset: {
x: 0,
y: 0,
},
status: {
isMoving: false,
},
};
},
computed: {
style() {
const xSum = this.offset.x;
const ySum = this.offset.y;
const style = {
zIndex: this.zIndex,
top: `${ySum}px`,
left: `${xSum}px`,
};
return style;
},
classes() {
const classes = [];
if (this.status.isMoving) {
classes.push('moving');
}
return classes;
},
},
watch: {},
created() {
this.offset.x = this.pos.x;
this.offset.y = this.pos.y;
},
mounted() {},
methods: {
handleClick() {},
handleClose() {},
/** 处理拖动事件
*
* 使用 Quasar v-touch-pan 指令实现
*
* [参考资料](https://v1.quasar.dev/vue-directives/touch-pan)
*/
handleMove({ isFinal, delta }) {
// console.log(`[ handleMove ] delta : `, delta);
this.status.isMoving = !isFinal;
// 累加每次移动变化量。
this.offset.x += delta.x;
this.offset.y += delta.y;
},
},
};
最後回到 app.vue
,直接将 base-window 加入 HTML 看看效果。
src\app.vue
<template lang="pug">
.screen
.info.font-orbitron(v-if='firmwareName')
| {{ firmwareName }} - v{{ ver }}
dialog-system-setting
base-window(:pos='{ x: 50, y: 50 }')
</template>
<style lang="sass">
// ...
</style>
<script>
// ...
import BaseWindow from '@/components/base-window.vue';
export default {
name: 'App',
components: {
'dialog-system-setting': DialogSystemSetting,
'base-window': BaseWindow,
},
// ...
};
</script>
看起来真不错 (≖‿ゝ≖)✧
接下来实际建立一个真正的视窗。
在 Vuex 中建立 window 模组,储存目前显示视窗与视窗相关数值。
设计 Window 资料格式
component
:组件名称
不同的视窗组件名称。
id
:视窗 ID
唯一 ID,用於识别视窗。
focusAt
:聚焦时间
判断视窗重叠关系
src\store\modules\window.store.js
/**
* 管理视窗相关资料
*/
/**
* @typedef {import('vuex').Module} Module
*
* @typedef {import('@/types/type').Window} Window
*/
/** @type {Module} */
const self = {
namespaced: true,
state: () => ({
/** @type {Window[]} */
list: [],
}),
mutations: {
},
actions: {
},
getters: {
},
modules: {
},
};
export default self;
并在 Vuex 引入。
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import core from './modules/core.store';
import board from './modules/board.store';
import window from './modules/window.store';
export default new Vuex.Store({
// ...
modules: {
core, board, window
},
});
接着增加「新增、删除视窗」的功能。
每个视窗要建立一个专属的 ID,所以先我们在 utils 新增 getRandomString()
/** 取得随机长度字串
* @param {number} len 指定字串长度
* @param {String} [charSet] 指定组成字符
* @return {string}
*/
export function getRandomString(len = 5, charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
let randomString = '';
for (let i = 0; i < len; i++) {
const randomPoz = Math.floor(Math.random() * charSet.length);
randomString += charSet.substring(randomPoz, randomPoz + 1);
}
return randomString;
}
回到 src\store\modules\window.store.js
/** @type {Module} */
const self = {
// ...
mutations: {
/** 新增视窗 */
add(state, component) {
/** @type {Window} */
const window = {
component,
key: getRandomString(),
focusAt: dayjs().valueOf(),
}
state.list.push(window);
},
/** 删除视窗 */
remove(state, id) {
/** @type {Window[]} */
const windows = state.list;
const targetIndex = windows.findIndex((window) =>
window.id === id
);
if (targetIndex < 0) {
console.error(`[ window.store remove ] window 不存在,id : `, id);
return;
}
windows.splice(targetIndex, 1);
},
},
// ...
};
export default self;
接着建立范例视窗 src\components\window-example.vue
<template lang="pug">
base-window(
:pos='pos',
body-class='c-col p-20px pt-20px',
title='范例视窗'
)
</template>
<style lang="sass">
</style>
<script>
import BaseWindow from '@/components/base-window.vue';
export default {
name: 'WindowExample',
components: {
'base-window': BaseWindow,
},
props: {
pos: {
type: Object,
default() {
return {
x: 0,
y: 0,
};
},
},
},
data() {
return {
id: this.$vnode.key,
};
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {},
};
</script>
最後我们回到 app.vue
,新增以下功能。
window-example.vue
组件addWindow()
,向 Vuex 提交新增视窗src\app.vue <script>
/**
* @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
* @typedef {import('@/types/type').Window} Window
*/
// ...
import WindowExample from '@/components/window-example.vue';
export default {
name: 'App',
components: {
'dialog-system-setting': DialogSystemSetting,
'window-example': WindowExample,
},
// ...
computed: {
...mapState({
/** @type {PortTransceiver} */
portTransceiver: (state) => state.core.transceiver,
ver: (state) => state.board.info.ver,
firmwareName: (state) => state.board.info.firmwareName,
/** @type {Window[]} */
windows: (state) => state.window.list,
}),
},
// ...
methods: {
// ...
addWindow(name) {
this.$store.commit('window/add', name);
},
},
};
在 pug 中增加「新增视窗用的右键选单」并「显示目前所有视窗」。
src\app.vue <template lang="pug">
.screen
.windows
component(
v-for='(window, i) in windows',
:is='window.component',
:key='window.id',
:pos='{ x: (i + 1) * 30, y: (i + 1) * 30 }'
)
.info.font-orbitron(v-if='firmwareName')
| {{ firmwareName }} - v{{ ver }}
dialog-system-setting
// 右键选单
q-menu(context-menu, content-class='border-radius-s')
q-list.min-w-260px
q-item(@click='addWindow("window-example")', clickable, v-close-popup)
q-item-section
| 新增「范例视窗」
可以任意新增视窗了!
可以看到现在就算点击视窗,也不会改变视窗堆叠的顺序,这样没办法看到最先生成的视窗内容,来着手加入调整重叠顺序功能吧!
预期功能
我们先透过 provide / inject 将视窗的 id 注入至所有子组件中。
src\components\window-example.vue <script>
import BaseWindow from '@/components/base-window.vue';
export default {
name: 'WindowExample',
// ...
data() {
return {
id: this.$vnode.key,
};
},
provide() {
return {
id: this.id,
};
},
// ...
};
src\components\base-window.vue <script>
export default {
name: 'BaseWindow',
// ..
inject: ['id'],
// ..
created() {
if (!this.id) {
throw new Error(`父组件必须透过 provide / inject 提供 id 数值`);
}
this.offset.x = this.pos.x;
this.offset.y = this.pos.y;
},
// ..
};
接着在 src\store\modules\window.store.js
新增 focus 相关功能。
/**
* 管理视窗相关资料
*/
// ...
/** @type {Module} */
const self = {
namespaced: true,
state: () => ({
/** @type {Window[]} */
list: [],
focusId: null,
}),
mutations: {
// ...
/** 设目前 Focus 视窗 */
setFocus(state, id) {
state.focusId = id;
/** @type {Window[]} */
const windows = state.list;
const target = windows.find((window) =>
window.id === id
);
if (!target) {
return;
}
target.focusAt = dayjs().valueOf();
},
},
// ...
};
export default self;
并於 base-window.vue
新增 focus()
Method
src\components\base-window.vue <script>
export default {
name: 'BaseWindow',
// ...
methods: {
foucs() {
this.$store.commit('window/setFocus', this.id);
},
handleClick() {
this.foucs();
},
/** 处理拖动事件
*
* 使用 Quasar v-touch-pan 指令实现
*
* [参考资料](https://v1.quasar.dev/vue-directives/touch-pan)
*/
handleMove({ isFirst, isFinal, delta }) {
// 拖动时让视窗 focus
if (isFirst) {
this.foucs();
}
// ...
},
// ...
},
};
试试看 Vuex 有没有储存目前 focus 视窗的 ID。
成功!接下来就是最关键的一步,以 focusAt
为依据,计算每个视窗的 z-index 达成自动调整重叠效果。
src\store\modules\window.store.js
在 getters
加入 zIndexMap
/**
* 管理视窗相关资料
*/
/**
* @typedef {import('vuex').Module} Module
*
* @typedef {import('@/types/type').Window} Window
*/
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import { getRandomString } from '@/script/utils/utils';
/** @type {Module} */
const self = {
namespaced: true,
// ...
getters: {
/** Window 对应的 z-index
*
* 视窗 ID 与 z-index 以 key-value 对应
* @example
* map['abcds']: 1
* map['gr56w']: 2
*/
zIndexMap: (state) => {
/** @type {Window[]} */
const windows = cloneDeep(state.list);
windows.sort((a, b) => a.focusAt > b.focusAt ? 1 : -1);
return windows.reduce((map, window, index) => {
map[window.id] = index;
return map;
}, {});
},
},
};
export default self;
接着在 base-window.vue
中提取 getters zIndexMap
,用来取得自身 z-index
src\components\base-window.vue <script>
export default {
name: 'BaseWindow',
// ...
computed: {
zIndex() {
const zIndexMap = this.$store.getters['window/zIndexMap'];
return zIndexMap?.[this.id] ?? 0;
},
style() {
const xSum = this.offset.x;
const ySum = this.offset.y;
const style = {
zIndex: this.zIndex,
top: `${ySum}px`,
left: `${xSum}px`,
};
return style;
},
// ...
},
// ...
};
尝试看看堆叠有没有变化。
接着加点视窗 focus 样式,让效果看起来酷一点 (´,,•ω•,,)
src\components\base-window.vue <script>
export default {
name: 'BaseWindow',
// ...
computed: {
// ...
classes() {
const classes = [];
if (this.isFoucs) {
classes.push('focused');
}
if (this.status.isMoving) {
classes.push('moving');
}
return classes;
},
isFoucs() {
return this.$store.state.window.focusId === this.id;
},
},
// ...
};
src\components\base-window.vue <style scoped lang="sass">
@import '@/styles/quasar.variables.sass'
.base-window
position: fixed
min-width: 200px
min-height: 100px
overflow: hidden
transition-duration: 0.5s
transform: translateZ(0px)
transition-timing-function: cubic-bezier(0.83, 0, 0.17, 1)
box-shadow: $unfocus-shadow
border-radius: $border-radius-m
background: rgba(white, 0.8)
backdrop-filter: blur(4px)
&.focused
background: rgba(white, 0.98)
transform: translateY(-2px)
box-shadow: $focus-shadow
.base-window-header-bar .base-window-title
letter-spacing: 2px
transition-delay: 0.2s
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1)
// ...
并在 app.vue
加入去除 focus 的事件,让滑鼠点到桌面时,所有视窗会取消 focus。
src\app.vue <template lang="pug">
.screen(@click='handleClick')
// ...
src\app.vue <script>
// ...
export default {
name: 'App',
// ...
methods: {
handleClick() {
this.$store.commit('window/setFocus', null);
},
// ...
},
};
最後就是关闭视窗功能了,由於我们已经在 window.store.js
完成删除功能(remove()
)。
所以只要完成 base-window.vue
预留的 handleClose()
即可。
src\components\base-window.vue <script>
export default {
name: 'BaseWindow',
// ...
methods: {
// ...
handleClose() {
this.$store.commit('window/remove', this.id);
},
// ...
},
};
一行完成!ヽ(●`∀´●)ノ
好像太快了.. ( ・ิω・ิ),那就来帮视窗出现与消失加上动画吧!
新增集中动画样式的 sass 档案。
src\styles\animation.sass
.fade-right-enter-active, .fade-right-leave-active
transition-duration: 0.4s
pointer-events: none
.fade-right-enter, .fade-right-leave-to
transform: translateX(5px) !important
opacity: 0 !important
.fade-up-enter-active, .fade-up-leave-active
transition-duration: 0.4s
pointer-events: none
.fade-up-enter, .fade-up-leave-to
transform: translateY(-5px) !important
opacity: 0 !important
.opacity-enter-active, .opacity-leave-active
transition-duration: 0.4s
pointer-events: none
.opacity-enter, .opacity-leave-to
opacity: 0 !important
.list-complete-enter-active, .list-complete-leave-active, .list-complete-move
transition-duration: 0.4s
pointer-events: none
.list-complete-enter, .list-complete-leave-to
opacity: 0 !important
transform: translateY(30px) !important
.list-complete-leave-active
position: absolute !important
@keyframes bounce
40%
transform: scale(1.2)
60%
transform: scale(0.9)
80%
transform: scale(1.05)
100%
transform: scale(1)
@keyframes jelly-bounce
40%
transform: scale(0.5, 1.5)
60%
transform: scale(1.3, 0.7)
80%
transform: scale(0.9, 1.1)
100%
transform: scale(1, 1)
并在 src\main.js
引入 animation.sass
import Vue from 'vue';
import App from './app.vue';
import router from './router/router';
import store from './store/store';
import './quasar';
import i18n from './i18n';
import '@/styles/global.sass';
import '@/styles/animation.sass';
import 'windi.css';
Vue.config.productionTip = false;
new Vue({
router,
store,
i18n,
render: (h) => h(App),
}).$mount('#app');
最後在 app.vue
中把原本的 .windows div 换成 transition-group
src\app.vue <template lang="pug">
.screen(@click='handleClick')
transition-group.windows(name='fade-up', tag='div')
component(
v-for='(window, i) in windows',
:is='window.component',
:key='window.id',
:pos='{ x: (i + 1) * 30, y: (i + 1) * 30 }'
)
// ...
完成了!✧*。٩(ˊᗜˋ*)و✧*。
大家可以自行加入更酷的渐入渐出动画喔
以上我们成功完成视窗的基本功能了,接下来终於要进入我们硬体整合的部分了!
(电子助教:我终於可以登场了吗... (›´ω`‹ ))
以上程序码已同步至 GitLab,大家可以前往下载:
>>: Day10 | Dart 非同步 - async/awiat
先来谈谈IDE是什麽吧! IDE,Integrated Development Environmen...
创建App-完结 30天的学习日记就这样慢慢结束啦! 从第一天的专案设计到现成的样子,过程学习、错误...
今天的主题是Yolo v2,不过所找到的资料图片可能比较不好理解,有更好的再更新,Yolo v2就是...
还不会创建ThinkPHP V5.1专案的朋友们可以先去看看创建ThinkPHP V5.1专案。 何...
前言: 屁屁痛了一整晚昨天全程跪着打文章,都这样了你们该进来看一下了吧,顺带一提如果有对Swift其...