前期回顾
Vue3 + Ts + Vite + pnpm 项目中集成 —— eslint 、prettier、stylelint、husky、commitizen_彩色之外的博客-CSDN博客搭建VIte + Ts + Vue3项目并集成eslint 、prettier、stylelint、huskyhttps://blog.csdn.net/m0_57904695/article/details/129950163?spm=1001.2014.3001.5502
目录
👍 适合谁
🎨 资料在哪
🏆 技术栈有哪些
🚀 效果图例
⏰ 配置缩放 【重要】
🚢 自动轮播地图
⌚ 时间
🔱 定位、天气
🎉 谢谢观看 :
👍 适合谁
1 、大学即将毕业 或者 自学前端 缺乏项目经验的
2 、入职以后需要做 Vue 系统的、需要跨过Vue2的直接学习Vue3的
3 、后端开发 没有前端经验 要做 vue + ts + java 项目的
4、 缺乏vue实战项目经验 基础不是很好的 本教程非常的详细 每一步都总结在md文档里面
🎨 资料在哪
文章最末,所有博文相关代码全部都在仓库中 ,注意所有数据均为假数据模拟,如有雷同纯属巧合,除学术研究以外不做任何适用范围,分享的是技术路径、排名无先后
🏆 技术栈有哪些
今天会从零开始搭建一个 Vue3 + Ts + Vite + pnpm 的大屏可视化项目,项目中集成 —— eslint 、prettier、stylelint、husky、commitizen,采用组件化封装思想、hooks工具、更好的符合企业级项目。
🚀 效果图例
⏰ 配置缩放 【重要】
<script setup lang="ts">
/***
* 盒子的原点默认在正中间,我们需要让原点在屏幕的正中间,如果原点不在屏幕正中间放大缩小,有可能会超出屏幕。
* 解决方法:
* 我们使用固定定位让盒子负50%,再把盒子的原点设置为左上角,这样原点就在屏幕的正中间了,
* 屏幕宽度除以设计稿的宽度得到你放大或缩小的比例倍数,具体用宽or高的比例,需要判断,
* 当屏幕宽度大于高度的时候(说明高度变小),我们取高度的比例,反之取宽度的比例。
* 用这个倍数在你页面加载的时候先缩放,再将盒子归位(之前负了5盒子0%)。getScale()方法就是用来计算这个比例的。
*/
import { onMounted, ref } from "vue";
const screenRef = ref();
// 获取缩放比例
function getScale(_w = 1920, _h = 1080) {
const ww = window.innerWidth / _w;
const wh = window.innerHeight / _h;
// ww > wh 什么情况下是true?当屏幕宽度大于高度的时候,我们取高度的比例,反之取宽度的比例
return ww > wh ? wh : ww;
}
// 封装缩放方法
function scale() {
const scale = getScale();
// 先缩放在归位
screenRef.value.style.transform = `scale(${scale}) translate(-50%, -50%)`;
}
onMounted(() => {
scale();
});
window.addEventListener("resize", () => {
// 监听屏幕变化,重新计算缩放比例,并且添加过渡效果
screenRef.value.style.transition = "all 1s linear";
scale();
});
</script>
🚢 自动轮播地图
<template>
<!-- 中国地图 省级 一级页面 -->
<div id="main"></div>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
import jsonData from "@/assets/china.json";
import { onBeforeUnmount, onMounted } from "vue";
import { useRouter } from "vue-router";
let timer: NodeJS.Timeout;
const router = useRouter();
interface DataItem {
ename: string;
name: string;
value?: number; // 这里添加了一个可选属性 value,它表示该地区的值
}
const dataList: DataItem[] = [
{ ename: "beijing", name: "北京" },
{ ename: "tianjin", name: "天津" },
{ ename: "shanghai", name: "上海" },
{ ename: "chongqing", name: "重庆" },
{ ename: "hebei", name: "河北" },
{ ename: "henan", name: "河南" },
{ ename: "yunnan", name: "云南" },
{ ename: "liaoning", name: "辽宁" },
{ ename: "heilongjiang", name: "黑龙江" },
{ ename: "hunan", name: "湖南" },
{ ename: "anhui", name: "安徽" },
{ ename: "shandong", name: "山东" },
{ ename: "xinjiang", name: "新疆" },
{ ename: "jiangsu", name: "江苏" },
{ ename: "zhejiang", name: "浙江" },
{ ename: "jiangxi", name: "江西" },
{ ename: "hubei", name: "湖北" },
{ ename: "guangxi", name: "广西" },
{ ename: "gansu", name: "甘肃" },
{ ename: "jin", name: "山西" },
{ ename: "neimenggu", name: "内蒙古" },
{ ename: "shanxi", name: "陕西" },
{ ename: "jilin", name: "吉林" },
{ ename: "fujian", name: "福建" },
{ ename: "guizhou", name: "贵州" },
{ ename: "guangdong", name: "广东" },
{ ename: "qinghai", name: "青海" },
{ ename: "xizang", name: "西藏" },
{ ename: "sichuan", name: "四川" },
{ ename: "ningxia", name: "宁夏" },
{ ename: "hainan", name: "海南" },
{ ename: "taiwan", name: "台湾" },
{ ename: "xianggang", name: "香港" },
{ ename: "aomen", name: "澳门" },
{ ename: "nanhaizhudao", name: "南海诸岛" },
];
/**
* @method autoHover
* @description 自动高亮并轮播显示 tooltip
* @ParamsDescription seriesIndex 参数为 0,表示第一个系列;
dataIndex 参数则使用 index - 1 来指定上一个数据项的索引,以便取消其高亮状态。
如果不提供 dataIndex 参数,则将取消当前系列的所有数据项的高亮状态。
* @returns void
* @example autoHover(chat)
* @author zk
* @createDate 2023/06/10 17:34:15
* @lastFixDate 2023/06/10 17:34:15
*/
function autoHover(chat: {
dispatchAction: (arg0: {
type: string;
seriesIndex: number;
dataIndex?: number; // 注意这里改为可选属性,因为取消高亮时不需要指定 dataIndex。
}) => void;
}) {
let index = 0;
function startTimer() {
timer = setInterval(function () {
chat.dispatchAction({
type: "downplay",
seriesIndex: 0,
dataIndex: index - 1, // 取消上一个数据项的高亮。
});
chat.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataIndex: index, // 高亮当前数据项。
});
// 显示 tooltip
chat.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: index,
});
index++;
if (index >= dataList.length) {
// 注意这里改为大于等于,否则最后一个数据项无法高亮。
index = 0;
}
}, 2000);
}
// 停止定时器并取消所有高亮
function stopTimer() {
clearInterval(timer);
chat.dispatchAction({
type: "downplay",
seriesIndex: 0,
});
}
startTimer();
const chartElement = document.getElementById("main")!;
chartElement.addEventListener("mouseenter", stopTimer);
chartElement.addEventListener("mouseleave", startTimer);
}
// 页面卸载之前清除定时器
onBeforeUnmount(() => {
clearInterval(timer);
});
onMounted(() => {
const myChart = echarts.init(document.getElementById("main") as HTMLElement);
// 注册中国地图 第一个参数为地图的名字,第二个参数为地图的json数据,第一个要和geo map一样
echarts.registerMap("china", jsonData as never);
// 模拟数据,给dataList添加一个随机的value值
for (let i = 0; i < dataList.length; i++) {
dataList[i].value = Math.floor(Math.random() * 1000 - 1);
}
const option = {
tooltip: {
trigger: "item",
// 背景颜色
backgroundColor: "#1f2a64",
// 边框颜色
borderColor: "#FFFFCC",
// 阴影
shadowColor: "#ffc706",
//文字颜色
textStyle: {
color: "#fff",
},
// 显示延迟,添加显示延迟可以避免频繁切换,
showDelay: 0,
// 隐藏延迟,
hideDelay: 0,
// 是否显示提示框浮层
enterable: true,
// 提示框浮层的移动距离过渡
transitionDuration: 0,
extraCssText: "z-index:100",
// {a}(系列名称),{b}(数据项名称),{c}(数值), {d}(百分比)可以使用标签
formatter: "{b} :<br/>贷款人数:{c}人",
},
visualMap: {
min: 0,
max: 1000,
text: ["高", "低"], //两端的文本
realtime: false,
calculable: true,
itemWidth: 20, //图形的宽度,即长条的宽度。
itemHeight: 90, //图形的高度,即长条的高度。
align: "auto", //指定组件中手柄和文字的摆放位置.可选值为:‘auto’ 自动决定。‘left’ 手柄和label在右。‘right’ 手柄和label在左。‘top’ 手柄和label在下。‘bottom’ 手柄和label在上。
left: "left", //组件离容器左侧的距离,‘left’, ‘center’, ‘right’,‘20%’
top: "60%", //组件离容器上侧的距离,‘top’, ‘middle’, ‘bottom’,‘20%’
right: "auto", //组件离容器右侧的距离,‘20%’
bottom: "auto", //组件离容器下侧的距离,‘20%’
orient: "vertical", //图例排列方向
inRange: {
color: ["#141c48", "#0d3d86"],
},
//设置字体颜色
textStyle: {
color: "#ffffff",
},
// 禁止点击分段型视觉映射组件
selectedMode: false,
},
geo: {
map: "china",
roam: true, //是否开启平游或缩放
zoom: 1.2, //当前视角的缩放比例
emphasis: {
label: {
color: "#000",
fontSize: 14,
},
// 鼠标放上高亮样式
itemStyle: {
areaColor: "#389BB7",
borderWidth: 0,
},
},
label: {
// 通常状态下的样式
show: true,
color: "#fff",
fontSize: 14,
},
// 地图区域的样式设置
itemStyle: {
borderColor: "rgba(147, 235, 248, 1)",
borderWidth: 1,
areaColor: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.8,
colorStops: [
{
offset: 0,
color: "rgba(147, 235, 248, 0)", // 0% 处的颜色
},
{
offset: 1,
color: "rgba(147, 235, 248, .2)", // 100% 处的颜色
},
],
globalCoord: false,
},
},
},
// 鼠标悬浮提示框
series: [
{
name: "省份",
type: "map",
geoIndex: 0,
data: dataList,
},
],
};
//设置配置项
myChart.setOption(option);
// 自动轮播
autoHover(myChart);
// 点击事件地图 enmae为获取省地图的json数据
// router.push({
// path: "/province",
// query: { provinceName: "tianjin", province: "天津" },
// });
myChart.on("click", function (params: any) {
// console.log("😂👨🏾❤️👨🏼==>: ", params.data.ename, params.name); //===>打印后类似 xinjiang 新疆
router.push({
path: "/province",
query: { provinceName: params.data.ename, province: params.name },
});
});
// 缩放适应
window.addEventListener("resize", () => {
myChart.resize();
});
});
</script>
<style scoped lang="scss">
#main {
position: relative;
top: -29%;
width: 100%;
height: 800px;
}
</style>
使用封装:
新建autoEchartsTooltip.ts
interface Option {
time: number;
isLoop: boolean;
}
class TooltipAuto {
// 功能相关的配置项
option: Option = {
time: 3000,
isLoop: true,
};
// 数据索引,要显示那条数据的tooltip
dataIndex = 0;
// 保存时间函数的指针,方便清除
timeTicket: ReturnType<typeof setInterval> | undefined;
// 实例化出来的echart
chart = {};
// echarts的相关配置
chartOptions = {};
dataLength = 0;
seriesIndex = 0;
constructor(chart: any, chartOptions: any, option: Option) {
this.option = option;
this.chart = chart;
this.chartOptions = chartOptions;
}
init() {
this.showTooltipLoop();
this.addMouseEvent();
}
// 展示tooltip
showTooltip() {
const series = this.chartOptions.series;
// 这里简单只处理地图的情况,series中的第一个为地图的配置项
this.dataLength = series[this.seriesIndex].data.length;
// 取消之前高亮的地图
this.downplay();
// 高亮当前图形
this.highlight();
this.showTip();
}
// 显示tooltip
showTip() {
this.chart.dispatchAction({
type: "showTip",
seriesIndex: this.seriesIndex,
dataIndex: this.dataIndex,
});
}
// 隐藏tooltip
hideTip() {
this.chart.dispatchAction({
type: "hideTip",
});
}
// 高亮图形
highlight() {
this.chart.dispatchAction({
type: "highlight",
seriesIndex: this.seriesIndex,
dataIndex: this.dataIndex,
});
}
// 取消高亮
downplay() {
this.chart.dispatchAction({
type: "downplay",
seriesIndex: this.seriesIndex,
dataIndex:
this.dataIndex === 0 ? this.dataLength - 1 : this.dataIndex - 1,
});
}
// 循环展示tooltip
showTooltipLoop() {
this.timeTicket && clearInterval(this.timeTicket);
if (this.option.isLoop) {
this.showTooltip();
this.timeTicket = setInterval(() => {
if (this.dataIndex < this.dataLength - 1) {
this.dataIndex++;
} else {
this.dataIndex = 0;
}
this.showTooltip();
}, this.option.time);
}
}
// 关闭循环展示
closeTooltipLoop() {
if (this.timeTicket) {
clearInterval(this.timeTicket);
this.timeTicket = null;
this.hideTip();
this.chart.dispatchAction({
type: "downplay",
seriesIndex: this.seriesIndex,
dataIndex: this.dataIndex,
});
}
}
// 为地图添加鼠标事件,当鼠标移动时,停止轮播
addMouseEvent() {
this.chart.on("mousemove", this.mouseEventCallback.bind(this));
// 鼠标离开时继续轮播
this.chart.on("globalout", () => {
this.showTooltipLoop();
});
}
mouseEventCallback(param: any) {
if (param.event) {
// 阻止canvas上的鼠标移动事件冒泡
param.event.cancelBubble = true;
}
this.closeTooltipLoop();
}
}
export function initTooltip(chart: any, chartOptions: any, option: Option) {
const tooltip = new TooltipAuto(chart, chartOptions, option);
tooltip.init();
}
使用:
initTooltip(myChart, option, {
time: 3000,
isLoop: true,
});
⌚ 时间
<template>
<div class="nowTime">
<i class="iconfont icon-weibiaoti-"></i>
<p class="date">{{ currentDate }}</p>
<p class="time"> {{ currentTime }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
const currentTime = ref<string>("");
const currentDate = ref<string>("");
const updateTime = () => {
const date = new Date();
currentTime.value = date.toLocaleTimeString();
currentDate.value = `${date.getFullYear()}年${
date.getMonth() + 1
}月${date.getDate()}日 ${
["周日", "周一", "周二", "周三", "周四", "周五", "周六"][date.getDay()]
}`;
};
onMounted(() => {
updateTime();
setInterval(updateTime, 1000);
});
</script>
<style lang="scss" scoped>
@import url("@/assets/iconFont/iconfont.css");
.icon-weibiaoti- {
font-size: 30px;
color: #0585e8;
margin-right: 10px;
}
.nowTime {
position: absolute;
display: flex;
right: 100px;
top: 15px;
font-size: 16px;
.time,
.date {
font-size: 26px;
background-image: linear-gradient(to right, #d38328, #bd5717, #807568);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine 3s ease-in-out infinite;
}
@keyframes shine {
0% {
background-position: 0 0;
}
100% {
background-position: 100px 0;
}
}
.date {
font-size: 20px;
background-image: linear-gradient(to right, #fe6601, #ff953c, #fc741a);
}
}
</style>
🔱 定位、天气
可参考vue项目中嵌入「天气预报」功能 - 掘金
<template>
<!-- 天气 page -->
<div class="weather" v-show="showFlag">
<span>{{ province }} - {{ city }} - {{ weather }}</span>
<i :class="iconUrl"></i>
<span style="font-size: 25px; margin-left: 10px">{{ temperature }} </span
>
<span style="font-size: 16px">℃</span>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { getLocation, getWeather } from "@/api/api";
let showFlag = ref(false);
const province = ref("");
const city = ref("");
const weather = ref("");
const temperature = ref("");
// 获取位置
async function getInfoFn() {
// 如果用户短时间多次进入页面,则不再请求
if (showFlag.value) return;
const { data: res } = await getLocation();
if (res.status === 0) return console.log("获取位置失败");
const abCode = res.adcode;
// 存入时间和code 用于判断是否需要重新请求
localStorage.setItem(
"locationInfo",
JSON.stringify({ time: Date.now(), code: abCode })
);
getWeatherFn(abCode);
}
// 获取天气
async function getWeatherFn(payLoad: string) {
const { data: res } = await getWeather({
city: payLoad,
key: "33f7405fa0049ff120947b37a12567b2",
});
if (res.status === 0) return console.log("获取天气失败");
showFlag.value = true;
province.value = res.lives[0].province;
city.value = res.lives[0].city;
weather.value = res.lives[0].weather;
temperature.value = res.lives[0].temperature;
}
// 定义interface
interface IconMap {
[key: string]: string;
}
// 计算天气图标类型
const iconMap: IconMap = {
晴: "iconfont icon-31qing",
多云: "iconfont icon-qingjianduoyun",
阴: "iconfont icon-yin",
"雷阵雨|阵雨|强阵雨|强雷阵雨": "iconfont icon-leizhenyu",
雷阵雨并伴有冰雹: "iconfont icon-leizhenyubingbanyoubingbao",
"小雨|毛毛雨|细雨|中雨|大雨|冻雨": "iconfont icon-xiaoyu",
"暴雨|大暴雨": "iconfont icon-baoyu",
"特大暴雨|极端降雨": "iconfont icon-n-dabaoyuzhuantedabaoyu",
"雨雪天气|雨夹雪|阵雨夹雪": "iconfont icon-leizhenyu",
"小雪|中雪|大雪|雪": "iconfont icon-xiaoxue",
暴雪: "iconfont icon-baoxue",
阵雪: "iconfont icon-zhenxue",
"扬沙|沙尘暴|强沙尘暴": "iconfont icon-qiangshachenbao",
浮尘: "iconfont icon-fuchen",
霾: "iconfont icon-mai",
"平静|和风|清风": "iconfont icon-youfeng",
"有风|微风": "iconfont icon-feng",
"强风|劲风|疾风|大风|烈风|风暴|狂爆风|飓风|热带风暴":
"iconfont icon-redaifengbao",
龙卷风: "iconfont icon-longjuanfeng",
轻雾: "iconfont icon-wu",
热: "iconfont icon-redu",
冷: "iconfont icon-leng",
"浓雾|大雾": "iconfont icon-tianqi-teqiangnongwu",
};
function getIconUrl(weather: string) {
for (const itemCondition in iconMap) {
const regex = new RegExp(itemCondition);
if (regex.test(weather)) {
return iconMap[itemCondition];
}
}
}
const iconUrl = computed(() => getIconUrl(weather.value));
// 测试
// const iconUrl = computed(() => getIconUrl("阵雪"));
onMounted(() => {
// 封装优化请求,请求的时候存本地且不过期,就使用本地数据,否则重新请求,
//在页面加载调用一下这个函数,不去直接调用getType请求接口
// 判断本地有没有数据
const cates = localStorage.getItem("locationInfo") || "";
// console.log(cates);
if (cates) {
const { time, code } = JSON.parse(cates);
// 判断是否过期
if (Date.now() - time > 1000 * 600) {
getInfoFn();
} else {
getWeatherFn(code);
console.log("使用本地数据");
}
}
return getInfoFn();
});
</script>
<style scoped lang="scss">
@import url("@/assets/iconFont/iconfont.css");
.weather {
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
left: 100px;
top: 15px;
font-size: 22px;
background-image: linear-gradient(to right, #fe6601, #ff953c, #fc741a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine 3s ease-in-out infinite;
.iconfont {
color: #fff !important;
margin-left: 10px;
font-size: 30px;
}
@keyframes shine {
0% {
background-position: 0 0;
}
100% {
background-position: 280px 0;
}
}
img {
width: 40px;
height: 40px;
margin: 0 10px;
border-radius: 50%;
}
}
</style>
🎉 谢谢观看 :
从零配置完整企业级Vue3 + Ts + Vite 项目,集成(路由、pinia、组件、hooks、全局钩子、全局规约、等......)Vue3 + Ts + Vite + pnpm 项目中集成 —— eslint 、prettier、stylelint、husky、commitizen_彩色之外的博客-CSDN博客搭建VIte + Ts + Vue3项目并集成eslint 、prettier、stylelint、huskyhttps://blog.csdn.net/m0_57904695/article/details/129950163?spm=1001.2014.3001.5502
_______________________________ 期待再见 _______________________________