|
|
|
@ -134,7 +134,21 @@
|
|
|
|
</el-col>
|
|
|
|
</el-col>
|
|
|
|
<el-col :span="12">
|
|
|
|
<el-col :span="12">
|
|
|
|
<el-card shadow="hover">
|
|
|
|
<el-card shadow="hover">
|
|
|
|
<template #header><span class="card-title">车间平均单机产量(今日)<el-tooltip content="各车间今日总产量除以机床数量,反映每台机床的平均产出效率,消除车间规模差异。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span></template>
|
|
|
|
<template #header>
|
|
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
|
|
<span class="card-title">车间平均单机产量({{ workshopDateLabel }})<el-tooltip content="各车间总产量除以机床数量,反映每台机床的平均产出效率,消除车间规模差异。" placement="top"><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>
|
|
|
|
<div ref="workshopChartRef" style="height: 260px"></div>
|
|
|
|
</el-card>
|
|
|
|
</el-card>
|
|
|
|
</el-col>
|
|
|
|
</el-col>
|
|
|
|
@ -181,8 +195,17 @@
|
|
|
|
<el-card shadow="hover">
|
|
|
|
<el-card shadow="hover">
|
|
|
|
<template #header>
|
|
|
|
<template #header>
|
|
|
|
<div class="card-header">
|
|
|
|
<div class="card-header">
|
|
|
|
<span class="card-title">机床产量排行 TOP10<el-tooltip content="今日单台机床加工零件数由高到低排列,同时显示当前正在执行的程序名。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span>
|
|
|
|
<span class="card-title">机床产量排行 TOP10<el-tooltip content="单台机床加工零件数由高到低排列,同时显示当前正在执行的程序名。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span>
|
|
|
|
<span class="card-sub">今日</span>
|
|
|
|
<div class="date-filter">
|
|
|
|
|
|
|
|
<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>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
<el-table :data="machineRank" stripe size="small" style="width: 100%">
|
|
|
|
<el-table :data="machineRank" stripe size="small" style="width: 100%">
|
|
|
|
@ -206,8 +229,17 @@
|
|
|
|
<el-card shadow="hover">
|
|
|
|
<el-card shadow="hover">
|
|
|
|
<template #header>
|
|
|
|
<template #header>
|
|
|
|
<div class="card-header">
|
|
|
|
<div class="card-header">
|
|
|
|
<span class="card-title">工人产量排行 TOP10<el-tooltip content="今日工人所绑定的所有机床产量合计,由高到低排列。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span>
|
|
|
|
<span class="card-title">工人产量排行 TOP10<el-tooltip content="工人所绑定的所有机床产量合计,由高到低排列。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span>
|
|
|
|
<span class="card-sub">今日</span>
|
|
|
|
<div class="date-filter">
|
|
|
|
|
|
|
|
<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>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
<el-table :data="workerRank" stripe size="small" style="width: 100%">
|
|
|
|
<el-table :data="workerRank" stripe size="small" style="width: 100%">
|
|
|
|
@ -223,7 +255,7 @@
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
<script setup lang="ts">
|
|
|
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
|
import request from '@/utils/request'
|
|
|
|
import request from '@/utils/request'
|
|
|
|
import { useMockMode } from '@/composables/useMockMode'
|
|
|
|
import { useMockMode } from '@/composables/useMockMode'
|
|
|
|
import echarts from '@/utils/echarts'
|
|
|
|
import echarts from '@/utils/echarts'
|
|
|
|
@ -245,6 +277,37 @@ const startLoading = ref(false)
|
|
|
|
const stopLoading = ref(false)
|
|
|
|
const stopLoading = ref(false)
|
|
|
|
const refreshLoading = ref(false)
|
|
|
|
const refreshLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 日期筛选状态
|
|
|
|
|
|
|
|
type DateType = 'today' | 'yesterday' | 'last3' | 'last7' | 'custom'
|
|
|
|
|
|
|
|
const workshopDateType = ref<DateType>('today')
|
|
|
|
|
|
|
|
const workshopDateRange = ref<[string, string]>()
|
|
|
|
|
|
|
|
const machineDateType = ref<DateType>('today')
|
|
|
|
|
|
|
|
const machineDateRange = ref<[string, string]>()
|
|
|
|
|
|
|
|
const workerDateType = ref<DateType>('today')
|
|
|
|
|
|
|
|
const workerDateRange = ref<[string, string]>()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dateLabels: Record<DateType, string> = { today: '今日', yesterday: '昨日', last3: '近3天', last7: '近7天', custom: '自定义' }
|
|
|
|
|
|
|
|
const workshopDateLabel = computed(() => dateLabels[workshopDateType.value])
|
|
|
|
|
|
|
|
const machineDateLabel = computed(() => dateLabels[machineDateType.value])
|
|
|
|
|
|
|
|
const workerDateLabel = computed(() => dateLabels[workerDateType.value])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getDateRange(type: DateType, customRange?: [string, string]): { startDate: string; endDate: string } {
|
|
|
|
|
|
|
|
const today = new Date()
|
|
|
|
|
|
|
|
const fmt = (d: Date) => d.toISOString().slice(0, 10)
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function onWorkshopDateChange() { workshopDateType.value !== 'custom' ? loadWorkshopData() : undefined }
|
|
|
|
|
|
|
|
function onMachineDateChange() { machineDateType.value !== 'custom' ? loadMachineRankData() : undefined }
|
|
|
|
|
|
|
|
function onWorkerDateChange() { workerDateType.value !== 'custom' ? loadWorkerRankData() : undefined }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ECharts refs
|
|
|
|
// ECharts refs
|
|
|
|
const trendChartRef = ref<HTMLElement>()
|
|
|
|
const trendChartRef = ref<HTMLElement>()
|
|
|
|
@ -290,23 +353,7 @@ function alertTypeLabel(type: string): string {
|
|
|
|
return map[type] || type
|
|
|
|
return map[type] || type
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initCharts() {
|
|
|
|
function initWorkshopChart() {
|
|
|
|
// 产量趋势折线图
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
|
|
|
}],
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 车间平均单机产量柱状图
|
|
|
|
|
|
|
|
if (workshopChartRef.value && workshopData.value.length) {
|
|
|
|
if (workshopChartRef.value && workshopData.value.length) {
|
|
|
|
workshopChart = echarts.init(workshopChartRef.value)
|
|
|
|
workshopChart = echarts.init(workshopChartRef.value)
|
|
|
|
workshopChart.setOption({
|
|
|
|
workshopChart.setOption({
|
|
|
|
@ -327,6 +374,26 @@ function initCharts() {
|
|
|
|
}],
|
|
|
|
}],
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initCharts() {
|
|
|
|
|
|
|
|
// 产量趋势折线图
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
|
|
|
}],
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 车间平均单机产量柱状图
|
|
|
|
|
|
|
|
initWorkshopChart()
|
|
|
|
|
|
|
|
|
|
|
|
// 机床状态饼图
|
|
|
|
// 机床状态饼图
|
|
|
|
if (statusPieRef.value) {
|
|
|
|
if (statusPieRef.value) {
|
|
|
|
@ -357,35 +424,58 @@ function disposeCharts() {
|
|
|
|
statusPie = null
|
|
|
|
statusPie = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadWorkshopData() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const { startDate, endDate } = getDateRange(workshopDateType.value, workshopDateRange.value)
|
|
|
|
|
|
|
|
const res: ApiResponse<{ items: WorkshopProduction }> = await request.get('/admin/dashboard/workshop-production', { params: { startDate, endDate } })
|
|
|
|
|
|
|
|
workshopData.value = res.data?.items || []
|
|
|
|
|
|
|
|
workshopChart?.dispose(); workshopChart = null
|
|
|
|
|
|
|
|
await nextTick(); initWorkshopChart()
|
|
|
|
|
|
|
|
} 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 } })
|
|
|
|
|
|
|
|
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 } })
|
|
|
|
|
|
|
|
workerRank.value = res.data?.items || []
|
|
|
|
|
|
|
|
} catch { /* */ }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadData() {
|
|
|
|
async function loadData() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const [summaryRes, collectorRes, machineRankRes, workerRankRes, trendRes, workshopRes, statusRes, alertsRes]: [
|
|
|
|
const [summaryRes, collectorRes, trendRes, statusRes, alertsRes]: [
|
|
|
|
ApiResponse<DashboardSummary>, ApiResponse<CollectorStatus>, ApiResponse<{ items: MachineRankRow[] }>,
|
|
|
|
ApiResponse<DashboardSummary>, ApiResponse<CollectorStatus>,
|
|
|
|
ApiResponse<{ items: WorkerRankRow }>, ApiResponse<{ items: DashboardTrendItem[] }>,
|
|
|
|
ApiResponse<{ items: DashboardTrendItem[] }>,
|
|
|
|
ApiResponse<{ items: WorkshopProduction }>, ApiResponse<MachineStatusDistribution>,
|
|
|
|
ApiResponse<MachineStatusDistribution>,
|
|
|
|
ApiResponse<{ items: RecentAlert[] }>
|
|
|
|
ApiResponse<{ items: RecentAlert[] }>
|
|
|
|
] = await Promise.all([
|
|
|
|
] = await Promise.all([
|
|
|
|
request.get('/admin/dashboard/summary'),
|
|
|
|
request.get('/admin/dashboard/summary'),
|
|
|
|
request.get('/admin/collector/status'),
|
|
|
|
request.get('/admin/collector/status'),
|
|
|
|
request.get('/admin/dashboard/machine-rank'),
|
|
|
|
|
|
|
|
request.get('/admin/dashboard/worker-rank'),
|
|
|
|
|
|
|
|
request.get('/admin/dashboard/trend'),
|
|
|
|
request.get('/admin/dashboard/trend'),
|
|
|
|
request.get('/admin/dashboard/workshop-production'),
|
|
|
|
|
|
|
|
request.get('/admin/dashboard/machine-status-distribution'),
|
|
|
|
request.get('/admin/dashboard/machine-status-distribution'),
|
|
|
|
request.get('/admin/dashboard/recent-alerts'),
|
|
|
|
request.get('/admin/dashboard/recent-alerts'),
|
|
|
|
])
|
|
|
|
])
|
|
|
|
summary.value = summaryRes.data || summary.value
|
|
|
|
summary.value = summaryRes.data || summary.value
|
|
|
|
collectorStatus.value = collectorRes.data || collectorStatus.value
|
|
|
|
collectorStatus.value = collectorRes.data || collectorStatus.value
|
|
|
|
machineRank.value = machineRankRes.data?.items || []
|
|
|
|
|
|
|
|
workerRank.value = workerRankRes.data?.items || []
|
|
|
|
|
|
|
|
trendData.value = trendRes.data?.items || []
|
|
|
|
trendData.value = trendRes.data?.items || []
|
|
|
|
workshopData.value = workshopRes.data?.items || []
|
|
|
|
|
|
|
|
statusDist.value = statusRes.data || statusDist.value
|
|
|
|
statusDist.value = statusRes.data || statusDist.value
|
|
|
|
recentAlerts.value = alertsRes.data?.items || []
|
|
|
|
recentAlerts.value = alertsRes.data?.items || []
|
|
|
|
|
|
|
|
|
|
|
|
disposeCharts()
|
|
|
|
disposeCharts()
|
|
|
|
await nextTick()
|
|
|
|
await nextTick()
|
|
|
|
initCharts()
|
|
|
|
initCharts()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 日期筛选的三个区域独立加载
|
|
|
|
|
|
|
|
await Promise.all([loadWorkshopData(), loadMachineRankData(), loadWorkerRankData()])
|
|
|
|
} catch {
|
|
|
|
} catch {
|
|
|
|
// 保持容错
|
|
|
|
// 保持容错
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -488,11 +578,19 @@ onUnmounted(() => {
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
justify-content: space-between;
|
|
|
|
align-items: center;
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.card-sub {
|
|
|
|
.card-sub {
|
|
|
|
font-size: 12px;
|
|
|
|
font-size: 12px;
|
|
|
|
color: #909399;
|
|
|
|
color: #909399;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.date-filter {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.machine-link {
|
|
|
|
.machine-link {
|
|
|
|
|