宁德大屏第二版总结

碰到难点 

1.wss 心跳机制

实现前端和后端双向绑定 只要后端发送了消息 前端通过全局总线去触发你想要的函数。

全局总线

vue3可以全局总线下一个mitt

新建一个eventBus.js

import mitt from "mitt";
const eventBus = mitt();

export default eventBus;

然后wss新建一个useWebSocket.js

import { ref } from "vue";
import eventBus from "../mixins/eventBus";

// 连接状态
export const SocketStatus = {
  Connecting: "正在连接...", // 表示正在连接,这是初始状态。
  Connected: "连接已建立", // 表示连接已经建立。
  Disconnecting: "连接正在关闭", // 表示连接正在关闭。
  Disconnected: "连接已断开", // 表示连接已经关闭。
};

const DEFAULT_OPTIONS = {
  url: "", // WebSocket URL
  heartBeatData: "", // 心跳数据
  heartBeatInterval: 60 * 1000, // 心跳间隔,单位 ms
  reconnectInterval: 5 * 1000, // 断线重连间隔,单位 ms
  maxReconnectAttempts: 10, // 最大重连次数
};

const SocketCloseCode = 1000;

export default function useWebSocket(options = {}, onMessageCallback) {
  //onMessageCallback 处理回调函数 确保在收到消息时候调用
  const state = ref({
    options: { ...DEFAULT_OPTIONS, ...options },
    socket: null,
    heartBeatSendTimer: null, // 心跳发送定时器
    heartBeatTimeoutTimer: null, // 心跳超时定时器
    reconnectAttempts: 0,
    reconnectTimeout: null,
  });

  const status = ref(SocketStatus.Disconnected);

  // 连接 WebSocket
  const connect = () => {
    disconnect(); // 断开之前的连接

    status.value = SocketStatus.Connecting;

    state.value.socket = new WebSocket(state.value.options.url);

    state.value.socket.onopen = (openEvent) => {
      console.log("socket连接:", openEvent);
      status.value = SocketStatus.Connected;
      startHeartBeat(); // 开始心跳
    };

    state.value.socket.onmessage = (msgEvent) => {
      console.log("socket消息:", msgEvent);
      if (typeof onMessageCallback === "function") {
        // onMessageCallback(); // 调用传入的回调函数
        // 广播消息
        if (msgEvent.data == "ping") {
          console.log("服务端活着,还未收到数据");
        } else {
          eventBus.emit("socketMessage", msgEvent.data);
          eventBus.emit("Messageaa");
          eventBus.emit("Messagebb");
        }
      } else {
        console.error("getDate is not a function");
      }
      // if (typeof getDate === "function") {
      //   getDate(); // 调用 getDate 函数
      // } else {
      //   console.error("getDate is not a function");
      // }

      startHeartBeat(); // 收到消息时重新开始心跳
    };

    state.value.socket.onclose = (closeEvent) => {
      console.log("socket关闭:", closeEvent);
      status.value = SocketStatus.Disconnected;
      // 非正常关闭,尝试重连
      if (closeEvent.code !== SocketCloseCode) {
        reconnect();
      }
    };

    state.value.socket.onerror = (errEvent) => {
      console.log("socket报错:", errEvent);
      status.value = SocketStatus.Disconnected;
      reconnect(); // 连接失败,尝试重连
    };
  };

  // 断开 WebSocket
  const disconnect = () => {
    // 如果 WebSocket 实例存在且处于开放或连接中的状态,则关闭连接。
    if (state.value.socket && (state.value.socket.OPEN || state.value.socket.CONNECTING)) {
      console.log("socket断开连接");
      status.value = SocketStatus.Disconnecting;
      state.value.socket.close(SocketCloseCode, "normal closure");
      state.value.socket = null;
      stopHeartBeat(); // 停止心跳
      stopReconnect(); // 停止重连
    }
  };

  // 开始心跳检测
  const startHeartBeat = () => {
    stopHeartBeat(); // 先清除之前的定时器

    state.value.heartBeatSendTimer = setTimeout(() => {
      if (status.value === SocketStatus.Connected) {
        state.value.socket.send(state.value.options.heartBeatData);
        console.log("socket心跳发送:", state.value.options.heartBeatData);
      }

      // 心跳超时
      state.value.heartBeatTimeoutTimer = setTimeout(() => {
        console.log("心跳超时,关闭连接");
        state.value.socket.close(4444, "heart timeout");
      }, state.value.options.heartBeatInterval);
    }, state.value.options.heartBeatInterval);
  };

  // 停止心跳检测
  const stopHeartBeat = () => {
    if (state.value.heartBeatSendTimer) {
      clearTimeout(state.value.heartBeatSendTimer);
      state.value.heartBeatSendTimer = null;
    }
    if (state.value.heartBeatTimeoutTimer) {
      clearTimeout(state.value.heartBeatTimeoutTimer);
      state.value.heartBeatTimeoutTimer = null;
    }
  };

  // 重连机制
  const reconnect = () => {
    // reconnect:如果连接状态不是 Connected 或 Connecting,并且重连尝试次数小于最大值,则尝试重连。
    if (status.value === SocketStatus.Connected || status.value === SocketStatus.Connecting) {
      return;
    }

    stopHeartBeat(); // 停止心跳

    if (state.value.reconnectAttempts < state.value.options.maxReconnectAttempts) {
      console.log("socket重连:", state.value.reconnectAttempts);

      // 重连间隔,5秒起步,下次递增1秒
      const interval = Math.max(state.value.options.reconnectInterval, state.value.reconnectAttempts * 1000);
      console.log("间隔时间:", interval);

      state.value.reconnectTimeout = setTimeout(() => {
        if (status.value !== SocketStatus.Connected && status.value !== SocketStatus.Connecting) {
          connect();
        }
      }, interval);

      state.value.reconnectAttempts += 1;
    } else {
      status.value = SocketStatus.Disconnected;
      stopReconnect(); // 停止重连
    }
  };

  // 停止重连
  const stopReconnect = () => {
    if (state.value.reconnectTimeout) {
      clearTimeout(state.value.reconnectTimeout);
      state.value.reconnectTimeout = null;
    }
  };

  return {
    connect,
    disconnect,
    status,
  };
}

