feat(dashboard): 车间产量/机床排行/工人排行增加独立日期筛选(今日/昨日/近3天/近7天/自定义)

main
haoliang 1 week ago
parent e663bbd388
commit 8ce0c11e0f

@ -1,4 +1,45 @@
import type { MockMethod } from './types' import type { MockMethod, MockRequest } from './types'
// 日期工具
function getToday(): string { return new Date().toISOString().slice(0, 10) }
function daysAgo(n: number): string { const d = new Date(); d.setDate(d.getDate() - n); return d.toISOString().slice(0, 10) }
// 不同日期范围的数据倍率(模拟不同日期的数据差异)
function getMultiplier(startDate: string, endDate: string): number {
const today = getToday()
if (startDate === today && endDate === today) return 1.0 // 今日
if (startDate === daysAgo(1) && endDate === daysAgo(1)) return 0.92 // 昨日
if (startDate === daysAgo(2) && endDate === today) return 0.95 // 近3天
if (startDate === daysAgo(6) && endDate === today) return 0.93 // 近7天
return 0.88 // 自定义
}
const baseMachineRank = [
{ rank: 1, machineId: 1, machineName: '西-1.8', program: '1566.NC', quantity: 580, status: 1 },
{ rank: 2, machineId: 2, machineName: '西-1.10', program: 'O123.NC', quantity: 420, status: 1 },
{ rank: 3, machineId: 3, machineName: '东-2.0', program: 'A456.NC', quantity: 380, status: 1 },
{ rank: 4, machineId: 4, machineName: '东-2.5', program: 'B789.NC', quantity: 310, status: 0 },
{ rank: 5, machineId: 5, machineName: '南-3.1', program: 'C012.NC', quantity: 290, status: 1 },
{ rank: 6, machineId: 6, machineName: '南-3.2', program: 'D345.NC', quantity: 240, status: 1 },
{ rank: 7, machineId: 7, machineName: '北-4.0', program: 'E678.NC', quantity: 210, status: 1 },
{ rank: 8, machineId: 8, machineName: '北-4.1', program: 'F901.NC', quantity: 180, status: 0 },
{ rank: 9, machineId: 9, machineName: '西-1.5', program: 'G234.NC', quantity: 150, status: 1 },
{ rank: 10, machineId: 10, machineName: '东-2.8', program: 'H567.NC', quantity: 87, status: 1 },
]
const baseWorkerRank = [
{ rank: 1, workerId: 1, workerName: '张三', machineCount: 3, totalQuantity: 1240 },
{ rank: 2, workerId: 2, workerName: '李四', machineCount: 2, totalQuantity: 980 },
{ rank: 3, workerId: 3, workerName: '王五', machineCount: 4, totalQuantity: 870 },
{ rank: 4, workerId: 4, workerName: '赵六', machineCount: 2, totalQuantity: 650 },
{ rank: 5, workerId: 5, workerName: '孙七', machineCount: 3, totalQuantity: 520 },
]
const baseWorkshopProduction = [
{ workshopName: 'A栋', quantity: 1280, machineCount: 80, avgQuantity: 16.0 },
{ workshopName: 'B栋', quantity: 860, machineCount: 45, avgQuantity: 19.1 },
{ workshopName: 'C栋', quantity: 707, machineCount: 35, avgQuantity: 20.2 },
]
const mock: MockMethod[] = [ const mock: MockMethod[] = [
{ {
@ -35,38 +76,27 @@ const mock: MockMethod[] = [
{ {
url: '/mock-api/admin/dashboard/machine-rank', url: '/mock-api/admin/dashboard/machine-rank',
method: 'get', method: 'get',
response: { response: (req: MockRequest) => {
code: 0, const m = getMultiplier(req.query.startDate, req.query.endDate)
data: { return {
items: [ code: 0,
{ rank: 1, machineId: 1, machineName: '西-1.8', program: '1566.NC', quantity: 580, status: 1 }, data: {
{ rank: 2, machineId: 2, machineName: '西-1.10', program: 'O123.NC', quantity: 420, status: 1 }, items: baseMachineRank.map(r => ({ ...r, quantity: Math.round(r.quantity * m) })),
{ rank: 3, machineId: 3, machineName: '东-2.0', program: 'A456.NC', quantity: 380, status: 1 }, },
{ rank: 4, machineId: 4, machineName: '东-2.5', program: 'B789.NC', quantity: 310, status: 0 }, }
{ rank: 5, machineId: 5, machineName: '南-3.1', program: 'C012.NC', quantity: 290, status: 1 },
{ rank: 6, machineId: 6, machineName: '南-3.2', program: 'D345.NC', quantity: 240, status: 1 },
{ rank: 7, machineId: 7, machineName: '北-4.0', program: 'E678.NC', quantity: 210, status: 1 },
{ rank: 8, machineId: 8, machineName: '北-4.1', program: 'F901.NC', quantity: 180, status: 0 },
{ rank: 9, machineId: 9, machineName: '西-1.5', program: 'G234.NC', quantity: 150, status: 1 },
{ rank: 10, machineId: 10, machineName: '东-2.8', program: 'H567.NC', quantity: 87, status: 1 },
],
},
}, },
}, },
{ {
url: '/mock-api/admin/dashboard/worker-rank', url: '/mock-api/admin/dashboard/worker-rank',
method: 'get', method: 'get',
response: { response: (req: MockRequest) => {
code: 0, const m = getMultiplier(req.query.startDate, req.query.endDate)
data: { return {
items: [ code: 0,
{ rank: 1, workerId: 1, workerName: '张三', machineCount: 3, totalQuantity: 1240 }, data: {
{ rank: 2, workerId: 2, workerName: '李四', machineCount: 2, totalQuantity: 980 }, items: baseWorkerRank.map(r => ({ ...r, totalQuantity: Math.round(r.totalQuantity * m) })),
{ rank: 3, workerId: 3, workerName: '王五', machineCount: 4, totalQuantity: 870 }, },
{ rank: 4, workerId: 4, workerName: '赵六', machineCount: 2, totalQuantity: 650 }, }
{ rank: 5, workerId: 5, workerName: '孙七', machineCount: 3, totalQuantity: 520 },
],
},
}, },
}, },
// ===== 新增:产量趋势(近7天) ===== // ===== 新增:产量趋势(近7天) =====
@ -92,15 +122,18 @@ const mock: MockMethod[] = [
{ {
url: '/mock-api/admin/dashboard/workshop-production', url: '/mock-api/admin/dashboard/workshop-production',
method: 'get', method: 'get',
response: { response: (req: MockRequest) => {
code: 0, const m = getMultiplier(req.query.startDate, req.query.endDate)
data: { return {
items: [ code: 0,
{ workshopName: 'A栋', quantity: 1280, machineCount: 80, avgQuantity: 16.0 }, data: {
{ workshopName: 'B栋', quantity: 860, machineCount: 45, avgQuantity: 19.1 }, items: baseWorkshopProduction.map(w => ({
{ workshopName: 'C栋', quantity: 707, machineCount: 35, avgQuantity: 20.2 }, ...w,
], quantity: Math.round(w.quantity * m),
}, avgQuantity: Math.round(w.avgQuantity * m * 10) / 10,
})),
},
}
}, },
}, },
// ===== 新增:机床状态分布 ===== // ===== 新增:机床状态分布 =====

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

Loading…
Cancel
Save