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

<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>