重点是这步  这边要和服务端确认 这边需要服务端收到我们的消息同时他也要做个回应 因为服务端有时候很长不发消息 wss就挂了

  state.value.socket.onmessage = (msgEvent) => {
      console.log("socket消息:", msgEvent);
      if (typeof onMessageCallback === "function") {
        // onMessageCallback(); // 调用传入的回调函数
        //这边要和服务端确认 这边需要服务端收到我们的消息同时他也要做个回应 因为服务端有时候很长不发消息 wss就挂了
        if (msgEvent.data == "ping") {
          console.log("服务端活着,还未收到数据");
        } else {
          eventBus.emit("socketMessage", msgEvent.data);
          eventBus.emit("Messageaa");
          eventBus.emit("Messagebb");
        }
      } else {
        console.error("getDate is not a function");
      }
      // if (typeof getDate === "function") {
      //   getDate(); // 调用 getDate 函数
      // } else {
      //   console.error("getDate is not a function");
      // }

      startHeartBeat(); // 收到消息时重新开始心跳
    };

然后在自己的组件 要是3分钟服务器挂了 你就循环一次问他联不联 不然 他挂了 你也挂了 没有人手动去联的

import useWebSocket, { SocketStatus } from "@/mixins/useWebSocket";
import eventBus from "@/mixins/eventBus";

onMounted(() => {
  form.startDate = date.value[0];
  form.endDate = date.value[1];
  eventBus.on("tenDays", getDate);
  eventBus.on("socketMessage", getDate); // 监听 WebSocket 消息事件
  getDate(); // 在组件初始化时调用 getDate
  getUser(); //为了链接wss
  console.log("status1111111111.value", status.value);

  interdate = setInterval(() => {
    getDate2();
    console.log("status.value", status.value);
    if (status.value === SocketStatus.Disconnected) {
      console.log("wss挂了,三分钟一次自动重联");
      connect();
    } else {
      console.log("wss本身就联着,不需要重联");
    }
  }, 1000 * 60 * 3);
});
onUnmounted(() => {
  eventBus.off("tenDays", getDate);
  eventBus.off("socketMessage", getDate);
  clearInterval(interdate);
  disconnect(); // 断开 WebSocket 连接
});

 我传函数进去了其实不用的 懒得改了 因为 我要接收到数据 好几个函数一起被触发 所以全局总线比较好

const ID = ref("");
const getUser = () => {
  userInfo().then((res) => {
    // console.log("用户res", res);
    localStorage.setItem("userInof", JSON.stringify(res.data.sysUser));
    ID.value = res.data.sysUser.id;
    if (ID.value) {
      console.log("连接WebSocket");
      // setLoginCookie(); // 设置登录 Cookie(包括 token)
      connect(); // 连接 WebSocket
    } // 组件挂载时连接 WebSocket
  });
};

const { connect, disconnect, status } = useWebSocket(
  {
    url: computed(() => {
      // const token = Cookie.get("Authorization") || "";
      return ID.value
        ? `wss://www.tbaowl.com:9992/ws/mini/websocket/${ID.value}`
        : "";
    }), // 替换为实际的 WebSocket URL
    heartBeatData: "ping", // 心跳数据
    heartBeatInterval: 30000, // 心跳间隔,30秒
    reconnectInterval: 5000, // 重连间隔,5秒
    maxReconnectAttempts: 5, // 最大重连次数
  },
  getDate
);

2.关于弹窗红色预警,逻辑。

<template>
  <transition-group name="scroll" tag="div" class="warmTanChuan-container">
    <div
      v-if="isRunning && currentItem"
      class="warmTanChuan"
      ref="warmTanChuan"
      :key="currentItem.id"
    >
      <div class="title">
        <img src="@/assets/images/Frame103(91).png" alt="" />
        <span v-if="currentItem.deviceType == 1">摄像头警告:</span>
        <span v-if="currentItem.deviceType == 2">灵思传感器警告:</span>
        <span v-if="currentItem.deviceType == 3">大华电气设备警告:</span>
        <span v-if="currentItem.deviceType == 4">消防设备警告:</span>
        <span v-if="currentItem.deviceType == 5">车载设备警告:</span>
        <span v-if="currentItem.deviceType == 6">海康消防设备警告:</span>
        <span v-if="currentItem.deviceType == 7">消防传感器警告:</span>
        <span class="time">{{ time }}s</span>
      </div>
      <div class="content_text">
        <p>{{ currentItem.incidentDescribe }}</p>
      </div>
    </div>
  </transition-group>
  <div class="test" v-if="isTestVisible && warmList.length > 0"></div>
</template>

<script setup>
import { ref, onMounted, watch, computed, onUnmounted, nextTick } from "vue";
import { warmEvents } from "@/api/api.js";
import dayjs from "dayjs";
import useWebSocket, { SocketStatus } from "@/mixins/useWebSocket";
import { getSystemData, userInfo } from "@/api/api";
import eventBus from "@/mixins/eventBus";
const warmList = ref([]);
const queue = ref([]); // 用于存储接收到的警告数据的队列

const currentIndex = ref(0);
const time = ref(30);
const isRunning = ref(false);
const isTestVisible = ref(true);

