实现员工产量子页面 WorkerProduction.vue:筛选栏(日期+员工下拉)、4统计卡片、双图表(柱状图+饼图)、明细表格,编译通过

main
haoliang 1 month ago
parent 3eeb3cb30c
commit 31b331467c

@ -1,16 +1,440 @@
<template>
<div class="worker-production">
<h2>员工产量</h2>
<p>员工维度产量统计页面待实现</p>
<!-- 筛选栏 -->
<el-form :inline="true" :model="filters" class="filter-bar">
<el-form-item label="日期范围">
<el-date-picker
v-model="filters.dateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="员工">
<el-select
v-model="filters.workerId"
placeholder="请选择员工"
clearable
filterable
style="min-width: 220px"
>
<el-option
v-for="w in workerOptions"
:key="w.id"
:label="w.name"
:value="w.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"></el-button>
<el-button @click="handleReset"></el-button>
</el-form-item>
</el-form>
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-row">
<el-col :span="6">
<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.totalQuantity?.toLocaleString() ?? '-' }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<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.activeWorkerCount ?? '-' }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<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">{{ formatNumber(summary.avgPerWorker) }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<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 name-value">{{ summary.topWorkerName || '-' }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表行 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<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>
</el-card>
</el-col>
</el-row>
<!-- 明细表格 -->
<el-row :gutter="16" class="table-row">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span class="card-title">员工产量明细</span>
</template>
<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 prop="totalQuantity" label="总产量" width="100" align="center" />
<el-table-column label="占比" width="100" align="center">
<template #default="{ row }">
{{ row.percentage?.toFixed(1) }}%
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
// -
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import request from '@/utils/request'
import echarts from '@/utils/echarts'
import type { ECharts } from 'echarts/core'
// ==================== ====================
/** 员工产量汇总 */
interface WorkerProductionSummary {
totalQuantity: number
activeWorkerCount: number
avgPerWorker: number
topWorkerName: string
}
/** 员工产量明细行 */
interface WorkerProductionItem {
rank: number
workerName: string
machineCount: number
programCount: number
totalQuantity: number
percentage: number
}
/** 员工下拉选项 */
interface WorkerOption {
id: number | string
name: string
}
// ==================== ====================
/** 筛选条件 */
const filters = reactive({
dateRange: ['', ''] as string[],
workerId: '' as string | number,
})
/** 员工下拉选项 */
const workerOptions = ref<WorkerOption[]>([])
/** 汇总数据 */
const summary = ref<WorkerProductionSummary>({
totalQuantity: 0,
activeWorkerCount: 0,
avgPerWorker: 0,
topWorkerName: '',
})
/** 图表/表格明细数据 */
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() {
if (!barChartRef.value || tableData.value.length === 0) return
barChart?.dispose()
barChart = echarts.init(barChartRef.value)
barChart.setOption({
tooltip: {
trigger: 'axis',
formatter: (params: { name: string; value: number }[]) => {
const p = params[0]
return `${p.name}<br/>产量: ${p.value}`
},
},
grid: { left: 60, right: 20, top: 20, bottom: 40 },
xAxis: {
type: 'category',
data: tableData.value.map((i) => i.workerName),
axisLabel: { fontSize: 12, rotate: tableData.value.length > 8 ? 30 : 0 },
},
yAxis: {
type: 'value',
name: '产量(件)',
axisLabel: { fontSize: 12 },
},
series: [
{
type: 'bar',
data: tableData.value.map((i) => i.totalQuantity),
itemStyle: {
color: '#67C23A',
borderRadius: [4, 4, 0, 0],
},
barWidth: '50%',
label: {
show: true,
position: 'top',
fontSize: 12,
},
},
],
})
}
/** 初始化饼图 */
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 formatNumber(val: number | undefined | null): string {
if (val == null) return '-'
return Number(val).toFixed(2)
}
/** 获取今天的日期字符串 */
function getToday(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
// ==================== ====================
/** 加载员工下拉选项 */
async function loadWorkerOptions() {
try {
const res = await request.get<{ items: WorkerOption[] }>('/admin/worker/list')
workerOptions.value = res.data?.items ?? []
} catch {
//
}
}
/** 加载统计数据 */
async function loadData() {
const startDate = filters.dateRange[0] || getToday()
const endDate = filters.dateRange[1] || getToday()
const params: Record<string, string | number> = { startDate, endDate }
if (filters.workerId) {
params.workerId = filters.workerId
}
try {
//
const [summaryRes, listRes] = await Promise.all([
request.get<WorkerProductionSummary>('/admin/production/worker/summary', { params }),
request.get<{ items: WorkerProductionItem[] }>('/admin/production/worker/list', { params }),
])
summary.value = summaryRes.data ?? {
totalQuantity: 0,
activeWorkerCount: 0,
avgPerWorker: 0,
topWorkerName: '',
}
tableData.value = listRes.data?.items ?? []
//
disposeCharts()
await initCharts()
} catch {
//
}
}
// ==================== ====================
/** 查询按钮 */
function handleQuery() {
loadData()
}
/** 重置按钮 */
function handleReset() {
const today = getToday()
filters.dateRange = [today, today]
filters.workerId = ''
loadData()
}
// ==================== ====================
onMounted(async () => {
const today = getToday()
filters.dateRange = [today, today]
await loadWorkerOptions()
await loadData()
})
onUnmounted(() => {
disposeCharts()
})
</script>
<style scoped>
<style scoped lang="scss">
.worker-production {
padding: 20px;
.filter-bar {
margin-bottom: 16px;
}
.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;
&.name-value {
font-size: 22px;
}
}
}
}
.chart-row {
margin-bottom: 16px;
}
.table-row {
.el-card {
width: 100%;
}
}
.card-title {
font-size: 15px;
font-weight: 500;
}
}
</style>

Loading…
Cancel
Save