You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

536 lines
14 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<ContentWrap class="body" >
<el-row :gutter="20">
<!-- 左侧区域 -->
<el-col :span="7" >
<!-- 左上统计信息 -->
<el-card class="box-card" style="height: 250px" shadow="always" >
<template #header>
<div class="card-header" :body-style="{ borderTop: 'none' }">
<span>统计信息</span>
</div>
</template>
<div>
<p class="colour-text">总盘点数: {{ statistics.checkLogCount }}</p>
<p class="colour-text">总随行数: {{ statistics.orderCount }}</p>
<p class="colour-text">月盘点数: {{ statistics.checkLogMonthCount }}</p>
<p class="colour-text">月随行数: {{ statistics.orderMonthCount }}</p>
</div>
</el-card>
<!-- 左中条状图 -->
<el-card class="box-card" shadow="always">
<template #header>
<div class="card-header">
<span>盘点状态</span>
</div>
</template>
<Echart dark="true" style="width: 100%; height: 200px" :options="deviceStatusChartOption" />
</el-card>
</el-col>
<!-- 中间区域 -->
<el-col :span="10">
<!-- 中上和中中选择相机直播 -->
<el-card class="box-card" shadow="always" style="height: 590px">
<el-select v-model="selectedCamera" placeholder="请选择相机" style="width: 100%">
<el-option
v-for="camera in cameraList"
:key="camera.id"
:label="camera.name"
:value="camera.id"
/>
</el-select>
<div style="height: 500px; background-color: #555555" class="mt-20px">
<Camera v-if="selectedCamera" :cameraId="selectedCamera" />
</div>
</el-card>
</el-col>
<!-- 右侧区域 -->
<el-col :span="7">
<!-- 右上:快捷方式 -->
<el-card class="box-card" style="height: 250px" shadow="always">
<template #header>
<div class="card-header">
<span>快捷方式</span>
</div>
</template>
<!-- <el-skeleton :loading="loading" animated>
</el-skeleton> -->
<el-row>
<p></p>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-50px">
<div class="flex items-center">
<Icon :icon="item.icon" size="30" class="mr-3px" />
<router-link :to="item.url">
<el-link type="default" :underline="false">
<span class='white-text-icon'>{{ item.name }}</span>
</el-link>
</router-link>
</div>
</el-col>
</el-row>
</el-card>
<!-- 右中:另一个条状图 -->
<el-card class="box-card" shadow="always">
<template #header>
<div class="card-header">
<span>本月随行情况</span>
</div>
</template>
<Echart dark="true" style="width: 100%; height: 200px" :options="orderChartOption" />
</el-card>
</el-col>
</el-row>
<!-- 底部区域 -->
<el-row :gutter="20" class="mt-20px">
<el-col :span="7">
<!-- 左下:实时滚动信息 -->
<el-card class="box-card" shadow="always">
<template #header>
<div class="card-header">
<span>盘点饼状图</span>
</div>
</template>
<Echart dark="true" style="width: 100%; height: 200px" :options="stockPieOptions" />
</el-card>
</el-col>
<el-col :span="10">
<el-card class="box-card" shadow="always">
<template #header>
<div class="card-header">
<span>随行折线图</span>
</div>
</template>
<Echart dark="true" style="width: 100%; height: 200px" :options="laneInventoryLineOptions" />
</el-card>
</el-col>
<el-col :span="7">
<!-- -->
<el-card class="box-card" shadow="always">
<template #header>
<div class="card-header">
<span>实时滚动信息</span>
</div>
</template>
<el-scrollbar height="200px">
<div
v-for="(item, index) in realTimeLogs.slice().reverse()"
:key="index"
class="log-item"
>
<p class="colour-text">
{{ item }}
</p>
</div>
</el-scrollbar>
</el-card>
</el-col>
</el-row>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { CameraApi, CameraVO } from '@/api/logistics/camera'
import { homeApi } from '@/api/logistics/home'
import Camera from '@/components/camera/camera.vue'
import { EChartsOption } from 'echarts'
import { StockApi } from '@/api/logistics/stock'
import type { Shortcut } from './types'
// ----------------------------- -----------------------------
//
const statistics = ref({
checkLogCount: 0,
orderCount: 0,
checkLogMonthCount: 0,
orderMonthCount: 0
})
const laneInventoryStatistics = ref()
const laneInventoryLine = ref()
const stockLaneInventoryStatistics = ref()
const buttonByStatusMap = ref(new Map<string, number>()) // 存储 row-column 到 statusString 的映射
// 选择相机直播
const selectedCamera = ref<number | null>(null)
const cameraList = ref<CameraVO[]>([])
// 快捷方式
let shortcut = reactive<Shortcut[]>([])
// 实时滚动信息
const realTimeLogs = ref<string[]>([])
let logTimer: NodeJS.Timeout | null = null
// ----------------------------- 数据获取 -----------------------------
// 获取所有数据
const getAll = async () => {
try {
statistics.value = await homeApi.statistics()
laneInventoryStatistics.value = await homeApi.laneInventoryStatistics()
laneInventoryLine.value = await homeApi.laneInventoryLine()
stockLaneInventoryStatistics.value = await homeApi.stockLaneInventoryStatistics()
const chartData = stockLaneInventoryStatistics.value
// 设置设备状态条状图选项
deviceStatusChartOption.value = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: chartData.series.map((series) => series.name)
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: chartData.xaxis,
yAxis: chartData.yaxis,
series: chartData.series.map((series) => ({
name: series.name,
type: series.type,
stack: series.stack,
itemStyle: {
color: series.color
},
data: series.data
}))
}
// 设置本月随行情况条状图选项
orderChartOption.value = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: laneInventoryStatistics.value.xaxis,
yAxis: {
type: 'value',
interval: 1, // 设置步长为 1
splitNumber: 5 // 尝试把轴分成 5 段
},
series: laneInventoryStatistics.value.series
}
// 设置随行折线图选项
laneInventoryLineOptions.value = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: laneInventoryLine.value.legend,
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: laneInventoryLine.value.xaxis,
yAxis: {
type: 'value',
interval: 1, // 设置步长为 1
splitNumber: 5 // 尝试把轴分成 5 段
},
series: laneInventoryLine.value.series
}
// 获取街道状态数据并更新饼状图
const data = await StockApi.getStreetStatus({})
for (const item of data) {
if (buttonByStatusMap.value.has(item.statusString)) {
buttonByStatusMap.value.set(
item.statusString,
buttonByStatusMap.value.get(item.statusString)! + 1
)
} else {
buttonByStatusMap.value.set(item.statusString, 1)
}
}
stockPieOptions.series[0].data = echartData()
} catch (error) {
console.error('获取数据时出错:', error)
}
}
// 获取快捷入口
const getShortcut = async () => {
const data = [
{
name: '随行记录',
icon: 'ep:data-analysis',
url: 'logistics/order',
},
{
name: '盘点管理',
icon: 'ep:coin',
url: 'logistics/check-log',
},
{
name: '盘点信息',
icon: 'ep:pointer',
url: 'logistics/stock',
},
{
name: '实时视频',
icon: 'ep:magic-stick',
url: 'cameraTree/cameraStreet',
},
{
name: '巷道管理',
icon: 'fa-solid:rainbow',
url: 'system/street',
},
{
name: '相机管理',
icon: 'ep:camera-filled',
url: 'system/camera',
}
]
shortcut = Object.assign(shortcut, data)
}
// ----------------------------- 数据处理 -----------------------------
// 构造 ECharts 数据,使用 statusString 作为 name
const echartData = (): { name: string; value: number }[] => {
return Array.from(buttonByStatusMap.value, ([name, value]) => ({ name, value }))
}
// ----------------------------- 图表选项 -----------------------------
// 条状图数据
const deviceStatusChartOption = ref<EChartsOption>({})
const laneInventoryLineOptions = ref<EChartsOption>({})
const orderChartOption = ref<EChartsOption>({})
let eventSource: any = null
const sseUid = ref(`${Date.now()}-${Math.floor(Math.random() * 10000)}`)
const sse = ()=>{
// 创建一个 EventSource 实例,连接到后端的 SSE 路径
eventSource = new EventSource(import.meta.env.VITE_BASE_URL+'/app-api/sse/createSse?uid=' + sseUid.value );
// 监听来自后端的消息
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data);
realTimeLogs.value.push(data.message)
};
// 处理错误
eventSource.onerror = (error) => {
console.error('SSE 连接发生错误:', error);
};
}
// 盘点饼状图选项
const stockPieOptions = reactive({
tooltip: {
trigger: 'item'
},
legend: {
top: '4%',
left: 'center'
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderWidth: 2,
color: function (params) {
const colorList = ['#fa5762', '#97fa8c', '#32cd32', '#ff6347']
return colorList[params.dataIndex] // 根据数据项的index分配颜色
}
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
})
// ----------------------------- 快捷方式处理 -----------------------------
// 快捷方式处理
const handleShortcut = (action: string) => {
switch (action) {
case 'setting':
ElMessage.info('打开设置')
break
case 'help':
ElMessageBox.alert('帮助信息', '帮助', {
confirmButtonText: '确定'
})
break
case 'logout':
ElMessageBox.confirm('确认退出?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('退出成功')
})
.catch(() => {
ElMessage.info('已取消')
})
break
default:
ElMessage.warning('未知操作')
}
}
// ----------------------------- 实时滚动信息 -----------------------------
// 启动日志定时器
// const startLogTimer = () => {
// logTimer = setInterval(() => {
// const newLog = `日志 ${realTimeLogs.value.length + 1}: ${new Date().toLocaleTimeString()}`
// realTimeLogs.value.push(newLog)
// if (realTimeLogs.value.length > 10) {
// realTimeLogs.value.shift()
// }
// }, 2000)
// }
// // 清除日志定时器
// const clearLogTimer = () => {
// if (logTimer) {
// clearInterval(logTimer)
// logTimer = null
// }
// }
// ----------------------------- 生命周期钩子 -----------------------------
// 初始化
onMounted(() => {
getShortcut()
getAll() // 获取所有数据
// 获取相机列表
CameraApi.getCameraList().then((res) => {
cameraList.value = res
})
sse()
})
onUnmounted(() => {
// 在组件销毁时关闭 EventSource 连接
if (eventSource) {
eventSource.close();
}
})
</script>
<style scoped>
.body{
background-image:url(/public/pic02.jpg) ;
background-size:cover;
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
color: white;
background: url('/public/pic01.png') no-repeat center;
background-size: cover;
}
.colour-text {
color: white;
}
.card-header::v-deep .el-card__header {
border-bottom: none !important;
}
.el-card {
border-color: rgba(5, 2, 53, 0.87); /* 深蓝色透明边框 */
background-color: rgba(5, 2, 53, 0.87); /* 深蓝色透明背景 */
}
.log-item {
padding: 5px 0;
border-bottom: 1px solid #ebeef5;
}
.log-item:last-child {
border-bottom: none;
}
.mt-20px {
margin-top: 20px;
}
.mb-20px {
margin-bottom: 30px;
}
.box-card {
margin-bottom: 20px;
}
.box-card ::v-deep .el-card__header {
border-bottom: none !important;
}
.box-card ::v-deep .el-card__body {
background-color: #070225;
color: #7d6ea3ee; /* 接近黑色*/
}
.white-text-icon, .el-icon {
color: white !important;
}
/*.box-card { box-shadow: 0 0 10px rgba(0, 0, 255, 0.5); } */
.box-card { border-radius: 10px; } /* 加圆角*/
</style>