let audio = new Audio(require("@/assets/warm.mp3"));
audio.hidden = true; // 隐藏音频控件
document.body.appendChild(audio); // 将音频控件添加到页面中

let interval;
onMounted(() => {
  eventBus.on("socketMessage", handleMessage);
  audio.addEventListener("ended", handleAudioEnded);
});

onUnmounted(() => {
  eventBus.off("socketMessage", handleMessage);
  clearInterval(interval); // 清除1分钟的定时器
  clearInterval(countDown); // 清除倒计时定时器
  audio.pause();
  document.body.removeChild(audio); // 移除音频控件
});

const getDate = () => {
  const time = new Date();
  const endTime = dayjs(time).format("YYYY-MM-DD HH:mm:ss");
  const startTime = dayjs(time)
    .subtract(1, "days")
    .format("YYYY-MM-DD HH:mm:ss");
  const equipWarnPageDTO = {
    startTime: startTime,
    endTime: endTime,
  };
  const page = {
    size: 999,
  };
  const obj = Object.assign(equipWarnPageDTO, page);
  warmEvents(obj).then((res) => {
    if (res.code == 0) {
      const records = res.data?.records ?? [];
      warmList.value = [...warmList.value, ...records]; // 将新数据添加到 warmList 末尾
      nextTick(() => {
        if (warmList.value.length > 0) {
          playAudio();
        }
      });
    }
  });
};

const handleMessage = (msg) => {
  console.log("msg11111", msg);
  const data = JSON.parse(msg);
  queue.value.push(data); // 将新数据添加到队列中
  console.log("isRunning.value", isRunning.value);
  console.log("currentItem.value", currentItem.value);

  if (!isRunning.value) {
    // if (!isRunning.value && !currentItem.value) {
    // 如果当前没有正在播放的警告信息,开始播放
    console.log("播不播啊");
    showNextItem();
  }
};

const ID = ref("");
const getUser = () => {
  userInfo().then((res) => {
    localStorage.setItem("userInof", JSON.stringify(res.data.sysUser));
    ID.value = res.data.sysUser.id;
    if (ID.value) {
      connect(); // 连接 WebSocket
    }
  });
};

const { connect, disconnect, status } = useWebSocket(
  {
    url: computed(() => {
      return ID.value
        ? `wss://www.tbaowl.com:9992/ws/mini/websocket/${ID.value}`
        : "";
    }),
    heartBeatData: "ping", // 心跳数据
    heartBeatInterval: 60000, // 心跳间隔,30秒
    reconnectInterval: 5000, // 重连间隔,5秒
    maxReconnectAttempts: 5, // 最大重连次数
  },
  handleMessage
);

let countDown;
const startCountDown = () => {
  if (countDown) {
    clearInterval(countDown); // 清除之前的倒计时定时器
  }
  countDown = setInterval(() => {
    if (isRunning.value) {
      time.value -= 1;
      if (time.value <= 0) {
        clearInterval(countDown);
        currentItem.value = null;
        handleAudioEnded();
      }
    }
  }, 1000);
};

const showNextItem = () => {
  if (queue.value.length > 0) {
    const nextItem = queue.value.shift();
    console.log("queue.value:", nextItem);
    warmList.value.push(nextItem);
    currentIndex.value = warmList.value.length - 1; // 更新 currentIndex
    console.log("warmList.value2222", warmList.value);
    isRunning.value = true; // 标记为正在播放
    isTestVisible.value = true;
    time.value = 30; // 重置时间
    playAudio();
    startCountDown(); // 开始倒计时
  } else {
    isRunning.value = false; // 如果队列为空,停止播放
    // currentItem.value = null;
    isTestVisible.value = false;
  }
};

watch(currentIndex, () => {
  if (isRunning.value) {
    if (countDown) {
      clearInterval(countDown);
      startCountDown();
    }
  }
});

const currentItem = computed(() => {
  return warmList.value.length > 0 ? warmList.value[currentIndex.value] : null;
});

const playAudio = () => {
  audio.currentTime = 0;
  audio.play().catch((error) => {
    console.error("Error playing audio:", error);
  });
  // 设置定时器,10秒后停止音频
  setTimeout(() => {
    audio.pause();
  }, 10000); // 10000毫秒 = 10秒
};
const handleAudioEnded = () => {
  if (queue.value.length > 0) {
    // currentIndex.value++;
    showNextItem();
  } else {
    isRunning.value = false; // 没有更多数据,停止播放
    isTestVisible.value = false;
    // currentItem.value = null;
    audio.pause();
    document.body.removeChild(audio); // 移除音频控件
  }
};

// const handleAudioEnded = () => {
//   if (queue.value.length > 0) {
//     // 如果队列中还有未播放的数据,播放下一条
//     showNextItem();
//   } else {
//     // 如果没有更多数据,停止播放
//     isRunning.value = false;
//     audio.pause();
//     document.body.removeChild(audio); // 移除音频控件
//   }
// };
</script>

<style lang="scss" scoped>
.warmTanChuan-container {
  position: absolute;
  top: 25%;
  left: 35%;
  transform: translate(-50%, -50%);
  z-index: 999;
}

.warmTanChuan {
  width: 327px;
  color: #ffffff;
  .title {
    height: 40px;
    line-height: 40px;
    background: url("@/assets/images/jbbg.png") no-repeat;
    background-size: 100% 100%;
    font-weight: bold;
    padding: 0 10px;
    font-size: 14px;
    img {
      width: 20px;
      height: 20px;
      margin: 10px;
    }
    .time {
      float: right;
    }
  }
  .content_text {
    background-color: #3a0e0b;
    border: 2px solid #be4b44;
    padding: 10px;
    font-size: 14px;
    position: relative;
  }
}

