书接上篇:响应式项目(RxJS+Vue.js+Spring)大决战(4):主页的实现(后端服务模块)
5.2 前端视图模块
5.2.1 整体结构的设计
前端模块app-view/home负责主页视图的建构,其结构如下图所示:
本篇所述方法,体现了极强的独特性、技巧性!
5.2.2 主页home.html
对home.html现有内容作如下修改:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>教务辅助管理系统--官方主页</title>
<link href="image/favicon.ico" rel="icon" type="image/x-icon">
<script type="importmap">
{
"imports": {
"vue": "./lib/vue.esm-browser.prod.js",
"vue-demi": "./lib/vue-demi.js",
"pinia": "./lib/pinia.esm-browser.js",
"rxjs": "./lib/rxjs.min.js",
"rxjsFetch": "./lib/rxjs-fetch.min.js",
"rxjsAjax": "./lib/rxjs-ajax.min.js",
"rxjsWebsocket": "./lib/rxjs-websocket.min.js",
"echarts": "./lib/echarts.esm.min.js",
"vue3SfcLoader": "./lib/vue3-sfc-loader.esm.js"
}
}
</script>
</head>
<body>
<div id="tamsApp"></div>
<script src="home.js" type="module"></script>
</body>
</html>
type="module"是ES6中的语法规范:启用模块模式,以便能够在“.js”文件中使用import导入其他模块。
5.2.3 主页脚本home.js
主页脚本home.js的代码非常简洁:
import {createApp} from 'vue'
import {createPinia} from 'pinia' //导入状态管理
import sfcLoader from './sfc-loader.js' //导入SFC加载器
import appPlugins from "./plugins/app.plugins.js" //导入public模块中的插件
createApp(sfcLoader) //创建Vue应用
.use(createPinia()) //创建Pinia实例以便进行状态管理
.use(appPlugins) //安装插件
.mount('#tamsApp') //挂载到tamsApp层
SFC加载器负责加载各.vue组件,而app.plugins.js中定义了全局公共插件。
注意,由于app.plugins.js中定义的插件,是为整个项目的各模块服务的,因此并没有放置在模块home下,而是保存在app-view/public模块的resources/static/plugins文件夹下。
5.2.4 SFC加载器sfc-loader.js
通常,对于.vue组件,需要使用专门的编译打包工具,例如webpack、rollup等等,将其编译成浏览器可识别的代码。这些编译打包工具,往往需要作各种繁琐的构建配置,还需要Node.js支持!那么,对于非webpack、非Node.js环境,如何处理?
第三方插件vue3-sfc-loader.js,专门用于在运行时动态加载.vue文件,能够加载、解析、编译.vue文件中的模板、JavaScript脚本和CSS样式,并分析依赖项进行递归解析。vue3-sfc-loader.js不需要安装配置Node.js,也不需要webpack!vue3-sfc-loader.js包含了Vue3(Vue2)编译器、Babel JavaScript编译器、CSS转换器postcss、JavaScript标准库补丁库core-js,且内置了对ES6的支持!vue3-sfc-loader.js简单易用,几乎不需要做任何繁琐配置!!
关于vue3-sfc-loader.js,更详细的内容,可通过这个网址https://www.jsdelivr.com/package/npm/vue3-sfc-loader进行了解。
模块加载器sfc-loader.js,基于vue3-sfc-loader.js,为系统加载单文件组件SFC(.vue文件)提供强大支持。代码如下:
import * as vue from 'vue'
import * as rxjs from 'rxjs'
import * as rxjsFetch from 'rxjsFetch'
import * as rxjsAjax from 'rxjsAjax'
import * as rxjsWebsocket from 'rxjsWebsocket'
import * as echarts from 'echarts'
import {loadModule} from 'vue3SfcLoader'
const options = {
moduleCache: {
vue: vue, //缓存vue
rxjs: rxjs, //缓存RxJS常规函数库
fetch: rxjsFetch, //缓存RxJS的fetch函数
ajax: rxjsAjax, //缓存RxJS的ajax函数
websocket: rxjsWebsocket, //缓存RxJS的websocket函数
echarts: echarts //缓存Echarts图形库
},
getFile(url) { //加载.vue组件
return fetch(url).then(response => response.ok ?
response.text() : Promise.reject(response))
},
addStyle(styleStr) { //加载.css文件
const style = document.createElement('style')
style.textContent = styleStr
const ref = document.head
.getElementsByTagName('style')[0] || null
document.head.insertBefore(style, ref)
}
}
//异步加载页面主体组件home.index.vue
export default vue.defineAsyncComponent(() => loadModule('home.index.vue', options))
5.2.5 页面主体组件home.index.vue
页面主体组件将整个主页划分成4大部分:header(头部)、menu(菜单)、body(主体)、footer(底部),并利用Vue的<component>动态组件属性is,来动态加载系统的各个功能组件。代码如下:
<script setup>
import HomeLayout from './home.layout.vue' //导入页面布局组件
import HomeHeader from './home.header.vue' //导入标题及消息显示组件
import HomeMenu from './home.menu.vue' //导入导航菜单组件
import HomeBody from './home.body.vue' //导入页面主体组件
import HomeFooter from './home.footer.vue' //导入页面底部组件
</script>
<template>
<home-layout> <!--对应HomeLayout组件-->
<template #header>
<home-header></home-header> <!--对应HomeHeader组件-->
</template>
<template #menu>
<home-menu></home-menu> <!--对应HomeMenu组件-->
</template>
<template #body>
<home-body></home-body> <!--对应HomeBody组件-->
</template>
<template #footer>
<home-footer></home-footer> <!--对应HomeFooter组件-->
</template>
</home-layout>
</template>
5.2.6 页面布局组件home.layout.vue
布局组件使用具名插槽slot来匹配显示页面的4大部分:header、menu、body和footer,代码如下:
<template>
<div id="tamsApp">
<header>
<slot name="header"></slot>
</header>
<menu>
<slot name="menu"></slot>
</menu>
<main>
<slot name="body"></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<style scoped>
#tamsApp {
position: absolute; /* 绝对定位 */
width: 851px;
height: 551px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0 auto; /* 上下外边距为0,左右自动,即居中 */
padding: 0;
text-align: center; /* 文字居中 */
border: 1px dotted #4071e2; /* 边框:1像素、点线、浅蓝色 */
/* 水平、垂直方向阴影距离为1px、阴影展开大小为1px、颜色为灰色 */
box-shadow: 1px 1px 2px #ecdfdf;
}
:global(input, span, button) {
font-size: 15px;
}
:global(img) {
vertical-align: middle; /* 图片垂直方向对齐 */
}
</style>
5.2.7 标题及消息显示组件home.header.vue
home.header.vue用来构建主页标题部分的内容,主要包括:系统名称、登录用户的logo头像、后端推送的消息数量及消息内容。代码如下:
<script setup>
import {getCurrentInstance} from 'vue'
const store = getCurrentInstance().appContext.config
.globalProperties.$pinia._s.get('loginer')
</script>
<template>
<div class="home-header">
教务辅助<img alt="logo" src="image/logo.png"/>管理系统
<div v-show="store.user.username!=null" class="home-loginer">
<img :src="store.user.logo" :style="store.msgStyle"
alt="消息" height="16" width="16"/>
<span :style="store.msgStyle">{{ store.user.username }}</span>
<div :title="store.message" class="msg-title">
{{ store.msgCount }}
</div>
</div>
</div>
</template>
<style scoped>
.home-header {
width: 848px;
height: 28px;
float: left;
text-align: center;
margin: 0 auto;
padding: 2px;
font-size: 20px;
border: 0;
background: rgba(216, 234, 234, 0.6);
}
.home-loginer {
position: relative;
width: 8em;
height: 1.7em;
font-size: 0.7em;
color: #00f;
top: 3px;
right: 1em;
border: 0 solid #f15555;
float: right;
text-align: right;
padding: 1px;
margin: 0;
display: table-cell;
z-index: 99999;
}
.msg-title {
position: relative;
left: 0;
top: -6px;
border-radius: 50%;
display: inline-block;
text-align: center;
font-style: normal;
color: #fff;
background-color: #f15555;
width: 1.9em;
height: 1.7em;
line-height: 1.7em;
font-size: 0.7em;
cursor: pointer;
}
</style>
值得注意的是代码中的store对象!这里通过Vue当前实例,再通过上下文属性appContext获取应用的配置,并利用globalProperties.$pinia._s,拿到id为loginer的pinia状态管理数据。也就是说,系统利用Pinia,将用户登录状态数据保存起来。该状态数据的id号为loginer,后续会专门实现状态管理代码。
5.2.8 导航菜单组件home.menu.vue
导航组件使用无序列表构建页面的导航菜单,代码如下:
<script setup>
import {defineComponent, getCurrentInstance, h} from 'vue'
const menus = [
{id: 'home', name: '首\u3000页'},
{id: 'login.index', name: '用户登录'},
{id: 'regist.index', name: '用户注册'},
{id: 'college.index', name: '学院风采'},
{id: 'query.index', name: '学生查询'},
{id: 'enroll.index', name: '招生一览'},
{id: 'upload.index', name: '资料上传'},
{id: 'chat.index', name: '畅论空间'}
]
const appCtx = getCurrentInstance().appContext //获取当前Vue应用程序上下文的相关数据
//异步加载组件
Promise.all( //若为home,则设置为null组件
menus.map(m => m.id === 'home' ? null : import(`./modules/${m.id}.vue`))
).then(modules => modules.forEach(m => m == null ?
appCtx.app.component('home', m) : appCtx.app.component(m.__name, m)))
const store = appCtx.config.globalProperties.$pinia._s.get('loginer')
const TamsMenu = defineComponent({
render() {
return h('div', {class: 'home-menu'},
h('ul', {class: 'home-ul'},
menus.map(({id, name}) =>
h('li', {
id: id,
innerText: name,
//利用Pinia的Action方法setModule更改当前模块
onClick: event => store.setModule(event.target.id)
}))
))
}
})
</script>
<template>
<tams-menu></tams-menu>
</template>
<style>
.home-menu {
position: relative;
float: left;
width: 850px;
height: 30px;
font-size: 16px;
left: -40px;
top: 0;
/* 背景色:设置红、绿、蓝色彩值,不透明度为0.2 */
background: rgba(236, 223, 223, 0.2);
}
.home-ul {
position: relative;
width: 848px;
height: 23px;
top: 0;
font-size: 16px;
list-style: none; /* 去掉列表符号 */
padding: 1px; /* 空白填充量 **/
margin: 1px;
display: flex; /* 弹性布局 */
justify-content: space-around; /* 列表项间隔均等 */
}
.home-ul li:hover {
color: #fff;
background-color: #f1625d;
border-radius: 3px; /* 矩形四边角的弧度 */
cursor: pointer; /* 手形鼠标 */
box-shadow: 2px 2px 2px #d7d4d4;
}
</style>
代码利用加载组件的“__name”属性,将该组件注册到Vue应用中。注意:__name是两个下画线“_”加上name组成!
在构建TamsMenu组件时,直接利用渲染函数render()构建主页菜单。当点击某个菜单项时,通过状态函数setModule(event.target.id)传递当前组件的id号,并利用Vue的<component :is="store.module"></component>动态组件属性is,达到响应式动态加载功能模块的目的!
5.2.9 页面主体组件home.body.vue
该组件比较简单,主要是利用Vue动态组件的is属性,来加载并显示当前导航菜单所对应的功能模块:
<script setup>
import {getCurrentInstance} from 'vue'
//获取状态数据
const store = getCurrentInstance().appContext.config
.globalProperties.$pinia._s.get('loginer')
</script>
<template>
<div class="home-body">
<component :is="store.module"></component>
</div>
</template>
<style scoped>
.home-body {
position: relative;
width: 100%;
height: 460px;
font-size: 16px;
top: 0;
float: left; /* 靠左浮动 */
text-align: center;
margin: 0 auto;
padding: 1px;
border: 0; /* 无边框 */
/* 背景图像:图片源、水平垂直方向不拉伸、泊靠顶部、居中 */
background: url("image/homebg.png") no-repeat top center;
background-size: 848px 475px; /* 背景图像固定大小 */
background-origin: padding-box; /* 背景图像相对于内边距框来定位 */
z-index: 9;
}
</style>
5.2.10 页面底部组件
该组件代码非常简单:
<template>
<span class="copyright">
版权所有 ©2023 Copyrights all reserved
</span>
</template>
<style scoped>
.copyright {
font-size: 10px; /* 文字大小 */
color: #cecccc; /* 前景色 */
}
</style>
5.3 插件
在app-view/public模块(注意不是home模块)的src/main/resources/static/plugins文件夹下新建文件app.plugins.js。我们在该文件中定义下面2个自定义指令:
- focus:设置当某个input元素被Vue插入到DOM中后,自动获得焦点。该指令的应用形式为:v-focus。为什么不通过设置input元素的autofocus属性自动获得焦点?因为autofocus仅在首次加载时有效,当Vue组件模块动态切换时,并不能自动获得焦点。
- submitButton:定义一个具有统一外观样式的提交按钮,并允许通过传递width参数值来定制按钮长度。该指令的应用形式为:v-submit-button。
这些指令以插件形式,注册到应用层级,这样的话系统中的所有组件均可使用。
import {h, render} from 'vue'
import {filter, fromEvent, mergeWith, Observable, scan, switchMap, tap} from 'rxjs'
import {useStore} from '../store/index.js' //导入状态管理
export default {
name: 'app.plugins',
install: (app, _) => {
useStore() //初始化状态数据
app.directive('focus', { //定义focus指令
mounted: (element) => element.focus()
})
app.directive('submitButton', { //定义submitButton指令
mounted: (element, binding) => {
const style = {
cursor: 'pointer',
width: binding.value.width, //binding.value是用户传送的CSS值
height: '27px',
textAlign: 'center',
color: '#fff',
background: binding.value.background,
border: '1px solid #70abe7',
borderRadius: '3px',
fontSize: '15px'
}
Reflect.ownKeys(style).forEach(key => { //反射获取style的各属性
//将style的key所对应属性值一一赋值给element元素的对应属性
element.style[key] = style[key]
})
}
})
}
}
5.4 状态管理
状态管理相关组件位于public模块下,其结构如下图所示。
5.4.1 states定义状态量
在public/src/main/resources/static/store文件夹下新建states.js。然后,在states.js中定义4个状态量:module,当前加载的模块;user,登录用户;token,登录用户的令牌;message,后台服务器推送的消息。代码如下:
export default {
name: 'store.states',
module: null,
user: {
username: null,
logo: null,
role: null
},
token: null,
message: ''
}
5.4.2 actions.js更改状态数据
在public/src/main/resources/static/store文件夹下新建actions.js文件。与上一节states.js中的状态量相对应,actions.js定义了4个setter方法,可用来更改前述4个状态量的值:
export default {
name: 'store.actions',
setModule: function (module) {
this.module = module
},
setUser: function (user) {
this.user = user
},
setToken: function (token) {
this.token = token
},
setMessage: function (message) {
this.message = message
}
}
5.4.3 getters.js计算函数
在public/src/main/resources/static/store文件夹下新建getters.js文件。getters.js中主要包含2个计算函数:
- msgStyle,定义后台推送消息的样式。如果用户登录状态已失效,则将用户logo头像设置为灰色,以便提示该用户处于登录失效状态。
- msgCount,统计后端服务推送的消息总数。
getters.js的代码如下:
export default {
name: 'store.getters',
msgStyle: ({token}) =>
({
color: token == null ? '#ccc' : '#00f', //若登录失效则文字设置为灰色
filter: token == null ? 'grayscale(1)' : 'grayscale(0)' //应用灰度转换滤镜
}),
msgCount: ({message}) => {
let regex = new RegExp(/\u000D/g) //匹配消息中的换行符\u000D
let m = message.match(regex) //匹配消息
return m ? m.length : 0 //根据匹配结果返回消息数量
}
}
5.4.4 index.js创建状态实例
在public/src/main/resources/static/store文件夹下新建index.js文件。现在,可利用前面创建好的状态量、计算函数、Action方法,来定义并输出Pinia Store对象useStore:
import storeStates from './states.js'
import storeGetters from './getters.js'
import storeActions from './actions.js'
import {defineStore} from 'pinia'
export const useStore = defineStore({
id: 'loginer', //Pinia状态管理标识ID
state: () => ({...storeStates}), //展开storeStates中的属性或对象
getters: {...storeGetters}, //展开storeGetters中的计算函数
actions: {...storeActions} //展开storeActions中的操作方法
})
5.5 通用进度提示组件
通用进度提示组件类似于进度条,用于提示用户当前任务的处理状态。该组件采用动画gif图片+提示文字的组合形式,例如下图表示当前正在上传文件的进度提示。用户可自由控制:组件的显示/隐藏;动画gif图片的显示/隐藏;提示文字的内容。
在public/src/main/resources/static/modules文件夹下新建文件loading.vue,代码如下:
<script setup>
import {toRef} from 'vue'
const props = defineProps({
isLoading: Object, //该属性控制是否显示进度提示组件
imaged: {type: Boolean, default: true} //该属性控制是否显示动画gif图片
})
// visible链接到isLoading.visible,与其值保持同步
const visible = toRef(() => props.isLoading.visible)
</script>
<template>
<span v-show="visible">
<img v-show="imaged" alt="正在进行"
height="45" src="image/doing.gif" width="50"/>
<slot>正在进行...</slot>
</span>
</template>
<style scoped>
span {
color: #F00;
font-size: 14px;
width: max-content; /*元素中的最大宽度作为整体宽度*/
height: max-content; /*元素中的最大高度作为整体高度*/
}
</style>
至此,整个主页模块的代码编写工作结束。现在可继续通过Gradle面板再次启动项目,然后在浏览器地址栏输入:http://192.168.1.5/,将显示主页界面,主页构建成功!
提示:由于其他功能模块并没有编写完成,import(`./modules/${m.id}.vue`)并不能导入这些模块,因此如果打开浏览器控制台,会显示报错信息,但这并不妨碍主页界面的成功渲染。后续,逐步补充完善这些模块。
下一步:用户登录,且听下回分解。
标签:vue,Spring,视图,js,Vue,import,组件,home,id From: https://blog.csdn.net/acoease/article/details/143369233