产量报表前端:删饼图+柱状图铺满百分比标签+跨页面联动跳转+URL参数初始化

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
main
haoliang 1 month ago
parent be8d8c323e
commit 507283850c

@ -24,11 +24,12 @@
</el-form-item>
<el-form-item label="机床">
<el-select
v-model="filters.machineId"
v-model="filters.machineIds"
value-key="value"
placeholder="请选择机床"
clearable
filterable
multiple
style="min-width:200px"
>
<el-option
@ -111,20 +112,12 @@
<!-- 图表行 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">机床产量柱状图</span>
</template>
<div ref="barChartRef" style="height: 300px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span class="card-title">各机床产量占比</span>
</template>
<div ref="pieChartRef" style="height: 300px"></div>
<div ref="barChartRef" style="height: 350px"></div>
</el-card>
</el-col>
</el-row>
@ -137,7 +130,11 @@
<el-table :data="tableData" stripe size="small" v-loading="loading">
<el-table-column prop="rank" label="排名" width="60" align="center" />
<el-table-column prop="machineName" label="机床名称" width="120" />
<el-table-column prop="programName" label="程序名" show-overflow-tooltip />
<el-table-column prop="programName" label="程序名" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="goToProgram(row)">{{ row.programName }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalQuantity" label="产量" width="100" align="center" />
<el-table-column label="运行时间" width="100" align="center">
<template #default="{ row }">{{ row.runTime }}h</template>
@ -150,6 +147,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import request from '@/utils/request'
import echarts from '@/utils/echarts'
import type { ECharts } from 'echarts/core'
@ -179,6 +177,10 @@ interface DropdownOption {
label: string
}
// ---------- ----------
const router = useRouter()
const route = useRoute()
// ---------- ----------
const loading = ref(false)
const tableData = ref<MachineProductionItem[]>([])
@ -187,7 +189,7 @@ const summary = ref<Partial<MachineProductionSummary>>({})
const filters = reactive({
dateRange: [] as string[],
workshopId: '' as string | number,
machineId: '' as string | number,
machineIds: [] as string[],
})
const options = reactive({
@ -197,9 +199,7 @@ const options = reactive({
// ---------- ECharts ----------
const barChartRef = ref<HTMLElement>()
const pieChartRef = ref<HTMLElement>()
let barChart: ECharts | null = null
let pieChart: ECharts | null = null
/** 初始化柱状图 */
function initBarChart() {
@ -208,6 +208,7 @@ function initBarChart() {
barChart = echarts.init(barChartRef.value)
const names = tableData.value.map((i) => i.machineName)
const quantities = tableData.value.map((i) => i.totalQuantity)
const total = tableData.value.reduce((sum, i) => sum + i.totalQuantity, 0)
barChart.setOption({
tooltip: {
trigger: 'axis',
@ -216,7 +217,7 @@ function initBarChart() {
return `${item.name}<br/>产量: ${item.value}`
},
},
grid: { left: 50, right: 20, top: 20, bottom: 40 },
grid: { left: 50, right: 20, top: 30, bottom: 40 },
xAxis: {
type: 'category',
data: names,
@ -232,38 +233,15 @@ function initBarChart() {
borderRadius: [4, 4, 0, 0],
},
barWidth: '50%',
},
],
})
}
/** 初始化饼图 */
function initPieChart() {
if (!pieChartRef.value || !tableData.value.length) return
if (pieChart) pieChart.dispose()
pieChart = echarts.init(pieChartRef.value)
const pieData = tableData.value.map((i) => ({
value: i.totalQuantity,
name: i.machineName,
}))
pieChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{b}: {c}件 ({d}%)',
},
series: [
{
type: 'pie',
radius: ['40%', '65%'],
center: ['50%', '50%'],
label: {
show: true,
formatter: '{b}: {c}件 ({d}%)',
fontSize: 12,
},
data: pieData,
emphasis: {
label: { fontSize: 14, fontWeight: 'bold' },
position: 'top',
formatter: (params: { value: number }) => {
if (total === 0) return ''
const pct = ((params.value / total) * 100).toFixed(1)
return `${params.value}件 (${pct}%)`
},
fontSize: 11,
},
},
],
@ -273,9 +251,22 @@ function initPieChart() {
/** 销毁所有图表 */
function disposeCharts() {
barChart?.dispose()
pieChart?.dispose()
barChart = null
pieChart = null
}
// ---------- ----------
/** 点击程序名跳转到程序产量页面 */
function goToProgram(row: MachineProductionItem) {
const startDate = filters.dateRange[0] ?? ''
const endDate = filters.dateRange[1] ?? ''
router.push({
path: '/admin/production/program',
query: {
startDate,
endDate,
programNames: row.programName,
},
})
}
// ---------- ----------
@ -306,7 +297,7 @@ async function loadData() {
startDate: filters.dateRange[0] ?? '',
endDate: filters.dateRange[1] ?? '',
workshopId: filters.workshopId ?? '',
machineId: filters.machineId ?? '',
machineIds: filters.machineIds.join(','),
}
const [summaryRes, listRes] = await Promise.all([
@ -327,7 +318,6 @@ async function loadData() {
disposeCharts()
await nextTick()
initBarChart()
initPieChart()
} catch {
// API catch
} finally {
@ -341,16 +331,23 @@ function resetFilters() {
const todayStr = fmtDate(today)
filters.dateRange = [todayStr, todayStr]
filters.workshopId = ''
filters.machineId = ''
filters.machineIds = []
loadData()
}
// ---------- ----------
onMounted(async () => {
//
const today = new Date()
const todayStr = fmtDate(today)
filters.dateRange = [todayStr, todayStr]
// URL query
if (route.query.startDate) {
filters.dateRange = [route.query.startDate as string, (route.query.endDate as string) || (route.query.startDate as string)]
} else {
const today = new Date()
const todayStr = fmtDate(today)
filters.dateRange = [todayStr, todayStr]
}
if (route.query.machineIds) {
filters.machineIds = (route.query.machineIds as string).split(',').filter(Boolean)
}
await loadOptions()
loadData()

@ -13,7 +13,10 @@
/>
</el-form-item>
<el-form-item label="程序名">
<el-input v-model="filters.programName" placeholder="输入程序名" style="width: 240px" clearable />
<el-tooltip content="支持多个程序名,用逗号隔开(中英文逗号均可)" placement="top">
<span class="info-icon" style="margin-right:4px; cursor:help;"></span>
</el-tooltip>
<el-input v-model="filters.programName" placeholder="输入程序名,多个用逗号分隔" style="width: 380px" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData"></el-button>
@ -79,7 +82,7 @@
<!-- 图表行 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">
@ -89,20 +92,7 @@
</el-tooltip>
</span>
</template>
<div ref="barChartRef" style="height: 320px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span class="card-title">
程序产量饼图
<el-tooltip content="各NC程序产量占比分布。" placement="top">
<span class="info-icon"></span>
</el-tooltip>
</span>
</template>
<div ref="pieChartRef" style="height: 320px"></div>
<div ref="barChartRef" style="height: 350px"></div>
</el-card>
</el-col>
</el-row>
@ -120,7 +110,11 @@
<el-table :data="tableData" stripe size="small" v-loading="loading">
<el-table-column prop="rank" label="排名" width="60" align="center" />
<el-table-column prop="programName" label="程序名" show-overflow-tooltip />
<el-table-column prop="machineCount" label="涉及机床数" width="100" align="center" />
<el-table-column label="涉及机床数" width="100" align="center">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="goToMachine(row)">{{ row.machineCount }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalQuantity" label="总产量" width="100" align="center" />
<el-table-column label="平均单机" width="100" align="center">
<template #default="{ row }">{{ row.avgPerMachine?.toFixed(1) ?? '-' }}</template>
@ -135,6 +129,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import request from '@/utils/request'
import echarts from '@/utils/echarts'
import type { ECharts } from 'echarts/core'
@ -155,8 +150,13 @@ interface ProgramListItem {
totalQuantity: number
avgPerMachine: number
percentage: number
machineIds: string
}
//
const router = useRouter()
const route = useRoute()
//
const filters = reactive({
dateRange: [] as string[],
@ -170,9 +170,7 @@ const tableData = ref<ProgramListItem[]>([])
// ECharts
const barChartRef = ref<HTMLElement>()
const pieChartRef = ref<HTMLElement>()
let barChart: ECharts | null = null
let pieChart: ECharts | null = null
/** 初始化柱状图 */
function initBarChart() {
@ -181,6 +179,7 @@ function initBarChart() {
barChart = echarts.init(barChartRef.value)
const names = tableData.value.map(item => item.programName)
const quantities = tableData.value.map(item => item.totalQuantity)
const total = tableData.value.reduce((sum, item) => sum + item.totalQuantity, 0)
barChart.setOption({
tooltip: {
trigger: 'axis',
@ -189,7 +188,7 @@ function initBarChart() {
return `${p.name}<br/>产量: ${p.value}`
}
},
grid: { left: 50, right: 20, top: 20, bottom: 40 },
grid: { left: 50, right: 20, top: 30, bottom: 40 },
xAxis: {
type: 'category',
data: names,
@ -203,37 +202,16 @@ function initBarChart() {
itemStyle: {
color: '#E6A23C',
borderRadius: [4, 4, 0, 0]
}
}]
})
}
/** 初始化饼图 */
function initPieChart() {
if (!pieChartRef.value || !tableData.value.length) return
pieChart?.dispose()
pieChart = echarts.init(pieChartRef.value)
const pieData = tableData.value.map(item => ({
value: item.totalQuantity,
name: item.programName
}))
pieChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{b}: {c}件 ({d}%)'
},
series: [{
type: 'pie',
radius: ['40%', '65%'],
center: ['50%', '50%'],
},
label: {
show: true,
formatter: '{b}: {c}件 ({d}%)',
position: 'top',
formatter: (params: { value: number }) => {
if (total === 0) return ''
const pct = ((params.value / total) * 100).toFixed(1)
return `${params.value}件 (${pct}%)`
},
fontSize: 11
},
data: pieData,
emphasis: {
label: { fontSize: 14, fontWeight: 'bold' }
}
}]
})
@ -242,11 +220,27 @@ function initPieChart() {
/** 销毁图表 */
function disposeCharts() {
barChart?.dispose()
pieChart?.dispose()
barChart = null
pieChart = null
}
// ==================== ====================
/** 点击涉及机床数 → 跳转机床产量页面 */
function goToMachine(row: ProgramListItem) {
const startDate = filters.dateRange[0] ?? ''
const endDate = filters.dateRange[1] ?? ''
router.push({
path: '/admin/production/machine',
query: {
startDate,
endDate,
machineIds: row.machineIds,
},
})
}
// ==================== ====================
/** 加载数据 */
async function loadData() {
loading.value = true
@ -254,7 +248,7 @@ async function loadData() {
const params = {
startDate: filters.dateRange?.[0] ?? '',
endDate: filters.dateRange?.[1] ?? '',
programName: filters.programName ?? ''
programNames: filters.programName ?? ''
}
const [summaryRes, listRes] = await Promise.all([
request.get<ProgramSummary>('/admin/production/program/summary', { params }),
@ -267,7 +261,6 @@ async function loadData() {
disposeCharts()
await nextTick()
initBarChart()
initPieChart()
} finally {
loading.value = false
}
@ -290,7 +283,22 @@ function init() {
loadData()
}
onMounted(init)
onMounted(() => {
// URL query
if (route.query.startDate) {
filters.dateRange = [route.query.startDate as string, (route.query.endDate as string) || (route.query.startDate as string)]
} else {
const today = new Date()
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
filters.dateRange = [fmt(today), fmt(today)]
}
if (route.query.programNames) {
filters.programName = route.query.programNames as string
}
loadData()
})
onUnmounted(disposeCharts)
</script>

@ -93,20 +93,12 @@
<!-- 图表行 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">员工产量柱状图</span>
</template>
<div ref="barChartRef" style="height: 300px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span class="card-title">员工产量占比</span>
</template>
<div ref="pieChartRef" style="height: 300px"></div>
<div ref="barChartRef" style="height: 350px"></div>
</el-card>
</el-col>
</el-row>
@ -121,8 +113,16 @@
<el-table :data="tableData" 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 prop="programCount" label="涉及程序数" width="100" align="center" />
<el-table-column label="绑定机床数" width="100" align="center">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="goToMachine(row)">{{ row.machineCount }}</el-link>
</template>
</el-table-column>
<el-table-column label="涉及程序数" width="100" align="center">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="goToProgram(row)">{{ row.programCount }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalQuantity" label="总产量" width="100" align="center" />
<el-table-column label="占比" width="100" align="center">
<template #default="{ row }">
@ -138,6 +138,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import request from '@/utils/request'
import echarts from '@/utils/echarts'
import type { ECharts } from 'echarts/core'
@ -160,6 +161,8 @@ interface WorkerProductionItem {
programCount: number
totalQuantity: number
percentage: number
machineIds: string
programNames: string
}
/** 员工下拉选项 */
@ -168,6 +171,10 @@ interface WorkerOption {
label: string
}
// ==================== ====================
const router = useRouter()
const route = useRoute()
// ==================== ====================
/** 筛选条件 */
@ -193,9 +200,7 @@ const tableData = ref<WorkerProductionItem[]>([])
// ==================== ECharts ====================
const barChartRef = ref<HTMLElement>()
const pieChartRef = ref<HTMLElement>()
let barChart: ECharts | null = null
let pieChart: ECharts | null = null
/** 初始化柱状图 */
function initBarChart() {
@ -210,7 +215,7 @@ function initBarChart() {
return `${p.name}<br/>产量: ${p.value}`
},
},
grid: { left: 60, right: 20, top: 20, bottom: 40 },
grid: { left: 60, right: 20, top: 30, bottom: 40 },
xAxis: {
type: 'category',
data: tableData.value.map((i) => i.workerName),
@ -240,54 +245,40 @@ function initBarChart() {
})
}
/** 初始化饼图 */
function initPieChart() {
if (!pieChartRef.value || tableData.value.length === 0) return
pieChart?.dispose()
pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{b}: {c}件 ({d}%)',
},
legend: {
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: tableData.value.map((i) => ({
value: i.totalQuantity,
name: i.workerName,
})),
},
],
})
}
/** 初始化所有图表 */
async function initCharts() {
await nextTick()
initBarChart()
initPieChart()
}
/** 销毁图表实例 */
function disposeCharts() {
barChart?.dispose()
pieChart?.dispose()
barChart = null
pieChart = null
}
// ==================== ====================
/** 点击绑定机床数 → 跳转机床产量页面 */
function goToMachine(row: WorkerProductionItem) {
const startDate = filters.dateRange[0] || ''
const endDate = filters.dateRange[1] || ''
router.push({
path: '/admin/production/machine',
query: {
startDate,
endDate,
machineIds: row.machineIds,
},
})
}
/** 点击涉及程序数 → 跳转程序产量页面 */
function goToProgram(row: WorkerProductionItem) {
const startDate = filters.dateRange[0] || ''
const endDate = filters.dateRange[1] || ''
router.push({
path: '/admin/production/program',
query: {
startDate,
endDate,
programNames: row.programNames,
},
})
}
// ==================== ====================
@ -341,7 +332,8 @@ async function loadData() {
//
disposeCharts()
await initCharts()
await nextTick()
initBarChart()
} catch {
//
}
@ -365,8 +357,14 @@ function handleReset() {
// ==================== ====================
onMounted(async () => {
const today = getToday()
filters.dateRange = [today, today]
// URL query
if (route.query.startDate) {
filters.dateRange = [route.query.startDate as string, (route.query.endDate as string) || (route.query.startDate as string)]
} else {
const today = getToday()
filters.dateRange = [today, today]
}
await loadWorkerOptions()
await loadData()
})

Loading…
Cancel
Save