.scroll-enter-active,
.scroll-leave-active {
  transition: transform 1s;
}
.scroll-enter {
  transform: translateY(100%);
}
.scroll-leave-to {
  transform: translateY(-100%);
}
.test {
  width: 972px;
  height: calc(100vh - 440px);
  background: url("@/assets/images/image2/warm.png") no-repeat;
  background-size: 100%;
  position: absolute;
  top: 96px;
  left: 50%;
  transform: translateX(-50%);
  animation: blink 1s infinite; // 添加闪烁动画
}
// 闪烁动画
@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}
</style>

难点:

1.一个是关于弹窗如何控制30s显示然后下一个显示,同时伴有警告声音10s消失?

2.数据如果是一个一个传给你或者是一次性多个传给你,怎么办?

3.闪烁动画怎么做?

先处理第三个问题:首先闪烁动画是一个比较图片的盒子,实现一闪一闪的效果

如下

 样式可以这样写

 

 闪烁由数据的长度和isTestVisible共同决定

 <div class="test" v-if="isTestVisible && warmList.length > 0"></div>
.test {
  width: 972px;
  height: calc(100vh - 440px);
  background: url("@/assets/images/image2/warm.png") no-repeat;
  background-size: 100%;
  position: absolute;
  top: 96px;
  left: 50%;
  transform: translateX(-50%);
  animation: blink 1s infinite; // 添加闪烁动画
}
// 闪烁动画
@keyframes blink {
  0%,            // 动画开始时
  100% {         // 动画结束时
    opacity: 1;  // 元素完全可见 (不透明)
  }
  50% {          // 动画进行到一半时
    opacity: 0;  // 元素完全不可见 (透明)
  }
}

 好 现在解决第一个问题。如何让他实现关于弹窗如何控制30s显示然后下一个显示,同时伴有警告声音10s消失。这边就会说明刚才isTestVisible是什么东西了。

首先还是从样式transition-group 来处理这些警告信息的进入和离开动画。

<template>
  <transition-group name="scroll" tag="div" class="warmTanChuan-container">
    <div
      v-if="isRunning && currentItem"
      class="warmTanChuan"
      ref="warmTanChuan"
      :key="currentItem.id"
    >
      <div class="title">
        <img src="@/assets/images/Frame103(91).png" alt="" />
        <span v-if="currentItem.deviceType == 1">摄像头警告:</span>
        <span v-if="currentItem.deviceType == 2">灵思传感器警告:</span>
        <span v-if="currentItem.deviceType == 3">大华电气设备警告:</span>
        <span v-if="currentItem.deviceType == 4">消防设备警告:</span>
        <span v-if="currentItem.deviceType == 5">车载设备警告:</span>
        <span v-if="currentItem.deviceType == 6">海康消防设备警告:</span>
        <span v-if="currentItem.deviceType == 7">消防传感器警告:</span>
        <span class="time">{{ time }}s</span>
      </div>
      <div class="content_text">
        <p>{{ currentItem.incidentDescribe }}</p>
      </div>
    </div>
  </transition-group>
  <div class="test" v-if="isTestVisible && warmList.length > 0"></div>
</template>
  1. transition-group:

    • 名称为 scroll 的 transition-group 组件,用于处理列表项的动画。
    • tag="div" 设置容器元素为 div
    • 内部包含一个动态渲染的 div 元素,用于显示当前警告信息。
  2. div:

    • 根据 isRunning 和 currentItem 的值来决定是否显示警告信息。
    • 使用 :key 绑定唯一的标识符,以便 transition-group 能够正确跟踪元素的变化。
  3. .warmTanChuan-container:

    • 定位样式,使警告信息居中显示。
  4. .test:

    • 一个用于测试的 div 元素,当 isTestVisible 为真时显示,带有闪烁动画。

 第一步肯定是拿到数据 如果没有正在播放的数据才可以播

import { ref, onMounted, watch, computed, onUnmounted, nextTick } from "vue";
import { warmEvents } from "@/api/api.js";
import dayjs from "dayjs";
import useWebSocket, { SocketStatus } from "@/mixins/useWebSocket";
import { getSystemData, userInfo } from "@/api/api";
import eventBus from "@/mixins/eventBus";
const warmList = ref([]);
const queue = ref([]); // 用于存储接收到的警告数据的队列

const currentIndex = ref(0);
const time = ref(30);
const isRunning = ref(false);
const isTestVisible = ref(true);

let audio = new Audio(require("@/assets/warm.mp3"));
audio.hidden = true; // 隐藏音频控件
document.body.appendChild(audio); // 将音频控件添加到页面中

const currentItem = computed(() => {
  return warmList.value.length > 0 ? warmList.value[currentIndex.value] : null;
});



const handleMessage = (msg) => {
  const data = JSON.parse(msg);
  queue.value.push(data); // 将新数据添加到队列中

  if (!isRunning.value ) {
    // 如果当前没有正在播放的警告信息,开始播放
    showNextItem();
  }
};

下一条给warnList添加数据 同时queue移除

const showNextItem = () => {
   if (queue.value.length > 0) {
    const nextItem = queue.value.shift();
    console.log("queue.value:", nextItem);
    warmList.value.push(nextItem);
    currentIndex.value = warmList.value.length - 1; // 更新 currentIndex
    console.log("warmList.value2222", warmList.value);
    isRunning.value = true; // 标记为正在播放
    isTestVisible.value = true;
    time.value = 30; // 重置时间
    playAudio();
    startCountDown(); // 开始倒计时
  } else {
    isRunning.value = false; // 如果队列为空,停止播放
    // currentItem.value = null;
    isTestVisible.value = false;
  }
};

 播放声音

