碰到难点
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>
-
transition-group
:- 名称为
scroll
的transition-group
组件,用于处理列表项的动画。 tag="div"
设置容器元素为div
。- 内部包含一个动态渲染的
div
元素,用于显示当前警告信息。
- 名称为
-
div
:- 根据
isRunning
和currentItem
的值来决定是否显示警告信息。 - 使用
:key
绑定唯一的标识符,以便transition-group
能够正确跟踪元素的变化。
- 根据
-
.warmTanChuan-container
:- 定位样式,使警告信息居中显示。
-
.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);
},