Vue3 + TS + Vite 项目实战 —— 大屏可视化

9a69fede8b2044a79dd834e3e48f20b4.png前期回顾f8e3cc1a0f694ac2b665ca2ad14c49d7.png  

目录

👍 适合谁

🎨 资料在哪

🏆 技术栈有哪些 

 🚀 效果图例

⏰ 配置缩放 【重要】

🚢 自动轮播地图

⌚ 时间

🔱 定位、天气

地址:数据安全大屏 -- 项目: 🔥🌴🌴 该系统会从多个数据源(如日志、网络设备、云服务等)中采集、整合、处理数据,并将其转化为易于理解和分析的可视化图表、地图等形式,以帮助用户快速识别、监测和预警潜在的安全风险和威胁。通过数据安全大屏,用户可以深入了解各种系统和网络设备的运行状态、流量情况、攻击来源等信息,及时发现并应对异常行为、漏洞、恶意攻击等问题,提高数据安全保障能力。https://gitee.com/zhang-kun8888/data-security-big-screen

🎉   谢谢观看 :


👍 适合谁

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 class="container-wrapper">
		<div ref="screenRef" class="screen">大屏</div>
	</div>
</template>

<script setup lang="ts" name="home">
import { onMounted, ref } from 'vue';
const screenRef = ref<HTMLDivElement | null>();

/*
 * 计算缩放比例
 * @param _w 设计稿宽度
 * @param _h 设计稿高度
 */
function getScale(_w = 1920, _h = 1080): number {
	const ww = window.innerWidth / _w;
	const wh = window.innerHeight / _h;
	// ww > wh 什么情况下是true?当屏幕宽度大于高度的时候,我们取高度的比例,反之取宽度的比例
	return ww > wh ? wh : ww;
}
/*
 * 缩放屏幕
 * 注意:如果你的设计稿不是1920*1080,你需要传入你的设计稿的宽度和高度,否则会出现黑边
 * 先缩放,再平移到屏幕中心
 */
function scale(w = 1920, h = 1080): void {
	const scale = getScale(w, h);
	if (screenRef.value) {
		screenRef.value.style.transform = `scale( ${scale}) translate(-50%, -50%)`;
		screenRef.value.style.transition = 'all 1s linear';
	}
}

onMounted(() => {
	/**
	 * 目前.screen的宽度是1920,高度是1080,
	 * @method scale
	 * @description 如果你的设计稿不是这个尺寸,你需要传入你的设计稿的宽度和高度并修改.screen样式宽高
	 * @example scale(2560, 1440);
	 */
	scale();
});

window.addEventListener('resize', () => scale());
</script>

<style scoped lang="scss">
.container-wrapper {
	width: 1920px;
	height: 1080px;
}

.screen {
	position: fixed;
	overflow: hidden;
	top: 50%;
	left: 50%;
	width: 100%;
	height: 100%;
	transform-origin: left top;
	background-color: pink;
}
</style>

🚢 自动轮播地图

<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">&nbsp;{{ 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
    >&nbsp;
    <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>

地址:数据安全大屏 -- 项目: 🔥🌴🌴 该系统会从多个数据源(如日志、网络设备、云服务等)中采集、整合、处理数据,并将其转化为易于理解和分析的可视化图表、地图等形式,以帮助用户快速识别、监测和预警潜在的安全风险和威胁。通过数据安全大屏,用户可以深入了解各种系统和网络设备的运行状态、流量情况、攻击来源等信息,及时发现并应对异常行为、漏洞、恶意攻击等问题,提高数据安全保障能力。icon-default.png?t=N7T8https://gitee.com/zhang-kun8888/data-security-big-screen

🎉   谢谢观看 :

从零配置完整企业级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

7730e2bd39d64179909767e1967da702.jpeg

 _______________________________  期待再见  _______________________________ 

  • 13
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彩色之外

你的打赏是我创作的氮气加速动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值