const playAudio = () => {
  audio.currentTime = 0;
  audio.play().catch((error) => {
    console.error("Error playing audio:", error);
  });
  // 设置定时器,10秒后停止音频
  setTimeout(() => {
    audio.pause();
  }, 10000); // 10000毫秒 = 10秒
};

 开始倒计时

const startCountDown = () => {
  if (countDown) {
    clearInterval(countDown); // 清除之前的倒计时定时器
  }
  countDown = setInterval(() => {
    if (isRunning.value) {
      time.value -= 1;
      if (time.value <= 0) {
        clearInterval(countDown);
        handleAudioEnded();
      }
    }
  }, 1000);
};

const handleAudioEnded = () => {
  if (queue.value.length > 0) {
    showNextItem();
  } else {
    isRunning.value = false; // 没有更多数据,停止播放
    isTestVisible.value = false;
    audio.pause();
    document.body.removeChild(audio); // 移除音频控件
  }
};

watch(currentIndex, () => {
  if (isRunning.value) {
    if (countDown) {
      clearInterval(countDown);
      startCountDown();
    }
  }
});

 因为warmList肯定是有数据的所以再加一个条件isTestVisible来控制闪烁动画。

3.地图部分的数据筛选。

效果如图

代码:

<template>
  <div class="DataSelectModal" ref="DataSelectModal" v-if="isModalVisible">
    <div class="title">
      预警数据筛选
      <img
        src="@/assets/images/Frame103(37).png"
        alt=""
        @click="close"
        class="closeModals"
      />
    </div>
    <div class="warmSelectContent">
      <div class="slect">
        常用:
        <!-- <div class="ofenUse" @click="getTime()">总计</div>
        <div class="ofenUse" @click="getTime(1)">过去24小时</div>
        <div class="ofenUse" @click="getTime(7)">过去7天</div>
        <div class="ofenUse" @click="getTime(30)">过去30天</div>
        <div class="ofenUse" @click="getTime(90)">过去90天</div>
        <div class="ofenUse" @click="getTime(180)">过去180天</div>
        <div class="ofenUse" @click="getTime(365)">过去365天</div> -->
        <div
          v-for="item in buttonData"
          :key="item.id"
          :class="item.id === selectedButton ? 'selected' : 'ofenUse'"
          @click="handleButtonClick(item)"
        >
          {{ item.name }}
        </div>
      </div>
      <div class="slect">
        条件筛选:
        <a-select v-model="selectedYear" :key="resetKey" @change="handleChange">
          <a-select-option v-for="year in yearArr" :key="year" :value="year">
            {{ year }} 年
          </a-select-option>
        </a-select>

        <a-select
          v-model="selectedSeason"
          @change="handleChange2"
          :key="resetKey"
          :disabled="!selectedYear"
        >
          <a-select-option
            v-for="season in seasonArr"
            :key="season.value"
            :value="season.value"
          >
            {{ season.name }}
          </a-select-option>
        </a-select>

        <a-select
          v-model="selectedMonth"
          @change="handleChange3"
          :key="selectedSeason + selectedYear"
        >
          <a-select-option
            v-for="(month, index) in monthArr"
            :key="month"
            :value="index"
            :disabled="!selectedYear || !selectedSeason"
          >
            {{ month }}月
          </a-select-option>
        </a-select>
      </div>
      <div class="slect">
        时间筛选:
        <a-range-picker
          v-model:value="datea"
          separator="至"
          valueFormat="YYYY-MM-DD"
          @change="dataCheck"
          placeholder=""
        >
          <template #suffixIcon>
            <down-outlined />
          </template>
        </a-range-picker>
      </div>
    </div>
    <div class="immediately" @click="search">立即查询</div>
  </div>
</template>
    
<script setup>
import { ref, watch, onMounted, computed, reactive, nextTick } from "vue";
import { DownOutlined } from "@ant-design/icons-vue";
const emit = defineEmits(["update:start-time", "update:end-time"]);
import dayjs from "dayjs";
// const emit = defineEmits(["close"]);
const DataSelectModal = ref(null);
const isModalVisible = ref(false);
const form = reactive({
  startTime: "",
  endDate: "",
});
const time = new Date().getFullYear();
const yearArr = ref([]);
const startTime = ref("");
const endTime = ref("");
const selectedMonth = ref("");
const selectedSeason = ref("");
const selectedYear = ref("");

import eventBus from "@/mixins/eventBus";
import { message } from "ant-design-vue";

const seasonArr = [
  {
    value: 1,
    name: "第一季度",
  },
  {
    value: 2,
    name: "第二季度",
  },
  {
    value: 3,
    name: "第三季度",
  },
  {
    value: 4,
    name: "第四季度",
  },
];
const monthArr = ref([]);

const initializeYears = () => {
  var i;
  for (i = 2024; i <= time; i++) {
    yearArr.value.push(i);
  }
};
const datea = ref([]);

const buttonData = [
  { id: 1, name: "总计", time: 0 },
  { id: 2, name: "过去24小时", time: 1 },
  { id: 3, name: "过去7天", time: 7 },
  { id: 4, name: "过去30天", time: 30 },
  { id: 5, name: "过去90天", time: 90 },
  { id: 6, name: "过去180天", time: 180 },
  { id: 7, name: "过去365天", time: 365 },
];
// 当前选中的按钮ID
const selectedButton = ref(0);
const resetKey = ref(0);
const changeButton = ref("");

onMounted(() => {
  nextTick(() => {
    // selectedButton.value = 1;
  });
  initializeYears();

  // getTime();
});
// 处理按钮点击事件
const handleButtonClick = (date) => {
  selectedYear.value = null;
  selectedSeason.value = null;
  selectedMonth.value = null;
  datea.value = [];
  selectedButton.value = date.id;
  resetKey.value++; // 触发组件重新渲染
  // const resetKey = selectedYear.value + "_" + selectedSeason.value;
  nextTick(() => {
    console.log(" selectedYear.value", selectedYear.value);
    console.log(" selectedSeason.value", selectedSeason.value);
    console.log(" selectedMonth.value", selectedMonth.value);
  });
  getTime(date.time);
  changeButton.value = date.name;
};

