今天开始使用 vue3 + ts 搭建一个项目管理的后台,因为文章会将项目的每一个地方代码的书写都会讲解到,所以本项目会分成好几篇文章进行讲解,我会在最后一篇文章中会将项目代码开源到我的GithHub上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关注本专栏,学习更多前端vue知识,然后开篇先简单介绍一下本项目用到的技术栈都有哪几个方面(阅读本文章能够学习到的技术):
当然还有许多其他的需要安装的第三方库,这里就不再一一介绍了,在项目中用到的地方自行会进行讲解,大家自行学习即可,现在就让我们走进vue3+ts的实战项目吧。
目录
首页与页脚内容搭建
接下来实现首页与页脚内容的搭建与展示,首页的内容展示就很简单了,只需要一个card用来包含一些文章图片内容,卡片下面再放置一个svg图标资源即可,整体的排版布局非常的简易,具体的代码如下:
在svg图标资源处,我设置了一下 Animate.css 动画库的一个样式,给首页整体的印象加点色彩
<template>
<div>
<el-card>
<div class="box">
<img :src="userStore.avatar" alt="头像图片" class="avatar" />
<div class="bottom">
<h3 class="title">{{ getTime() }}好呀{{ userStore.username }}</h3>
<p class="subtitle">商品运营管理平台</p>
</div>
</div>
</el-card>
<div class="bottoms animate__animated animate__backInUp">
<svg-icon name="welcome" width="600px" height="400px"></svg-icon>
</div>
</div>
</template>
关于首页获取用户的昵称,这里可以通过我们以前设置的关于存储用户数据的仓库来获取,如下:
<script setup lang="ts">
// 引入用户相关的仓库,获取当前用户的头像和昵称
import useUserStore from '@/store/user'
import { getTime } from '@/utils/time'
// 获取存储用户信息的仓库对象
let userStore = useUserStore()
</script>
设置完基本布局之后,就可以给其添加一些css样式了,具体的css代码如下:
<style lang="scss" scoped>
.box {
display: flex;
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
}
.bottom {
margin-left: 20px;
.title {
font-size: 25px;
font-weight: 900;
margin-bottom: 30px;
}
.subtitle {
font-style: italic;
color: #5bd4d4;
}
}
}
.bottoms {
display: flex;
margin-top: 50px;
justify-content: center;
align-items: center;
}
</style>
最终呈现的效果如下:
首页的基本样式布局完成之后,接下来开始书写底部页脚的内容,在前面的文章页脚的内容一直没有书写,乘着编写首页样式的时候,将页脚的内容也完成吧,这里我单独将页脚的内容抽离出一个组件,也是采用了svg图标资源,给页脚添点亮色。因为我已经打算再明天就把这个项目写完,所以项目的竣工时间定在明天的 7月9号吧。
<template>
<div class="footer">
<div class="main">
author:
<span style="color: red">亦世凡华、</span>
项目详情:
<a href="https://blog.csdn.net/qq_53123067/category_11818711.html?spm=1001.2014.3001.5482" target="_blank">
点击跳转项目源文章讲解页面
</a>
项目竣工:
<span>2023-7-9</span>
</div>
<div class="svg">
<svg-icon name="bg" width="1400px" height="100px"></svg-icon>
</div>
</div>
</template>
<style lang="scss" scoped>
.footer {
position: relative;
.main {
position: relative; /* 添加定位属性 */
text-align: center;
line-height: $base-tabbar-height;
font-size: 18px;
z-index: 1;
a {
text-decoration: none; /* 去除下划线 */
color: rgb(1, 32, 1); /* 继承父元素颜色,或者自定义链接颜色 */
}
a:hover {
text-decoration: underline; /* 添加下划线 */
color: red; /* 更改链接颜色 */
}
a:active {
color: rgb(33, 11, 161); /* 更改链接颜色 */
}
}
.svg {
position: absolute; /* 修改为绝对定位 */
top: 0; /* 调整位置,使其重叠 */
left: 0; /* 调整位置,使其重叠 */
z-index: 0; /* 修改为较小的层叠顺序 */
}
}
</style>
在后台的index.vue根组件中进行底部页脚组件的引入,如下:
最终呈现的结果如下:
暗黑模式实现
在element-plus组件库中,给我们提供了暗黑模式的功能,如下:
那么在我们的项目中如何实现暗黑模式呢? 我们采用如下的方式:
根据官方文档说明,我们在入口文件处引入该样式:
引入样式之后,接下来需要设置一下switch来进行模式的选择了,这里我采用的是组件库提供的气泡卡片用来放置表单元素,如下:
我们双向绑定响应式数据 dark ,默认值是 false,通过点击switch来进行布尔值的切换,然后通过change事件来给html根标签添加class类名,如下:
// switch开关的change事件进行暗黑模式的切换
const changeDark = () => {
// 获取html根节点
let html = document.documentElement
// 判断HTML标签是否含有类名dark
dark.value ? (html.className = 'dark') : (html.className = '')
}
因为设置的暗黑模式是给没有设置颜色默认是白色的区域更改为黑色,如果区域本身就有背景的话是不能改变其颜色的,结果如下:
如果想实现数据持久化的话,可以采用如下方式进行:
主题颜色的切换
主题颜色的切换也很简单,在element组件库当中,也给与了我们相关的介绍,如下:
更换主题颜色的按钮我们采用的是组件库给我们提供的取色器,如下:
我们将取色器的相关代码也和调整暗黑模式一样,放置在气泡卡片当中,如下:
接下来通过change事件来改变其颜色,这里我们也采用了数据的持久化存储,如下:
最终呈现的效果如下:
数据大屏Echarts展示
接下来通过调用相关的echarts图形库,来实现一个可视化大数据平台的展示,首先我们先搭建好静态页面的展示,我们先在echarts组件中排列好布局,如下:
<template>
<div class="container">
<!-- 数据大屏展示内容区域 -->
<div class="screen" ref="screen">
<div class="top">头部</div>
<div class="bottom">
<div class="left">左侧</div>
<div class="center">中间</div>
<div class="right">右侧</div>
</div>
</div>
</div>
</template>
我们给数据大屏展示内容区域设置一个ref获取该标签的dom,获取dom元素之后通过js代码使其实现响应式布局,数据大屏会随着屏幕大小的改变而改变,如下:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 获取数据大屏展示内容盒子的DOM元素
let screen = ref()
onMounted(() => {
screen.value.style.transform = `scale(${getScale()}) translate(-50%, -50%)`
})
// 定义大屏缩放比例
const getScale = (w = 1920, h = 1080) => {
const ww = window.innerWidth / w
const wh = window.innerHeight / h
return ww < wh ? ww : wh
}
// 监听视口的变化
window.onresize = () => {
screen.value.style.transform = `scale(${getScale()}) translate(-50%, -50%)`
}
</script>
实现完响应式布局之后,接下来开始编写样式使其呈现为1:2:1的排版布局,如下:
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
background: url(./images/bg.png) no-repeat;
background-size: cover;
.screen {
position: fixed;
width: 1920px;
height: 1080px;
left: 50%;
background: red;
top: 50%;
transform-origin: left top;
.top {
width: 100%;
height: 40px;
}
.bottom {
display: flex;
.left {
flex: 1;
}
.center {
flex: 2;
}
.right {
flex: 1;
}
}
}
}
</style>
最终呈现的效果如下:
顶部区域的页面实现:在搭建好基础样式布局之后,接下来开始对顶部的样式进行书写,在这里我们把每个区域的模块样式单独抽离出一个组件,这样便于后期的维护,如下我们先搭建好顶部样式
<template>
<div class="top">
<div class="left">
<span class="lbtn" @click="goHome">首页</span>
<span class="content">游客与旅游景区的静态分析数据</span>
</div>
<div class="center">
<div class="title">智慧旅游可视化大数据平台</div>
</div>
<div class="right">
<span class="rbtn">统计报告</span>
<span class="time">当前时间:2023年07月09日 10:35:31</span>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
// 获取路由器对象
let $router = useRouter()
// 点击首页按钮回到首页
const goHome = () => {
$router.push('/home')
}
</script>
<style lang="scss" scoped>
.top {
width: 100%;
height: 40px;
display: flex;
.left {
flex: 1;
background: url(../../images/dataScreen-header-left-bg.png) no-repeat;
background-size: cover;
text-align: center;
line-height: 40px;
.content {
font-size: 16px;
color: #29fcff;
}
.lbtn {
width: 150px;
height: 40px;
float: right;
background: url(../../images//dataScreen-header-btn-bg-l.png) no-repeat;
background-size: 100% 100%;
text-align: center;
line-height: 40px;
color: #29fcff;
font-size: 20px;
cursor: pointer;
}
.lbtn:hover {
color: rgb(222, 39, 39);
}
}
.center {
flex: 2;
.title {
width: 100%;
height: 74px;
background: url(../../images/dataScreen-header-center-bg.png) no-repeat;
background-size: cover;
text-align: center;
line-height: 74px;
color: #29fcff;
font-size: 30px;
}
}
.right {
flex: 1;
background: url(../../images/dataScreen-header-right-bg.png) no-repeat;
background-size: cover;
display: flex;
justify-content: space-between;
align-items: center;
.rbtn {
width: 150px;
height: 40px;
background: url(../../images/dataScreen-header-btn-bg-r.png) no-repeat;
background-size: 100% 100%;
text-align: center;
line-height: 40px;
color: #29fcff;
}
.time {
color: #29fcff;
margin-right: 40px;
font-size: 16px;
}
}
}
</style>
顶部区域的静态样式搭建完成之后, 接下来我们可以实现让左边的时间实现同步进行,这里我们需要安装一个用来处理时间的第三方库,安装命令如下:
pnpm i moment
执行如下代码获取当前的时间值,并通过间隔1秒的定时器一直调用获取实时动态数据:
import { ref, onMounted, onBeforeUnmount } from 'vue'
import moment from 'moment'
// 存储当前时间
let time = ref(moment().format('YYYY年MM月DD日 hh:mm:ss'))
let timer = ref(0)
// 组件挂载完毕更新当前的时间
onMounted(() => {
timer.value = setInterval(() => {
time.value = moment().format('YYYY年MM月DD日 hh:mm:ss')
}, 1000)
})
// 在组件销毁之前清除定时器
onBeforeUnmount(() => {
clearInterval(timer.value)
})
然后在html代码中引入该time即可,如下:
左侧区域的页面实现:接下来开始实现左侧区域的页面样式内容实现,其具体的排版也是采用flex布局进行上下排布,如下:
左侧区域我单独抽离出三个组件,组件之后的样式和排列在每个组件中单独布局,然后这里需要安装一下 echarts 组件库了,执行命令 pnpm i echarts 进行安装。之后就可以对每个组件的排版和样式布局进行单独设置了,举出其中一个例子,代码如下:
<template>
<div class="box">
<div class="top">
<p class="title">实时游客统计</p>
<p class="bg"></p>
<p class="right">
可预约总量
<span>999999</span>
人
</p>
</div>
<div class="number">
<span v-for="(item, index) in people" :key="index">{{ item }}</span>
</div>
<!-- 将来echarts展示图形图标节点 -->
<div class="charts" ref="charts"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
// 水球图拓展的插件
import 'echarts-liquidfill'
let people = ref<string>('215908人')
// 获取节点】
let charts = ref()
onMounted(() => {
//获取echarts类的实例
let mycharts = echarts.init(charts.value)
//设置实例的配置项
mycharts.setOption({
//标题组件
title: {
text: '水球图',
},
//x|y轴组件
xAxis: {},
yAxis: {},
//系列:决定你展示什么样的图形图标
series: {
type: 'liquidFill', //系列
data: [0.6, 0.4, 0.2], //展示的数据
waveAnimation: true, //动画
animationDuration: 3,
animationDurationUpdate: 0,
radius: '100%', //半径
outline: {
//外层边框颜色设置
show: true,
borderDistance: 8,
itemStyle: {
color: 'skyblue',
borderColor: '#294D99',
borderWidth: 8,
shadowBlur: 20,
shadowColor: 'rgba(0, 0, 0, 0.25)',
},
},
},
//布局组件
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
})
})
</script>
<style lang="scss" scoped>
.box {
background: url(../../images/dataScreen-main-lb.png) no-repeat;
background-size: 100% 100%;
margin-top: 10px;
.top {
margin-left: 20px;
.title {
color: white;
font-size: 20px;
}
.bg {
width: 68px;
height: 7px;
background: url(../../images/dataScreen-title.png);
background-size: 100% 100%;
margin-top: 10px;
}
.right {
float: right;
font-size: 20px;
color: white;
span {
color: yellowgreen;
}
}
}
.number {
padding: 10px;
margin-top: 30px;
display: flex;
span {
flex: 1;
height: 40px;
text-align: center;
line-height: 40px;
background: url(../../images/total.png) no-repeat;
background-size: 100% 100%;
color: #29fcff;
}
}
.charts {
width: 100%;
height: 240px;
}
}
</style>
最终的结果如下:
中间区域的页面实现: 中间区域我们放置了两个组件,上组件放置一个中国地图,下组件放置一个曲线图进行展示,这里也是采用的是echarts提供的图标库进行完成,具体的操作可以参考官方文档进行讲解,这里不再赘述,实现的部分代码如下:
<template>
<div class="box" ref="map"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
//引入中国地图的JSON数据
import chinaJSON from './china.json'
//获取DOM元素
let map = ref()
//注册中国地图
echarts.registerMap('china', chinaJSON as any)
onMounted(() => {
let mychart = echarts.init(map.value)
//设置配置项
mychart.setOption({
//地图组件
geo: {
map: 'china', //中国地图
roam: true, //鼠标缩放的效果
//地图的位置调试
left: 150,
top: 150,
right: 150,
zoom: 1.2,
bottom: 0,
//地图上的文字的设置
label: {
show: true, //文字显示出来
color: 'white',
fontSize: 14,
},
itemStyle: {
//每一个多边形的样式
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'red', // 0% 处的颜色
},
{
offset: 1,
color: 'blue', // 100% 处的颜色
},
],
global: false, // 缺省为 false
},
opacity: 0.8,
},
//地图高亮的效果
emphasis: {
itemStyle: {
color: 'red',
},
label: {
fontSize: 40,
},
},
},
//布局位置
grid: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
series: [
{
type: 'lines', //航线的系列
data: [
{
coords: [
[116.405285, 39.904989], // 起点
[119.306239, 26.075302], // 终点
],
// 统一的样式设置
lineStyle: {
color: 'orange',
width: 5,
},
},
{
coords: [
[116.405285, 39.904989], // 起点
[114.298572, 30.584355], // 终点
],
// 统一的样式设置
lineStyle: {
color: 'yellow',
width: 5,
},
},
],
//开启动画特效
effect: {
show: true,
symbol: 'arrow',
color: 'black',
symbolSize: 10,
},
},
],
})
})
</script>
实现的效果如下:
右侧区域的页面实现: 右侧区域的排版布局和左侧区域一样,也是采用flex布局进行上下排布,然后调用echarts图形库进行展示,举其中一个例子说明:
<template>
<div class="box">
<div class="title">
<p>热门景区排行</p>
<img src="../../images/dataScreen-title.png" />
</div>
<!-- 图形图标的容器 -->
<div class="charts" ref="charts"></div>
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts'
import { ref, onMounted } from 'vue'
//获取DOM节点
let charts = ref()
//组件挂载完毕
onMounted(() => {
//一个容器可以同时展示多种类型的图形图标
let mychart = echarts.init(charts.value)
//设置配置项
mychart.setOption({
//标题组件
title: {
//主标题
text: '景区排行',
link: 'http://www.baidu.com',
//标题的位置
left: '50%',
//主标题文字样式
textStyle: {
color: 'yellowgreen',
fontSize: 20,
},
//子标题
subtext: '各大景区排行',
//子标题的样式
subtextStyle: {
color: 'yellowgreen',
fontSize: 16,
},
},
//x|y轴组件
xAxis: {
type: 'category', //图形图标在x轴均匀分布展示
},
yAxis: {},
//布局组件
grid: {
left: 20,
bottom: 20,
right: 20,
},
//系列:决定显示图形图标是哪一种的
series: [
{
type: 'bar',
data: [10, 20, 30, 40, 50, 60, 70],
//柱状图的:图形上的文本标签,
label: {
show: true,
//文字的位置
position: 'insideTop',
//文字颜色
color: 'yellowgreen',
},
//是否显示背景颜色
showBackground: true,
backgroundStyle: {
//底部背景的颜色
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'black', // 0% 处的颜色
},
{
offset: 1,
color: 'blue', // 100% 处的颜色
},
],
global: false, // 缺省为 false
},
},
//柱条的样式
itemStyle: {
borderRadius: [10, 10, 0, 0],
//柱条颜色
color: function (data: any) {
//给每一个柱条这是背景颜色
let arr = ['red', 'orange', 'yellowgreen', 'green', 'purple', 'hotpink', 'skyblue']
return arr[data.dataIndex]
},
},
},
{
type: 'line',
data: [10, 20, 30, 40, 50, 60, 90],
smooth: true, //平滑曲线
},
{
type: 'bar',
data: [10, 20, 30, 40, 50, 60, 70],
//柱状图的:图形上的文本标签,
label: {
show: true,
//文字的位置
position: 'insideTop',
//文字颜色
color: 'yellowgreen',
},
//是否显示背景颜色
showBackground: true,
backgroundStyle: {
//底部背景的颜色
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'black', // 0% 处的颜色
},
{
offset: 1,
color: 'blue', // 100% 处的颜色
},
],
global: false, // 缺省为 false
},
},
//柱条的样式
itemStyle: {
borderRadius: [10, 10, 0, 0],
//柱条颜色
color: function (data: any) {
//给每一个柱条这是背景颜色
let arr = ['red', 'orange', 'yellowgreen', 'green', 'purple', 'hotpink', 'skyblue']
return arr[data.dataIndex]
},
},
},
],
tooltip: {
backgroundColor: 'rgba(50,50,50,0.7)',
},
})
})
</script>
<style scoped lang="scss">
.box {
width: 100%;
height: 100%;
background: url(../../images/dataScreen-main-cb.png) no-repeat;
background-size: 100% 100%;
margin: 20px 0px;
.title {
margin-left: 5px;
p {
color: white;
font-size: 20px;
}
}
.charts {
height: calc(100% - 30px);
}
}
</style>
最终呈现的结果如下:
数据大屏3D可视化展示
关于3D可视化的话,博主在 Three.js 专栏中讲解的也是非常的清楚了,那么这里进行展示的画面我就采用Three.js专栏中的一篇文章 实现图片转3D效果展示 进行展示吧,具体的方式可以参考我这篇文章的讲解,话不多说直接赋出代码:
<template>
<div class="canvas-container" ref="screenDom"></div>
<div class="content">
<h1>学习更多前端技术</h1>
<h2>
<span style="color: red; margin: 10px">请关注</span>
<a href="https://blog.csdn.net/qq_53123067" target="_blank">
<strong style="font-size: 20px">亦世凡华、</strong>
</a>
</h2>
</div>
</template>
<script setup lang="ts">
import * as THREE from 'three'
import { ref, onMounted } from 'vue'
import bg from './image/bg2.jpg'
import bg_depth from './image/bg2_depth.jpg'
let screenDom = ref<any>()
// 设置场景
const scene = new THREE.Scene()
// 设置相机
const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 0, 5)
// 渲染器
let renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
onMounted(() => {
screenDom.value.appendChild(renderer.domElement)
})
// 监听屏幕大小变化
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight)
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
})
// 加载纹理
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(bg)
const depthTexture = textureLoader.load(bg_depth)
// 创建屏幕
const geomery = new THREE.PlaneGeometry(19.2, 12.8)
// 创建鼠标对象
const mouse = new THREE.Vector2()
// 设置着色器材质
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uTexture: { value: texture },
uDepthTexture: { value: depthTexture },
uMouse: { value: mouse },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D uTexture;
uniform sampler2D uDepthTexture;
uniform vec2 uMouse;
varying vec2 vUv;
uniform float uTime;
void main() {
vec4 color = texture2D(uTexture, vUv);
vec4 depth = texture2D(uDepthTexture, vUv);
float depthValue = depth.r;
float x = vUv.x + (uMouse.x+sin(uTime))*0.01*depthValue;
float y = vUv.y + (uMouse.y+cos(uTime))*0.01*depthValue;
vec4 newColor = texture2D(uTexture, vec2(x, y));
gl_FragColor = newColor;
}
`,
})
const plane = new THREE.Mesh(geomery, material)
scene.add(plane)
// 设置渲染函数
const render = () => {
material.uniforms.uMouse.value = mouse
material.uniforms.uTime.value = performance.now() / 1000
requestAnimationFrame(render)
renderer.render(scene, camera)
}
render()
// 设置鼠标移动监听事件
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
})
</script>
<style lang="scss" scoped>
.canvas-container {
width: 100vw;
height: 100vh;
display: block;
position: fixed;
top: 0;
left: 0;
}
.content {
position: fixed;
top: 40%;
left: 20%;
h1 {
color: rgb(255, 247, 0);
font-size: 40px;
margin-bottom: 30px;
}
a {
text-decoration: none;
color: aqua;
}
a:hover {
color: rgb(151, 76, 9);
}
a:active {
color: rgb(157, 7, 110);
}
}
</style>