|
|
<template>
|
|
|
<div class="dashboard-page">
|
|
|
<!-- 第1行 — 3个统计卡片 -->
|
|
|
<el-row :gutter="16" class="stat-row">
|
|
|
<el-col :span="8">
|
|
|
<el-card shadow="hover">
|
|
|
<div class="stat-card">
|
|
|
<div class="stat-label">
|
|
|
在线机床数
|
|
|
<el-tooltip content="当前网络连通的机床数量占所有已启用机床总数的比例。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</div>
|
|
|
<div class="stat-value">{{ summary.onlineCount }}<span class="stat-unit"> / {{ summary.totalMachines }}</span></div>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-card shadow="hover">
|
|
|
<div class="stat-card">
|
|
|
<div class="stat-label">
|
|
|
今日总产量
|
|
|
<el-tooltip content="今日所有机床加工完成的零件总数。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</div>
|
|
|
<div class="stat-value">{{ summary.todayProduction?.toLocaleString() }}</div>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<!-- 第2行 — 双图表 -->
|
|
|
<el-row :gutter="16" class="chart-row">
|
|
|
<el-col :span="12">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header>
|
|
|
<span class="card-title">
|
|
|
产量趋势(近7天)
|
|
|
<el-tooltip content="最近7天每天的零件总产量变化趋势。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
</template>
|
|
|
<div ref="trendChartRef" style="height: 260px"></div>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header>
|
|
|
<div class="card-header">
|
|
|
<span class="card-title">
|
|
|
NC程序产量分布
|
|
|
<el-tooltip content="按NC程序名统计的产量占比分布。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
<div class="date-filter">
|
|
|
<el-radio-group v-model="programDistDateType" size="small" @change="onProgramDistDateChange">
|
|
|
<el-radio-button value="today">今日</el-radio-button>
|
|
|
<el-radio-button value="yesterday">昨日</el-radio-button>
|
|
|
<el-radio-button value="last3">近3天</el-radio-button>
|
|
|
<el-radio-button value="last7">近7天</el-radio-button>
|
|
|
<el-radio-button value="custom">自定义</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
<el-date-picker
|
|
|
v-if="programDistDateType === 'custom'"
|
|
|
v-model="programDistDateRange"
|
|
|
type="daterange"
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始日期"
|
|
|
end-placeholder="结束日期"
|
|
|
size="small"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
:disabled-date="(d: Date) => d > new Date()"
|
|
|
style="margin-left: 8px"
|
|
|
@change="loadProgramDistributionData"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div ref="programDistChartRef" style="height: 260px"></div>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<!-- 第3行 — 双排行 -->
|
|
|
<el-row :gutter="16" class="rank-row">
|
|
|
<el-col :span="12">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header>
|
|
|
<div class="card-header">
|
|
|
<span class="card-title">
|
|
|
机床产量排行 TOP
|
|
|
<el-select v-model="machineTopN" size="small" style="width: 72px" @change="loadMachineRankData">
|
|
|
<el-option :value="5" label="5" />
|
|
|
<el-option :value="10" label="10" />
|
|
|
<el-option :value="20" label="20" />
|
|
|
<el-option :value="50" label="50" />
|
|
|
<el-option :value="100" label="100" />
|
|
|
</el-select>
|
|
|
<el-tooltip content="单台机床加工零件数排列。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
<div class="date-filter">
|
|
|
<el-select v-model="machineSortOrder" size="small" style="width: 90px; margin-right: 8px" @change="loadMachineRankData">
|
|
|
<el-option value="desc" label="降序" />
|
|
|
<el-option value="asc" label="升序" />
|
|
|
</el-select>
|
|
|
<el-radio-group v-model="machineDateType" size="small" @change="onMachineDateChange">
|
|
|
<el-radio-button value="today">今日</el-radio-button>
|
|
|
<el-radio-button value="yesterday">昨日</el-radio-button>
|
|
|
<el-radio-button value="last3">近3天</el-radio-button>
|
|
|
<el-radio-button value="last7">近7天</el-radio-button>
|
|
|
<el-radio-button value="custom">自定义</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
<el-date-picker
|
|
|
v-if="machineDateType === 'custom'"
|
|
|
v-model="machineDateRange"
|
|
|
type="daterange"
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始日期"
|
|
|
end-placeholder="结束日期"
|
|
|
size="small"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
:disabled-date="(d: Date) => d > new Date()"
|
|
|
style="margin-left: 8px"
|
|
|
@change="loadMachineRankData"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-table :data="machineRank" stripe size="small" style="width: 100%">
|
|
|
<el-table-column prop="rank" label="排名" width="60" align="center" />
|
|
|
<el-table-column label="机床名称" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<router-link :to="isMock ? '/mock/machine/' + row.id : '/machine/' + row.id" class="machine-link">{{ row.machineName }}</router-link>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="program" label="当前程序" show-overflow-tooltip />
|
|
|
<el-table-column label="产量" width="80" align="center">
|
|
|
<template #default="{ row }">{{ formatNumber(row.quantity) }}</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="状态" width="70" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">{{ row.status === 1 ? '在线' : '离线' }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header>
|
|
|
<div class="card-header">
|
|
|
<span class="card-title">
|
|
|
NC程序产量排行 TOP
|
|
|
<el-select v-model="programRankTopN" size="small" style="width: 72px" @change="loadProgramRankData">
|
|
|
<el-option :value="5" label="5" />
|
|
|
<el-option :value="10" label="10" />
|
|
|
<el-option :value="20" label="20" />
|
|
|
<el-option :value="50" label="50" />
|
|
|
<el-option :value="100" label="100" />
|
|
|
</el-select>
|
|
|
<el-tooltip content="按NC程序名统计的产量排行。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
<div class="date-filter">
|
|
|
<el-radio-group v-model="programRankDateType" size="small" @change="onProgramRankDateChange">
|
|
|
<el-radio-button value="today">今日</el-radio-button>
|
|
|
<el-radio-button value="yesterday">昨日</el-radio-button>
|
|
|
<el-radio-button value="last3">近3天</el-radio-button>
|
|
|
<el-radio-button value="last7">近7天</el-radio-button>
|
|
|
<el-radio-button value="custom">自定义</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
<el-date-picker
|
|
|
v-if="programRankDateType === 'custom'"
|
|
|
v-model="programRankDateRange"
|
|
|
type="daterange"
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始日期"
|
|
|
end-placeholder="结束日期"
|
|
|
size="small"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
:disabled-date="(d: Date) => d > new Date()"
|
|
|
style="margin-left: 8px"
|
|
|
@change="loadProgramRankData"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-table :data="programRank" stripe size="small" style="width: 100%">
|
|
|
<el-table-column prop="rank" label="排名" width="60" align="center" />
|
|
|
<el-table-column prop="programName" label="程序名" show-overflow-tooltip />
|
|
|
<el-table-column label="总产量" width="100" align="center">
|
|
|
<template #default="{ row }">{{ formatNumber(row.totalQuantity) }}</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="machineCount" label="涉及机床数" width="120" align="center" />
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<!-- 第4行 — 双排行/图表 -->
|
|
|
<el-row :gutter="16" class="rank-row">
|
|
|
<el-col :span="12">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header>
|
|
|
<div class="card-header">
|
|
|
<span class="card-title">
|
|
|
工人产量排行 TOP
|
|
|
<el-select v-model="workerTopN" size="small" style="width: 72px" @change="loadWorkerRankData">
|
|
|
<el-option :value="5" label="5" />
|
|
|
<el-option :value="10" label="10" />
|
|
|
<el-option :value="20" label="20" />
|
|
|
<el-option :value="50" label="50" />
|
|
|
<el-option :value="100" label="100" />
|
|
|
</el-select>
|
|
|
<el-tooltip content="工人所绑定的所有机床产量合计。" placement="top">
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
<div class="date-filter">
|
|
|
<el-select v-model="workerSortOrder" size="small" style="width: 90px; margin-right: 8px" @change="loadWorkerRankData">
|
|
|
<el-option value="desc" label="降序" />
|
|
|
<el-option value="asc" label="升序" />
|
|
|
</el-select>
|
|
|
<el-radio-group v-model="workerDateType" size="small" @change="onWorkerDateChange">
|
|
|
<el-radio-button value="today">今日</el-radio-button>
|
|
|
<el-radio-button value="yesterday">昨日</el-radio-button>
|
|
|
<el-radio-button value="last3">近3天</el-radio-button>
|
|
|
<el-radio-button value="last7">近7天</el-radio-button>
|
|
|
<el-radio-button value="custom">自定义</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
<el-date-picker
|
|
|
v-if="workerDateType === 'custom'"
|
|
|
v-model="workerDateRange"
|
|
|
type="daterange"
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始日期"
|
|
|
end-placeholder="结束日期"
|
|
|
size="small"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
:disabled-date="(d: Date) => d > new Date()"
|
|
|
style="margin-left: 8px"
|
|
|
@change="loadWorkerRankData"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-table :data="workerRank" stripe size="small" style="width: 100%">
|
|
|
<el-table-column prop="rank" label="排名" width="60" align="center" />
|
|
|
<el-table-column prop="workerName" label="工人姓名" />
|
|
|
<el-table-column prop="machineCount" label="绑定机床" width="100" align="center" />
|
|
|
<el-table-column label="总产量" width="100" align="center">
|
|
|
<template #default="{ row }">{{ formatNumber(row.totalQuantity) }}</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header>
|
|
|
<div class="card-header">
|
|
|
<span class="card-title">
|
|
|
车间平均单机产量({{ workshopDateLabel }})
|
|
|
<el-tooltip placement="top">
|
|
|
<template #content>
|
|
|
<div style="max-width: 360px; line-height: 1.6">
|
|
|
<b>统计规则:</b>车间总产量 ÷ 车间机床数 ÷ 天数<br /><br />
|
|
|
<b>数据来源:</b>系统每日凌晨1:00自动执行日终汇总,将每台机床当天所有产量分段按程序名合并计算,生成日产量记录。多天范围取日均单机产量。<br /><br />
|
|
|
<b>产量计算:</b>以NC程序名标识零件,程序切换时自动结算上一段产量。同程序多次出现时分段记录、日汇总合并。含手工修正值。
|
|
|
</div>
|
|
|
</template>
|
|
|
<span class="info-icon">ⓘ</span>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
<div class="date-filter">
|
|
|
<el-radio-group v-model="workshopDateType" size="small" @change="onWorkshopDateChange">
|
|
|
<el-radio-button value="today">今日</el-radio-button>
|
|
|
<el-radio-button value="yesterday">昨日</el-radio-button>
|
|
|
<el-radio-button value="last3">近3天</el-radio-button>
|
|
|
<el-radio-button value="last7">近7天</el-radio-button>
|
|
|
<el-radio-button value="custom">自定义</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
<el-date-picker
|
|
|
v-if="workshopDateType === 'custom'"
|
|
|
v-model="workshopDateRange"
|
|
|
type="daterange"
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始日期"
|
|
|
end-placeholder="结束日期"
|
|
|
size="small"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
:disabled-date="(d: Date) => d > new Date()"
|
|
|
style="margin-left: 8px"
|
|
|
@change="loadWorkshopData"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div ref="workshopChartRef" style="height: 260px"></div>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<!-- 车间产量表格 -->
|
|
|
<el-row :gutter="16" style="margin-top:16px">
|
|
|
<el-col :span="24">
|
|
|
<el-card shadow="hover">
|
|
|
<template #header><span class="card-title">车间产量明细</span></template>
|
|
|
<el-table :data="workshopData" stripe size="small">
|
|
|
<el-table-column prop="workshopName" label="车间" />
|
|
|
<el-table-column prop="quantity" label="总产量" align="center" />
|
|
|
<el-table-column prop="machineCount" label="机床数" align="center" />
|
|
|
<el-table-column label="平均单产({{ workshopUnitLabel }})" align="center">
|
|
|
<template #default="{ row }">{{ row.machineCount ? (row.quantity / row.machineCount).toFixed(2) : '-' }} {{ workshopUnit }}</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import request from '@/utils/request'
|
|
|
import { useMockMode } from '@/composables/useMockMode'
|
|
|
import echarts from '@/utils/echarts'
|
|
|
import type { ECharts } from 'echarts/core'
|
|
|
import type { ApiResponse, DashboardSummary, CollectorStatus, MachineRankRow, WorkerRankRow, DashboardTrendItem, WorkshopProduction } from '@/types'
|
|
|
|
|
|
// ==================== 本地类型定义(不修改 types/index.ts) ====================
|
|
|
|
|
|
/** NC程序产量排行项 */
|
|
|
interface ProgramRankItem {
|
|
|
rank: number
|
|
|
programName: string
|
|
|
totalQuantity: number
|
|
|
machineCount: number
|
|
|
}
|
|
|
|
|
|
/** NC程序产量分布项 */
|
|
|
interface ProgramDistributionItem {
|
|
|
programName: string
|
|
|
totalQuantity: number
|
|
|
percentage: number
|
|
|
}
|
|
|
|
|
|
// ==================== Mock 模式 ====================
|
|
|
|
|
|
const { isMock } = useMockMode()
|
|
|
|
|
|
// ==================== 日期类型 ====================
|
|
|
|
|
|
type DateType = 'today' | 'yesterday' | 'last3' | 'last7' | 'custom'
|
|
|
|
|
|
const dateLabels: Record<DateType, string> = {
|
|
|
today: '今日',
|
|
|
yesterday: '昨日',
|
|
|
last3: '近3天',
|
|
|
last7: '近7天',
|
|
|
custom: '自定义',
|
|
|
}
|
|
|
|
|
|
// 日期范围格式化工具函数
|
|
|
function getDateRange(type: DateType, customRange?: [string, string]): { startDate: string; endDate: string } {
|
|
|
const today = new Date()
|
|
|
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
const endDate = fmt(today)
|
|
|
switch (type) {
|
|
|
case 'today': return { startDate: endDate, endDate }
|
|
|
case 'yesterday': { const y = new Date(today); y.setDate(y.getDate() - 1); const ys = fmt(y); return { startDate: ys, endDate: ys } }
|
|
|
case 'last3': { const d = new Date(today); d.setDate(d.getDate() - 2); return { startDate: fmt(d), endDate } }
|
|
|
case 'last7': { const d = new Date(today); d.setDate(d.getDate() - 6); return { startDate: fmt(d), endDate } }
|
|
|
case 'custom': return { startDate: customRange?.[0] || endDate, endDate: customRange?.[1] || endDate }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ==================== 响应式数据 ====================
|
|
|
|
|
|
// 第1行 — 统计卡片
|
|
|
const summary = ref<DashboardSummary>({
|
|
|
onlineCount: 0,
|
|
|
totalMachines: 0,
|
|
|
todayProduction: 0,
|
|
|
activeAlerts: 0,
|
|
|
collectSuccessRate: 0,
|
|
|
runningMachines: 0,
|
|
|
dataMissingMachines: 0,
|
|
|
})
|
|
|
const collectorStatus = ref<CollectorStatus>({
|
|
|
status: 'stopped',
|
|
|
uptimeSeconds: 0,
|
|
|
serviceStatus: 'NotInstalled',
|
|
|
serviceName: '',
|
|
|
})
|
|
|
|
|
|
// 采集服务按钮状态
|
|
|
const startLoading = ref(false)
|
|
|
const stopLoading = ref(false)
|
|
|
const refreshLoading = ref(false)
|
|
|
|
|
|
// 图表数据
|
|
|
const trendData = ref<DashboardTrendItem[]>([])
|
|
|
const programDistribution = ref<ProgramDistributionItem[]>([])
|
|
|
|
|
|
// 排行数据
|
|
|
const machineRank = ref<MachineRankRow[]>([])
|
|
|
const programRank = ref<ProgramRankItem[]>([])
|
|
|
const workerRank = ref<WorkerRankRow[]>([])
|
|
|
const workshopData = ref<WorkshopProduction[]>([])
|
|
|
|
|
|
// ==================== 日期筛选状态 ====================
|
|
|
|
|
|
// 产量趋势(近7天)— 无日期筛选
|
|
|
|
|
|
// NC程序产量分布日期
|
|
|
const programDistDateType = ref<DateType>('today')
|
|
|
const programDistDateRange = ref<[string, string]>()
|
|
|
|
|
|
// 机床排行日期
|
|
|
const machineDateType = ref<DateType>('today')
|
|
|
const machineDateRange = ref<[string, string]>()
|
|
|
|
|
|
// NC程序排行日期
|
|
|
const programRankDateType = ref<DateType>('today')
|
|
|
const programRankDateRange = ref<[string, string]>()
|
|
|
|
|
|
// 工人排行日期
|
|
|
const workerDateType = ref<DateType>('today')
|
|
|
const workerDateRange = ref<[string, string]>()
|
|
|
|
|
|
// 车间平均单机产量日期
|
|
|
const workshopDateType = ref<DateType>('today')
|
|
|
const workshopDateRange = ref<[string, string]>()
|
|
|
|
|
|
// ==================== TOP N 和排序 ====================
|
|
|
|
|
|
const machineTopN = ref(10)
|
|
|
const machineSortOrder = ref<'asc' | 'desc'>('desc')
|
|
|
|
|
|
const programRankTopN = ref(10)
|
|
|
|
|
|
const workerTopN = ref(10)
|
|
|
const workerSortOrder = ref<'asc' | 'desc'>('desc')
|
|
|
|
|
|
// ==================== 计算属性 ====================
|
|
|
|
|
|
const workshopDateLabel = computed(() => dateLabels[workshopDateType.value])
|
|
|
|
|
|
const workshopDays = computed(() => {
|
|
|
const { startDate, endDate } = getDateRange(workshopDateType.value, workshopDateRange.value)
|
|
|
return Math.max(1, Math.round((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000) + 1)
|
|
|
})
|
|
|
const workshopUnit = computed(() => workshopDays.value > 1 ? '件/台/天' : '件/台')
|
|
|
const workshopUnitLabel = computed(() => workshopDays.value > 1 ? '日均' : '平均')
|
|
|
|
|
|
// 采集服务状态标签
|
|
|
const collectorTagType = computed(() => {
|
|
|
const { status } = collectorStatus.value
|
|
|
if (status === 'running') return 'success'
|
|
|
if (status === 'paused') return 'info'
|
|
|
if (status === 'timeout') return 'warning'
|
|
|
if (status === 'stopped') return 'info'
|
|
|
if (status === 'not_installed') return 'danger'
|
|
|
return 'warning'
|
|
|
})
|
|
|
|
|
|
const collectorStatusText = computed(() => {
|
|
|
const { status } = collectorStatus.value
|
|
|
if (status === 'running') return '运行中'
|
|
|
if (status === 'paused') return '已暂停'
|
|
|
if (status === 'timeout') return '心跳超时'
|
|
|
if (status === 'stopped') return '已停止'
|
|
|
if (status === 'not_installed') return '未安装'
|
|
|
return status || '-'
|
|
|
})
|
|
|
|
|
|
// ==================== 工具函数 ====================
|
|
|
|
|
|
function formatUptime(seconds: number | undefined): string {
|
|
|
if (!seconds) return '-'
|
|
|
const days = Math.floor(seconds / 86400)
|
|
|
const hours = Math.floor((seconds % 86400) / 3600)
|
|
|
if (days > 0) return `${days}天${hours}时`
|
|
|
return `${hours}时`
|
|
|
}
|
|
|
|
|
|
function formatNumber(val: number | undefined | null): string {
|
|
|
if (val == null) return '-'
|
|
|
return Number(val).toFixed(2)
|
|
|
}
|
|
|
|
|
|
// ==================== 日期变更处理 ====================
|
|
|
|
|
|
function onWorkshopDateChange() {
|
|
|
workshopDateType.value !== 'custom' ? loadWorkshopData() : undefined
|
|
|
}
|
|
|
|
|
|
function onMachineDateChange() {
|
|
|
machineDateType.value !== 'custom' ? loadMachineRankData() : undefined
|
|
|
}
|
|
|
|
|
|
function onWorkerDateChange() {
|
|
|
workerDateType.value !== 'custom' ? loadWorkerRankData() : undefined
|
|
|
}
|
|
|
|
|
|
function onProgramDistDateChange() {
|
|
|
programDistDateType.value !== 'custom' ? loadProgramDistributionData() : undefined
|
|
|
}
|
|
|
|
|
|
function onProgramRankDateChange() {
|
|
|
programRankDateType.value !== 'custom' ? loadProgramRankData() : undefined
|
|
|
}
|
|
|
|
|
|
// ==================== 采集服务控制 ====================
|
|
|
|
|
|
async function startCollector() {
|
|
|
if (startLoading.value) return
|
|
|
startLoading.value = true
|
|
|
try {
|
|
|
if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'Running') {
|
|
|
ElMessage.info('采集服务已在运行中')
|
|
|
return
|
|
|
}
|
|
|
if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'NotInstalled') {
|
|
|
ElMessage.warning('采集服务未安装,请运行 install.ps1 安装脚本')
|
|
|
return
|
|
|
}
|
|
|
await request.post('/admin/collector/start')
|
|
|
ElMessage.success('采集服务已启动')
|
|
|
await loadData()
|
|
|
} catch {
|
|
|
/* request拦截器已显示错误 */
|
|
|
} finally {
|
|
|
startLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function stopCollector() {
|
|
|
if (stopLoading.value) return
|
|
|
stopLoading.value = true
|
|
|
try {
|
|
|
await request.post('/admin/collector/stop')
|
|
|
ElMessage.success('采集服务已停止')
|
|
|
await loadData()
|
|
|
} catch {
|
|
|
/* request拦截器已显示错误 */
|
|
|
} finally {
|
|
|
stopLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function refreshCollectorConfig() {
|
|
|
if (refreshLoading.value) return
|
|
|
refreshLoading.value = true
|
|
|
try {
|
|
|
await request.post('/admin/collector/refresh')
|
|
|
ElMessage.success('配置已刷新')
|
|
|
await loadData()
|
|
|
} catch {
|
|
|
/* request拦截器已显示错误 */
|
|
|
} finally {
|
|
|
refreshLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ==================== ECharts 实例 ====================
|
|
|
|
|
|
const trendChartRef = ref<HTMLElement>()
|
|
|
const programDistChartRef = ref<HTMLElement>()
|
|
|
const workshopChartRef = ref<HTMLElement>()
|
|
|
|
|
|
let trendChart: ECharts | null = null
|
|
|
let programDistChart: ECharts | null = null
|
|
|
let workshopChart: ECharts | null = null
|
|
|
|
|
|
// ==================== 图表初始化 ====================
|
|
|
|
|
|
function initTrendChart() {
|
|
|
if (trendChartRef.value && trendData.value.length) {
|
|
|
trendChart = echarts.init(trendChartRef.value)
|
|
|
trendChart.setOption({
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
grid: { left: 50, right: 20, top: 20, bottom: 30 },
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
data: trendData.value.map(i => i.date.slice(5)),
|
|
|
axisLabel: { fontSize: 12 },
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'value',
|
|
|
axisLabel: { fontSize: 12 },
|
|
|
},
|
|
|
series: [
|
|
|
{
|
|
|
type: 'line',
|
|
|
data: trendData.value.map(i => i.quantity),
|
|
|
smooth: true,
|
|
|
areaStyle: { opacity: 0.15 },
|
|
|
itemStyle: { color: '#409EFF' },
|
|
|
},
|
|
|
],
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function initProgramDistChart() {
|
|
|
if (programDistChartRef.value && programDistribution.value.length) {
|
|
|
programDistChart = echarts.init(programDistChartRef.value)
|
|
|
programDistChart.setOption({
|
|
|
tooltip: {
|
|
|
trigger: 'item',
|
|
|
formatter: (params: { name: string; value: number; percent: number }) =>
|
|
|
`${params.name}: ${params.value}件 (${params.percent}%)`,
|
|
|
},
|
|
|
legend: {
|
|
|
type: 'scroll',
|
|
|
bottom: 0,
|
|
|
itemWidth: 12,
|
|
|
itemHeight: 12,
|
|
|
textStyle: { fontSize: 12 },
|
|
|
},
|
|
|
series: [
|
|
|
{
|
|
|
type: 'pie',
|
|
|
radius: ['40%', '65%'],
|
|
|
center: ['50%', '45%'],
|
|
|
label: {
|
|
|
show: true,
|
|
|
formatter: '{b}: {c}件 ({d}%)',
|
|
|
fontSize: 12,
|
|
|
},
|
|
|
data: programDistribution.value.map(item => ({
|
|
|
value: item.totalQuantity,
|
|
|
name: item.programName,
|
|
|
})),
|
|
|
},
|
|
|
],
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function initWorkshopChart() {
|
|
|
if (workshopChartRef.value && workshopData.value.length) {
|
|
|
const unit = workshopUnit.value
|
|
|
const unitLabel = workshopUnitLabel.value
|
|
|
workshopChart = echarts.init(workshopChartRef.value)
|
|
|
workshopChart.setOption({
|
|
|
tooltip: {
|
|
|
trigger: 'axis',
|
|
|
formatter: (params: { name: string; value: number; dataIndex: number }[]) => {
|
|
|
const d = workshopData.value[params[0].dataIndex]
|
|
|
return `${d.workshopName}<br/>${unitLabel}产量: ${Number(params[0].value).toFixed(2)} ${unit}<br/>总产量: ${d.quantity} 件<br/>机床数: ${d.machineCount} 台`
|
|
|
},
|
|
|
},
|
|
|
grid: { left: 60, right: 20, top: 20, bottom: 30 },
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
data: workshopData.value.map(i => i.workshopName),
|
|
|
axisLabel: { fontSize: 12 },
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'value',
|
|
|
name: unit,
|
|
|
axisLabel: { fontSize: 12 },
|
|
|
},
|
|
|
series: [
|
|
|
{
|
|
|
type: 'bar',
|
|
|
data: workshopData.value.map(i => i.avgQuantity),
|
|
|
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] },
|
|
|
barWidth: '40%',
|
|
|
label: {
|
|
|
show: true,
|
|
|
position: 'top',
|
|
|
formatter: (p: { value: number }) => `${Number(p.value).toFixed(2)} ${unit}`,
|
|
|
fontSize: 12,
|
|
|
},
|
|
|
},
|
|
|
],
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function initCharts() {
|
|
|
initTrendChart()
|
|
|
initProgramDistChart()
|
|
|
// workshopChart 在 loadWorkshopData 中初始化
|
|
|
}
|
|
|
|
|
|
function disposeCharts() {
|
|
|
trendChart?.dispose()
|
|
|
programDistChart?.dispose()
|
|
|
workshopChart?.dispose()
|
|
|
trendChart = null
|
|
|
programDistChart = null
|
|
|
workshopChart = null
|
|
|
}
|
|
|
|
|
|
// ==================== 数据加载 ====================
|
|
|
|
|
|
async function loadProgramDistributionData() {
|
|
|
try {
|
|
|
const { startDate, endDate } = getDateRange(programDistDateType.value, programDistDateRange.value)
|
|
|
const res = await request.get<{ items: ProgramDistributionItem[] }>(
|
|
|
'/admin/dashboard/program-distribution',
|
|
|
{ params: { startDate, endDate } }
|
|
|
)
|
|
|
programDistribution.value = res.data?.items || []
|
|
|
programDistChart?.dispose()
|
|
|
programDistChart = null
|
|
|
await nextTick()
|
|
|
initProgramDistChart()
|
|
|
} catch {
|
|
|
/* 容错 */
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadProgramRankData() {
|
|
|
try {
|
|
|
const { startDate, endDate } = getDateRange(programRankDateType.value, programRankDateRange.value)
|
|
|
const res: ApiResponse<{ items: ProgramRankItem[] }> = await request.get(
|
|
|
'/admin/dashboard/program-rank',
|
|
|
{ params: { startDate, endDate, top: programRankTopN.value } }
|
|
|
)
|
|
|
programRank.value = res.data?.items || []
|
|
|
} catch {
|
|
|
/* 容错 */
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadMachineRankData() {
|
|
|
try {
|
|
|
const { startDate, endDate } = getDateRange(machineDateType.value, machineDateRange.value)
|
|
|
const res: ApiResponse<{ items: MachineRankRow[] }> = await request.get(
|
|
|
'/admin/dashboard/machine-rank',
|
|
|
{ params: { startDate, endDate, top: machineTopN.value, sortOrder: machineSortOrder.value } }
|
|
|
)
|
|
|
machineRank.value = res.data?.items || []
|
|
|
} catch {
|
|
|
/* 容错 */
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadWorkerRankData() {
|
|
|
try {
|
|
|
const { startDate, endDate } = getDateRange(workerDateType.value, workerDateRange.value)
|
|
|
const res: ApiResponse<{ items: WorkerRankRow[] }> = await request.get(
|
|
|
'/admin/dashboard/worker-rank',
|
|
|
{ params: { startDate, endDate, top: workerTopN.value, sortOrder: workerSortOrder.value } }
|
|
|
)
|
|
|
workerRank.value = res.data?.items || []
|
|
|
} catch {
|
|
|
/* 容错 */
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadWorkshopData() {
|
|
|
try {
|
|
|
const { startDate, endDate } = getDateRange(workshopDateType.value, workshopDateRange.value)
|
|
|
const res = await request.get<{ items: WorkshopProduction[] }>(
|
|
|
'/admin/dashboard/workshop-production',
|
|
|
{ params: { startDate, endDate } }
|
|
|
)
|
|
|
workshopData.value = res.data?.items || []
|
|
|
workshopChart?.dispose()
|
|
|
workshopChart = null
|
|
|
await nextTick()
|
|
|
initWorkshopChart()
|
|
|
} catch {
|
|
|
/* 容错 */
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadData() {
|
|
|
try {
|
|
|
// 并行加载:统计摘要 + 采集状态 + 产量趋势
|
|
|
const [summaryRes, trendRes] = await Promise.all([
|
|
|
request.get<DashboardSummary>('/admin/dashboard/summary'),
|
|
|
request.get<{ items: DashboardTrendItem[] }>('/admin/dashboard/trend'),
|
|
|
])
|
|
|
summary.value = summaryRes.data || summary.value
|
|
|
trendData.value = trendRes.data?.items || []
|
|
|
|
|
|
// 重置图表
|
|
|
disposeCharts()
|
|
|
await nextTick()
|
|
|
initCharts()
|
|
|
|
|
|
// 并行加载各区域独立数据
|
|
|
await Promise.all([
|
|
|
loadProgramDistributionData(),
|
|
|
loadWorkshopData(),
|
|
|
loadMachineRankData(),
|
|
|
loadWorkerRankData(),
|
|
|
loadProgramRankData(),
|
|
|
])
|
|
|
} catch {
|
|
|
// 保持容错
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ==================== 生命周期 ====================
|
|
|
|
|
|
onMounted(() => {
|
|
|
loadData()
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
disposeCharts()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.dashboard-page {
|
|
|
.stat-row {
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
.el-card {
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
.stat-card {
|
|
|
text-align: center;
|
|
|
padding: 10px 0;
|
|
|
|
|
|
.stat-label {
|
|
|
font-size: 14px;
|
|
|
color: #909399;
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
.info-icon {
|
|
|
margin-left: 2px;
|
|
|
font-size: 13px;
|
|
|
color: #c0c4cc;
|
|
|
cursor: help;
|
|
|
vertical-align: middle;
|
|
|
|
|
|
&:hover {
|
|
|
color: #409eff;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.stat-value {
|
|
|
font-size: 28px;
|
|
|
font-weight: bold;
|
|
|
color: #303133;
|
|
|
|
|
|
.stat-unit {
|
|
|
font-size: 14px;
|
|
|
font-weight: normal;
|
|
|
color: #909399;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.stat-sub {
|
|
|
font-size: 12px;
|
|
|
color: #909399;
|
|
|
margin-top: 4px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.collector-actions {
|
|
|
margin-top: 12px;
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
justify-content: flex-start;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.chart-row,
|
|
|
.rank-row {
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
.card-title {
|
|
|
font-size: 15px;
|
|
|
font-weight: 500;
|
|
|
|
|
|
.info-icon {
|
|
|
margin-left: 4px;
|
|
|
font-size: 14px;
|
|
|
color: #b0b5bd;
|
|
|
cursor: help;
|
|
|
|
|
|
&:hover {
|
|
|
color: #409eff;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.card-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 8px;
|
|
|
|
|
|
.card-sub {
|
|
|
font-size: 12px;
|
|
|
color: #909399;
|
|
|
}
|
|
|
|
|
|
.date-filter {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.machine-link {
|
|
|
color: var(--el-color-primary);
|
|
|
cursor: pointer;
|
|
|
text-decoration: none;
|
|
|
}
|
|
|
}
|
|
|
</style>
|