// const date = computed({
//   get() {
//     // if (!form.startTime || !form.endTime)
//     //   return [
//     //     dayjs().subtract(3, "day").format("YYYY-MM-DD HH:mm:ss"),
//     //     dayjs().format("YYYY-MM-DD HH:mm:ss"),
//     //   ];
//     return [
//       dayjs().format("YYYY-MM-DD HH:mm:ss"),
//       dayjs().format("YYYY-MM-DD HH:mm:ss"),
//     ];
//   },
//   set(date) {
//     console.log("date: ", date);
//     selectedButton.value = null;
//     startTime.value = date[0];
//     endTime.value = date[1];
//   },
// });

const showModal = () => {
  isModalVisible.value = true;
  selectedSeason.value = null;
  selectedMonth.value = null;
  datea.value = null;
  selectedYear.value = null;

  startTime.value = null;
  endTime.value = null;
  // selectedButton.value = 1;
  changeButton.value = "";
};
const close = () => {
  isModalVisible.value = false;
  selectedMonth.value = "";
};

const getTime = (data) => {
  if (data) {
    startTime.value = dayjs()
      .subtract(data, "day")
      .format("YYYY-MM-DD HH:mm:ss");
    endTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
    console.log("startDate: ", startTime.value);
    console.log("endTime: ", endTime.value);
  } else {
    startTime.value = null;
    endTime.value = null;
  }
};

const handleChange = (value) => {
  console.log("value", value);
  selectedYear.value = value;
  selectedSeason.value = null;
  selectedMonth.value = null;
  selectedButton.value = null;
  datea.value = [];
  updateStartEndTime();
  changeButton.value = "条件筛选";
  // console.log("queryParam", queryParam.value.deviceType);
};
const handleChange2 = (value) => {
  console.log("value", value);
  selectedSeason.value = value;
  selectedButton.value = null;
  selectedMonth.value = null; // 先清空月份
  monthArr.value = []; // 清空月份数组

  switch (Number.parseInt(value)) {
    case 1:
      monthArr.value = [1, 2, 3];
      break;
    case 2:
      monthArr.value = [4, 5, 6];
      break;
    case 3:
      monthArr.value = [7, 8, 9];
      break;
    case 4:
      monthArr.value = [10, 11, 12];
      break;
  }
  updateStartEndTime();
};

const handleChange3 = (value) => {
  console.log("value", value);
  selectedMonth.value = value;
  selectedButton.value = null;
  updateStartEndTime();
  // console.log("queryParam", queryParam.value.deviceType);
};

const updateStartEndTime = () => {
  console.log(111);
  if (selectedYear.value && selectedSeason.value) {
    // 如果选择了年和季度
    const seasonStartMonth = monthArr.value[0] - 1;
    const seasonEndMonth = monthArr.value[2];
    startTime.value = dayjs(new Date(selectedYear.value, seasonStartMonth, 1))
      .startOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
    endTime.value = dayjs(new Date(selectedYear.value, seasonEndMonth, 0))
      .endOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
    console.log("startDate: ", startTime.value);
    console.log("endTime: ", endTime.value);
  } else if (
    selectedYear.value &&
    selectedSeason.value &&
    selectedMonth.value !== ""
  ) {
    // 如果选择了年、季度和月份
  // 如果 monthArr.value 是 ['01', '02', '03'] 并且 selectedMonth.value 是 1,那么 month 的值将会是 2。
    // 如果 monthArr.value 是 [1, 2, 3],并且 selectedMonth.value 是 1,那么 month 的值将会是 2。parseInt(2, 10) 仍然返回 2。
    const month = parseInt(monthArr.value[selectedMonth.value], 10);
    startTime.value = dayjs(new Date(selectedYear.value, month - 1, 1))
      .startOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
    endTime.value = dayjs(new Date(selectedYear.value, month, 0))
      .endOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
    console.log("startDate: ", startTime.value);
    console.log("endTime: ", endTime.value);
  } else if (selectedYear.value && selectedSeason.value) {
    // 如果选择了年和季度
    const seasonStartMonth = monthArr.value[0] - 1;
    const seasonEndMonth = monthArr.value[2];
    startTime.value = dayjs(new Date(selectedYear.value, seasonStartMonth, 1))
      .startOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
    endTime.value = dayjs(new Date(selectedYear.value, seasonEndMonth, 0))
      .endOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
    console.log("startDate: ", startTime.value);
    console.log("endTime: ", endTime.value);
  } else if (selectedYear.value) {
    // 如果只选择了年
    startTime.value = dayjs(new Date(selectedYear.value, 0, 1))
      .startOf("year")
      .format("YYYY-MM-DD HH:mm:ss");
    endTime.value = dayjs(new Date(selectedYear.value, 11, 31))
      .endOf("year")
      .format("YYYY-MM-DD HH:mm:ss");
    console.log("startDate: ", startTime.value);
    console.log("endTime: ", endTime.value);
  }
};

const search = () => {
  if (!changeButton.value) {
    message.error("请选择查询时间范围");
    returm;
  } else {
    emit("update:start-time", startTime.value);
    emit("update:end-time", endTime.value);
    emit("changeButton", changeButton.value);
    console.log("查询时间范围:", startTime.value, endTime.value);
    close();
  }
};

