521 lines
18 KiB
Vue
521 lines
18 KiB
Vue
<template>
|
||
<div class="home-statistics">
|
||
<!-- 设备选择器 -->
|
||
<Card style="margin-bottom: 16px;">
|
||
<p slot="title">设备选择</p>
|
||
<Select
|
||
v-model="selectedDeviceSn"
|
||
@on-change="handleDeviceChange"
|
||
placeholder="请选择设备"
|
||
style="width: 300px;"
|
||
>
|
||
<Option
|
||
v-for="device in deviceList"
|
||
:value="device.deviceSn"
|
||
:key="device.deviceSn || device.deviceName"
|
||
>
|
||
{{ device.deviceName || device.deviceSn }}
|
||
</Option>
|
||
</Select>
|
||
</Card>
|
||
|
||
<!-- 统计卡片 -->
|
||
<Row :gutter="16" style="margin-bottom: 16px;">
|
||
<Col span="6">
|
||
<Card>
|
||
<div class="statistic-item">
|
||
<div class="statistic-title">今日投递</div>
|
||
<div class="statistic-value">{{ todayStats.applyCount }}</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
<Col span="6">
|
||
<Card>
|
||
<div class="statistic-item">
|
||
<div class="statistic-title">今日找工作</div>
|
||
<div class="statistic-value">{{ todayStats.jobSearchCount }}</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
<Col span="6">
|
||
<Card>
|
||
<div class="statistic-item">
|
||
<div class="statistic-title">今日沟通</div>
|
||
<div class="statistic-value">{{ todayStats.chatCount }}</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
<Col span="6">
|
||
<Card>
|
||
<div class="statistic-item">
|
||
<div class="statistic-title">执行中任务</div>
|
||
<div class="statistic-value">{{ todayStats.runningTaskCount }}</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<!-- 当前执行任务的命令区块(指令列表) -->
|
||
<Card v-if="runningTasks.length > 0" style="margin-bottom: 16px;">
|
||
<p slot="title">当前执行中的任务</p>
|
||
<Table :columns="taskColumns" :data="runningTasks" :loading="taskLoading">
|
||
<template slot-scope="{ row }" slot="commands">
|
||
<Tag
|
||
v-for="(cmd, index) in row.commands"
|
||
:key="index"
|
||
:color="getCommandColor(cmd.status)"
|
||
style="margin: 2px;"
|
||
>
|
||
{{ cmd.commandName }}
|
||
</Tag>
|
||
</template>
|
||
</Table>
|
||
</Card>
|
||
|
||
<!-- 趋势图表(7天趋势) -->
|
||
<Card>
|
||
<p slot="title">近7天统计趋势</p>
|
||
<div ref="chartContainer" style="height: 400px;"></div>
|
||
</Card>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import * as echarts from 'echarts'
|
||
import DeviceStatusServer from '../../api/device/device_status_server.js'
|
||
import StatisticsServer from '../../api/statistics/statistics_server.js'
|
||
|
||
export default {
|
||
name: 'HomePage',
|
||
|
||
data() {
|
||
return {
|
||
selectedDeviceSn: '',
|
||
deviceList: [],
|
||
todayStats: {
|
||
applyCount: 0,
|
||
jobSearchCount: 0,
|
||
chatCount: 0,
|
||
runningTaskCount: 0
|
||
},
|
||
chartData: {
|
||
dates: [],
|
||
applyData: [],
|
||
jobSearchData: [],
|
||
chatData: []
|
||
},
|
||
runningTasks: [],
|
||
taskLoading: false,
|
||
chartInstance: null, // ECharts实例
|
||
taskColumns: [
|
||
{
|
||
title: '任务名称',
|
||
key: 'taskName',
|
||
width: 200
|
||
},
|
||
{
|
||
title: '任务类型',
|
||
key: 'taskType',
|
||
width: 120
|
||
},
|
||
{
|
||
title: '开始时间',
|
||
key: 'startTime',
|
||
width: 180
|
||
},
|
||
{
|
||
title: '进度',
|
||
key: 'progress',
|
||
width: 100,
|
||
render: (h, params) => {
|
||
return h('Progress', {
|
||
props: {
|
||
percent: params.row.progress || 0,
|
||
status: 'active'
|
||
}
|
||
})
|
||
}
|
||
},
|
||
{
|
||
title: '命令列表',
|
||
slot: 'commands',
|
||
minWidth: 300
|
||
}
|
||
],
|
||
refreshTimer: null
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
// 从 store 获取选中的设备
|
||
storeDeviceSn() {
|
||
return this.$store.getters['device/selectedDeviceSn']
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
storeDeviceSn: {
|
||
immediate: true,
|
||
handler(val) {
|
||
if (val && val !== this.selectedDeviceSn) {
|
||
this.selectedDeviceSn = val
|
||
this.loadData()
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
this.loadDeviceList()
|
||
// 每30秒刷新一次数据
|
||
this.refreshTimer = setInterval(() => {
|
||
this.loadData()
|
||
}, 30000)
|
||
},
|
||
|
||
beforeDestroy() {
|
||
// 清理定时器
|
||
if (this.refreshTimer) {
|
||
clearInterval(this.refreshTimer)
|
||
this.refreshTimer = null
|
||
}
|
||
// 移除窗口大小调整监听
|
||
if (this._chartResizeHandler) {
|
||
window.removeEventListener('resize', this._chartResizeHandler)
|
||
this._chartResizeHandler = null
|
||
}
|
||
// 销毁图表实例
|
||
if (this.chartInstance) {
|
||
this.chartInstance.dispose()
|
||
this.chartInstance = null
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
// 加载设备列表
|
||
async loadDeviceList() {
|
||
try {
|
||
const res = await DeviceStatusServer.page({
|
||
seachOption: {},
|
||
pageOption: { page: 1, pageSize: 100 }
|
||
})
|
||
|
||
console.log('设备列表接口返回:', res)
|
||
console.log('res.data:', res.data)
|
||
console.log('res.data.list:', res.data?.list)
|
||
console.log('res.data.rows:', res.data?.rows)
|
||
|
||
// 支持多种数据格式
|
||
const deviceList = res.data?.list || res.data?.rows || res.data?.data || []
|
||
|
||
console.log('解析后的设备列表:', deviceList)
|
||
console.log('设备数量:', deviceList.length)
|
||
|
||
if (deviceList.length > 0) {
|
||
// 统一设备字段名,去重处理
|
||
const deviceMap = new Map()
|
||
|
||
deviceList.forEach(device => {
|
||
const sn = device.deviceSn || device.sn_code || device.snCode || device.id
|
||
if (sn && !deviceMap.has(sn)) {
|
||
deviceMap.set(sn, {
|
||
deviceSn: sn,
|
||
deviceName: device.deviceName || device.device_name || device.name || sn
|
||
})
|
||
}
|
||
})
|
||
|
||
this.deviceList = Array.from(deviceMap.values())
|
||
|
||
console.log('处理后的设备列表:', this.deviceList)
|
||
console.log('处理后设备数量:', this.deviceList.length)
|
||
|
||
this.$store.dispatch('device/setDeviceList', this.deviceList)
|
||
|
||
// 如果没有选中设备且有设备列表,选中第一个
|
||
if (!this.selectedDeviceSn && this.deviceList.length > 0) {
|
||
this.selectedDeviceSn = this.deviceList[0].deviceSn
|
||
this.$store.dispatch('device/setSelectedDevice', this.selectedDeviceSn)
|
||
// 立即加载数据
|
||
this.loadData()
|
||
} else if (this.selectedDeviceSn) {
|
||
// 如果已有选中设备,直接加载数据
|
||
this.loadData()
|
||
}
|
||
} else {
|
||
console.warn('设备列表为空,原始数据:', res)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载设备列表失败:', error)
|
||
this.$Message.error('加载设备列表失败: ' + (error.message || '未知错误'))
|
||
}
|
||
},
|
||
|
||
// 设备切换
|
||
handleDeviceChange(deviceSn) {
|
||
this.$store.dispatch('device/setSelectedDevice', deviceSn)
|
||
this.loadData()
|
||
},
|
||
|
||
// 加载所有数据
|
||
async loadData() {
|
||
if (!this.selectedDeviceSn) return
|
||
|
||
await Promise.all([
|
||
this.loadTodayStats(),
|
||
this.loadChartData(),
|
||
this.loadRunningTasks()
|
||
])
|
||
},
|
||
|
||
// 加载今日统计
|
||
async loadTodayStats() {
|
||
try {
|
||
console.log('加载今日统计,设备SN:', this.selectedDeviceSn)
|
||
const res = await StatisticsServer.getOverview(this.selectedDeviceSn)
|
||
console.log('今日统计接口返回:', res)
|
||
|
||
if (res.code === 0) {
|
||
this.todayStats = {
|
||
applyCount: res.data?.applyCount || res.data?.todayApplyCount || 0,
|
||
jobSearchCount: res.data?.jobSearchCount || res.data?.todayJobSearchCount || 0,
|
||
chatCount: res.data?.chatCount || res.data?.todayChatCount || 0,
|
||
runningTaskCount: res.data?.runningTaskCount || res.data?.runningTasks || 0
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载今日统计失败:', error)
|
||
// 不显示错误,避免干扰用户
|
||
}
|
||
},
|
||
|
||
// 加载图表数据
|
||
async loadChartData() {
|
||
try {
|
||
console.log('加载图表数据,设备SN:', this.selectedDeviceSn)
|
||
const res = await StatisticsServer.getDailyStatistics({
|
||
deviceSn: this.selectedDeviceSn,
|
||
days: 7
|
||
})
|
||
console.log('图表数据接口返回:', res)
|
||
|
||
if (res.code === 0) {
|
||
this.chartData = {
|
||
dates: res.data?.dates || [],
|
||
applyData: res.data?.applyData || [],
|
||
jobSearchData: res.data?.jobSearchData || [],
|
||
chatData: res.data?.chatData || []
|
||
}
|
||
this.$nextTick(() => {
|
||
this.renderChart()
|
||
})
|
||
} else {
|
||
console.warn('加载图表数据失败:', res.msg || res.message)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载图表数据失败:', error)
|
||
}
|
||
},
|
||
|
||
// 加载正在执行的任务
|
||
async loadRunningTasks() {
|
||
this.taskLoading = true
|
||
try {
|
||
console.log('加载执行中任务,设备SN:', this.selectedDeviceSn)
|
||
const res = await StatisticsServer.getRunningTasks(this.selectedDeviceSn)
|
||
console.log('执行中任务接口返回:', res)
|
||
|
||
if (res.code === 0) {
|
||
this.runningTasks = res.data || []
|
||
} else {
|
||
console.warn('加载执行中任务失败:', res.msg || res.message)
|
||
this.runningTasks = []
|
||
}
|
||
} catch (error) {
|
||
console.error('加载执行中任务失败:', error)
|
||
this.runningTasks = []
|
||
} finally {
|
||
this.taskLoading = false
|
||
}
|
||
},
|
||
|
||
// 渲染图表
|
||
renderChart() {
|
||
if (!this.$refs.chartContainer) return
|
||
|
||
// 如果没有数据,显示提示
|
||
if (!this.chartData.dates || this.chartData.dates.length === 0) {
|
||
if (this.chartInstance) {
|
||
this.chartInstance.dispose()
|
||
this.chartInstance = null
|
||
}
|
||
this.$refs.chartContainer.innerHTML = `
|
||
<div style="text-align: center; padding: 100px 0; color: #999;">
|
||
<p>暂无统计数据</p>
|
||
</div>
|
||
`
|
||
return
|
||
}
|
||
|
||
// 初始化或获取图表实例
|
||
if (!this.chartInstance) {
|
||
this.chartInstance = echarts.init(this.$refs.chartContainer)
|
||
}
|
||
|
||
// 格式化日期显示(只显示月-日)
|
||
const formattedDates = this.chartData.dates.map(date => {
|
||
const d = new Date(date)
|
||
return `${d.getMonth() + 1}-${d.getDate()}`
|
||
})
|
||
|
||
// 配置图表选项
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'cross'
|
||
}
|
||
},
|
||
legend: {
|
||
data: ['投递', '找工作', '沟通'],
|
||
top: 10
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: formattedDates,
|
||
axisLabel: {
|
||
rotate: 45
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
name: '数量'
|
||
},
|
||
series: [
|
||
{
|
||
name: '投递',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: this.chartData.applyData,
|
||
itemStyle: {
|
||
color: '#2d8cf0'
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: 'rgba(45, 140, 240, 0.3)' },
|
||
{ offset: 1, color: 'rgba(45, 140, 240, 0.1)' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '找工作',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: this.chartData.jobSearchData,
|
||
itemStyle: {
|
||
color: '#19be6b'
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: 'rgba(25, 190, 107, 0.3)' },
|
||
{ offset: 1, color: 'rgba(25, 190, 107, 0.1)' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '沟通',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: this.chartData.chatData,
|
||
itemStyle: {
|
||
color: '#ff9900'
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: 'rgba(255, 153, 0, 0.3)' },
|
||
{ offset: 1, color: 'rgba(255, 153, 0, 0.1)' }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
// 设置图表选项
|
||
this.chartInstance.setOption(option, true)
|
||
|
||
// 响应式调整 - 使用箭头函数确保this指向正确
|
||
if (!this._chartResizeHandler) {
|
||
this._chartResizeHandler = () => {
|
||
if (this.chartInstance) {
|
||
this.chartInstance.resize()
|
||
}
|
||
}
|
||
window.addEventListener('resize', this._chartResizeHandler)
|
||
}
|
||
},
|
||
|
||
// 获取命令状态颜色
|
||
getCommandColor(status) {
|
||
const colorMap = {
|
||
'pending': 'default',
|
||
'running': 'blue',
|
||
'success': 'green',
|
||
'failed': 'red',
|
||
'skipped': 'warning'
|
||
}
|
||
return colorMap[status] || 'default'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.home-statistics {
|
||
padding: 16px;
|
||
}
|
||
|
||
.statistic-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.statistic-title {
|
||
font-size: 14px;
|
||
color: #999;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.statistic-value {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
color: #2d8cf0;
|
||
}
|
||
</style>
|