const dataCheck = (data) => {
  console.log("Range picker clicked:", data);
  startTime.value = data[0];
  endTime.value = data[1];
  resetKey.value++; // 触发组件重新渲染
  console.log("startDate: ", startTime.value);
  console.log("endTime: ", endTime.value);
  selectedSeason.value = null;
  selectedMonth.value = null;

  selectedYear.value = null;
  selectedButton.value = null;
  changeButton.value = "时间筛选";

  // 在这里处理点击事件
};

//  onChangeTime (data) {
//     if (data.length != 0) {
//       this.queryParam.startDate = data[0];
//       this.queryParam.endDate = data[1];
//     } else {
//       delete this.queryParam.startDate;
//       delete this.queryParam.endDate;
//     }
//   },

defineExpose({
  //   close,
  showModal,
});
</script>
    
<style lang="scss" scoped>
.DataSelectModal {
  position: relative;
  font-size: 14px;
  width: 800px;
  height: 296px;
  color: #ffffff;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 999;
  background: url("@/assets/images/image2/selectbox.png") no-repeat;
  background-size: 100%;
  .title {
    height: 40px;
    line-height: 40px;
    padding-left: 20px;
    font-size: 14px;
    .closeModals {
      float: right;
      width: 24px;
      height: 24px;
      margin: 5px;
      cursor: pointer;
    }
  }
  .warmSelectContent {
    padding: 20px;
    .slect {
      display: flex;
      align-items: center;
      margin-bottom: 20px;
      .ofenUse {
        font-size: 12px;
        background: #042931;
        border-radius: 4px;
        border: 1px solid #0a8fab;
        margin-right: 20px;
        padding: 5px;
        cursor: pointer;
      }
      .selected {
        font-size: 12px;
        background: #148aa5;
        border-radius: 4px;
        border: 1px solid #0a8fab;
        margin-right: 20px;
        padding: 5px;
        cursor: pointer;
      }
    }
  }
  .immediately {
    position: absolute;
    bottom: 10%;
    left: 50%;
    transform: translateX(-50%);
    width: 104px;
    height: 44px;
    line-height: 44px;
    text-align: center;
    background: #134451;
    border-radius: 4px;
    border: 1px solid #165a6b;
    cursor: pointer;
  }
  :deep(.ant-select-selector) {
    background: #134451 !important;
    border: 1px solid #165a6b !important;
    color: #fff;
    width: 120px !important;
    line-height: 35px !important;
    height: 35px !important;
  }
  :deep(.ant-select-selection-item) {
    line-height: 35px !important;
  }
}
:deep(.ant-picker-range) {
  width: 300px !important;
}
</style>

 关于下拉框无法置空

这边的难点是关于下拉框是三级联动同时3种筛选选择其中一种的时候,其他两种都必须置为空。难就难在下拉框无法置空踩的坑。后面发现双向绑定还是无法置空 可以重置他们的key

有几个地方需要理解下

关于十进制的用法

 // 如果 monthArr.value 是 ['01', '02', '03'] 并且 selectedMonth.value 是 1,那么 month 的值将会是 2。
    // 如果 monthArr.value 是 [1, 2, 3],并且 selectedMonth.value 是 1,那么 month 的值将会是 2。parseInt(2, 10) 仍然返回 2。
    const month = parseInt(monthArr.value[selectedMonth.value], 10);

 关于一个月的开始和尾巴

 // 创建一个日期对象,表示selectedYear.value年seasonStartMonth月的第一天。
    startTime.value = dayjs(new Date(selectedYear.value, seasonStartMonth, 1))
      .startOf("month")
      .format("YYYY-MM-DD HH:mm:ss");
      // 这里0表示该月的最后一天。
    endTime.value = dayjs(new Date(selectedYear.value, seasonEndMonth, 0))
      .endOf("month")
      .format("YYYY-MM-DD HH:mm:ss");

4.关于函数默认传参

const getDate = (data = new Date().getFullYear()) => {}

5.关于后台合计 

问题是假设现在数据有6条 但是实际上7条,合计也就是最尾巴那条不算,后端是这么给数据的。

那么 我希望一页2条 到了第3页 一条也不显示 因为 有3个数据 但是我们写的是2 后端的逻辑返回是3 所以用arr.pop()取出多余的数据  Math.ceil()表示四舍五入 往大的走 然后添加用push行 用[]扩展运算符也行

这边的知识点是pop,Math.ceil,push,[]扩展运算符

扩展运算符 ... 可以用于数组、函数调用、字符串、Set、Map、解构赋值等多种场景。它可以将可迭代对象(如数组、Set、Map)的元素展开,或者将对象的属性展开。

    assetRentDetailAllCount() {
      this.loading = true;
      const pageList = {
        companyName: this.queryParam.companyName,
        current: this.pagination.current,
        size: this.pagination.pageSize,
      };
      // console.log("this.queryParam", this.queryParam);
      assetRentDetailAllCount(pageList)
        .then((res) => {
          this.loading = false;
          if (res.code == 0) {
            console.log("res", res);
            const { records, size, total, current } = res.data ?? {};
         
            if (records.length > size) {
              this.extraRecord = records.pop();
              console.log("extraRecord", this.extraRecord);
            }

            this.dataSource = records;
            console.log(" this.dataSource", this.dataSource);
            this.pagination.current = current;
            this.pagination.pageSize = size;
            this.pagination.total = total + 1; // 如果有多余数据,则增加总数
            // this.pagination.total = total + (this.extraRecord ? 1 : 0); // 如果有多余数据,则增加总数

            console.log("1111", this.extraRecord);

            // 检查当前页是否为最后一页,并且有多余的数据需要添加
            if (
              current === Math.ceil(this.pagination.total / size) &&
              this.extraRecord
            ) {
              // this.dataSource.push(this.extraRecord);
              this.dataSource = [...this.dataSource, this.extraRecord];
            }
          }
        })
        .catch((error) => {
          this.loading = false;
          console.error("Error fetching data:", error);
        });
    },

5.树形控件   

那个不用遍历,只要有树,id在树里面,就能展示他的label

  <a-form-item label="资产类别" hasFeedback>
              <a-tree-select
                :treeData="treeData"
                placeholder="请选择"
                :replaceFields="{
                  title: 'categoryName',
                  key: 'id',
                  value: 'id',
                  children: 'children',
                }"
                v-decorator="[
                  'categoryId',
                  {
                    initialValue: editData.categoryId,
                    rules: validatorRules.inputIf.rules,
                  },
                ]"
                @change="changeCategory"
              /> </a-form-item
          >

6.antd中的年选择器 mode=”year“ 导致无法选中 面板也关不掉

 <a-form-item label="年份">
              <a-date-picker
                v-model="queryParam.queryTime"
                mode="year"
                format="YYYY"
                :open="open"
                @panelChange="handlePanelChange"
                @openChange="handleOpenChange"
              />
            </a-form-item>
 
 open: false,


handlePanelChange(value) {
      console.log("value", value);
      this.queryParam.queryTime = dayjs(value).format("YYYY");
      // this.queryParam.queryTime = value;
      console.log("this.queryParam.queryTime", this.queryParam.queryTime);
      this.open = false;
    },

    handleOpenChange(open) {
      console.log("open", open);
      this.open = open;
    },

7.关于 上传

主要是  if (file.status === "done" || file.status === "error") { 因为我竟然打印了3个当前文件和文件列表 是因为上传的时候是有其他的状态的 所以要加这句代码

 <a-upload
                    accept=".xlsx,.xls"
                    :action="HttpAction"
                    :headers="headers"
                    :show-upload-list="false"
                    @change="handleChange($event)"
                  >
                    <a-button
                      type="link"
                      class="btnoneMore"
                      v-auth="'asset:assetscategory:import'"
                      >导入资产分类</a-button
                    >
                  </a-upload>


  handleChange({ file, fileList }) {
      if (file.status === "done" || file.status === "error") {
        console.log("当前文件:", file);
        console.log("文件列表:", fileList);
        this.localLoading = true;

        if (file.status === "done") {
          this.$message.success(`文件上传成功`);
          this.$refs.table.refresh();
          this.localLoading = false;
        } else {
          this.$message.error(`${file.name} 上传失败`);
          this.localLoading = false;
        }
      }
    },

8.关于上传需要一个进度条

 <el-upload
            class="upload-demo"
            :action="uploadFileUrl"
            multiple
            :show-file-list="false"
            :headers="headers"
            :on-change="startImport"
          >
            <!-- <el-upload
            class="upload-demo"
            :action="uploadFileUrl"
            multiple
            :show-file-list="false"
            :headers="headers"
            :on-success="handleUploadSuccess"
            :on-progress="handleUploadProgress"
          > -->
            <el-button style="margin-left: 10px">导入</el-button>
          </el-upload>







    startImport(file, fileList) {
      console.log("file", file);
      this.$refs.progressModals.clickShowModal(file);
    },

 

 <progressModals
      ref="progressModals"
      @handleUploadSuccess="handleUploadSuccess"
    ></progressModals>


import progressModals from "@/components/progress";

​​​​​​​

<template>
  <el-dialog
    :visible.sync="visible"
    title="导入进度"
    width="52%"
    append-to-body
  >
    <el-progress :percentage="uploadProgress" />

    <div slot="footer" class="dialog-footer">
      <el-button @click="handleCancel">取消导入</el-button>
      <el-button
        type="primary"
        :disabled="uploadProgress < 100"
        @click="handleConfirm"
      >
        确认导入
      </el-button>
    </div>
  </el-dialog>
</template>

<script>
export default {
  data() {
    return {
      visible: false,
      uploadProgress: 0,
      ImporList: [],
    };
  },

  methods: {
    handleEpx(event) {
      let that = this;
      that.uploadProgress = 0;
      let intervalId = setInterval(() => {
        // 每100毫秒增加1%直到100%
        if (that.uploadProgress < 99) {
          that.uploadProgress += 1;
        } else {
          clearInterval(intervalId); // 达到100%时停止定时器
          that.oncheck(event);
        }
      }, 100); // 每50毫秒增加一次
    },

    // 检查导入是否成功
    oncheck(event) {
      let that = this;
      if (this.uploadProgress === 99) {
        if (event.response.code === 200) {
          this.uploadProgress = 100; // 成功时将进度设置为100%
          setTimeout(() => {
            this.$nextTick(() => {
              this.ImporList = event.response.data;
            });
          }, 200);
        } else {
          console.error("导入失败:", event.response.msg);
          // 可根据需求处理导入失败的情况
        }
      }
    },
    clickShowModal(eRecod) {
      console.log("eRecod", eRecod);
      this.visible = true;
      this.ImporList = [];
      //   this.prosta = "active";
      //   this.msg = "检查数据中";
      //   this.expstatus = 0;
      //   this.percent = 0;
      //   this.file = {};
      //   this.assetList = [];
      this.handleEpx(eRecod);
    },
    handleConfirm() {
      this.visible = false;
      this.$emit("handleUploadSuccess", this.ImporList);
    },
    handleCancel() {
      this.visible = false;
      this.ImporList = [];
    },
  },
};
</script>
  handleUploadSuccess(data) {
      console.log("data", data);

      let mArr = JSON.parse(JSON.stringify(this.tableData));
      this.tableData = [];
      mArr = mArr.concat(
        data.map((item, index) => {
          return { ...item, sort: item.sort ? item.sort : index + 1 };
        })
      );
      this.getList(mArr);
    },

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值