first commit

This commit is contained in:
2026-03-05 16:52:12 +08:00
commit 8ca2e6d52f
1899 changed files with 321565 additions and 0 deletions

View File

@@ -0,0 +1,908 @@
<template>
<div class="fullscreen-container" ref="containerRef">
<!-- 顶部标题栏 -->
<div class="dashboard-header">
<div class="header-left">
<div class="logo-area">
<span class="logo-icon">📊</span>
</div>
<div class="title-area">
<h1>甜菊糖采集监控系统</h1>
<p class="subtitle">Real-time Data Acquisition Monitoring System</p>
</div>
</div>
<div class="header-center">
<div class="datetime-display">
<span class="date">{{ currentDate }}</span>
<span class="time">{{ currentTime }}</span>
</div>
</div>
<div class="header-right">
<div class="status-panel">
<div class="status-item">
<span class="status-dot" :class="{ online: isConnected }"></span>
<span class="status-text">{{ isConnected ? '系统在线' : '连接断开' }}</span>
</div>
<el-button class="exit-btn" @click="exitFullscreen">
<span class="btn-icon"></span>
退出大屏
</el-button>
</div>
</div>
</div>
<!-- 主体内容区 -->
<div class="dashboard-body">
<!-- 设备监控卡片区 -->
<div class="equipment-section">
<!-- 进料泵 -->
<div class="equipment-card" :class="{ active: feedPump.status === 'running' }">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon">🔄</div>
<div class="card-title">
<h3>进料泵</h3>
<span class="status-tag" :class="feedPump.status">
{{ feedPump.status === 'running' ? '● 运行中' : '○ 停止' }}
</span>
</div>
</div>
<div class="card-body">
<div class="data-item">
<span class="data-label">设定液位</span>
<div class="data-value">
<span class="number">{{ feedPump.setLevel }}</span>
<span class="unit"></span>
</div>
</div>
<div class="data-item">
<span class="data-label">控制模式</span>
<span class="mode-text" :class="{ auto: feedPump.isAuto }">
{{ feedPump.isAuto ? '自动' : '手动' }}
</span>
</div>
<div class="data-item">
<span class="data-label">频率设置</span>
<div class="data-value">
<span class="number">{{ feedPump.frequency }}</span>
<span class="unit">%</span>
</div>
</div>
<div class="data-item highlight">
<span class="data-label">蒸发液位</span>
<div class="data-value realtime">
<span class="number glow">{{ feedPump.evaporationLevel }}</span>
<span class="unit"></span>
</div>
</div>
</div>
</div>
<!-- 蒸汽阀 -->
<div class="equipment-card" :class="{ active: steamValve.status === 'running' }">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon">🌡</div>
<div class="card-title">
<h3>蒸汽阀</h3>
<span class="status-tag" :class="steamValve.status">
{{ steamValve.status === 'running' ? '● 运行中' : '○ 停止' }}
</span>
</div>
</div>
<div class="card-body">
<div class="data-item">
<span class="data-label">设定温度</span>
<div class="data-value">
<span class="number">{{ steamValve.setTemperature }}</span>
<span class="unit">°C</span>
</div>
</div>
<div class="data-item">
<span class="data-label">控制模式</span>
<span class="mode-text" :class="{ auto: steamValve.isAuto }">
{{ steamValve.isAuto ? '自动' : '手动' }}
</span>
</div>
<div class="data-item">
<span class="data-label">开度设置</span>
<div class="data-value">
<span class="number">{{ steamValve.opening }}</span>
<span class="unit">%</span>
</div>
</div>
<div class="data-item highlight">
<span class="data-label">酒精浓度</span>
<div class="data-value realtime">
<span class="number glow">{{ steamValve.alcoholConcentration }}</span>
<span class="unit">%</span>
</div>
</div>
</div>
</div>
<!-- 回料阀 -->
<div class="equipment-card" :class="{ active: returnValve.status === 'running' }">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon">🔁</div>
<div class="card-title">
<h3>回料阀</h3>
<span class="status-tag" :class="returnValve.status">
{{ returnValve.status === 'running' ? '● 运行中' : '○ 停止' }}
</span>
</div>
</div>
<div class="card-body">
<div class="data-item">
<span class="data-label">设定流量</span>
<div class="data-value">
<span class="number">{{ returnValve.setFlow }}</span>
<span class="unit"></span>
</div>
</div>
<div class="data-item">
<span class="data-label">控制模式</span>
<span class="mode-text" :class="{ auto: returnValve.isAuto }">
{{ returnValve.isAuto ? '自动' : '手动' }}
</span>
</div>
<div class="data-item">
<span class="data-label">开度设置</span>
<div class="data-value">
<span class="number">{{ returnValve.opening }}</span>
<span class="unit">%</span>
</div>
</div>
<div class="data-item highlight">
<span class="data-label">回料流量</span>
<div class="data-value realtime">
<span class="number glow">{{ returnValve.actualFlow }}</span>
<span class="unit"></span>
</div>
</div>
</div>
</div>
<!-- 浓液 -->
<div class="equipment-card" :class="{ active: concentrate.status === 'running' }">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon">💧</div>
<div class="card-title">
<h3>浓液系统</h3>
<span class="status-tag" :class="concentrate.status">
{{ concentrate.status === 'running' ? '● 运行中' : '○ 停止' }}
</span>
</div>
</div>
<div class="card-body">
<div class="data-item">
<span class="data-label">设定流量</span>
<div class="data-value">
<span class="number">{{ concentrate.setFlow }}</span>
<span class="unit"></span>
</div>
</div>
<div class="data-item">
<span class="data-label">控制模式</span>
<span class="mode-text" :class="{ auto: concentrate.isAuto }">
{{ concentrate.isAuto ? '自动' : '手动' }}
</span>
</div>
<div class="data-item">
<span class="data-label">开度设置</span>
<div class="data-value">
<span class="number">{{ concentrate.opening }}</span>
<span class="unit">%</span>
</div>
</div>
<div class="data-item highlight">
<span class="data-label">浓液流量</span>
<div class="data-value realtime">
<span class="number glow">{{ concentrate.actualFlow }}</span>
<span class="unit">L/min</span>
</div>
</div>
</div>
</div>
<!-- RO系统 -->
<div class="equipment-card ro-card" :class="{ active: roSystem.status === 'running' }">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon"></div>
<div class="card-title">
<h3>RO智能控制系统</h3>
<span class="status-tag" :class="roSystem.status">
{{ roSystem.status === 'running' ? '● 运行中' : '○ 停止' }}
</span>
</div>
</div>
<div class="card-body ro-body">
<div class="data-item highlight">
<span class="data-label">进水流量</span>
<div class="data-value realtime">
<span class="number glow">{{ roSystem.inletFlow }}</span>
<span class="unit">/h</span>
</div>
</div>
<div class="data-item highlight">
<span class="data-label">产水流量</span>
<div class="data-value realtime">
<span class="number glow">{{ roSystem.productFlow }}</span>
<span class="unit">/h</span>
</div>
</div>
<div class="data-item highlight">
<span class="data-label">回收率</span>
<div class="data-value realtime">
<span class="number glow accent">{{ roSystem.recoveryRate }}</span>
<span class="unit">%</span>
</div>
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<div class="chart-panel">
<div class="panel-header">
<span class="panel-icon">📈</span>
<h3>液位监控趋势</h3>
</div>
<div class="panel-body">
<Echart :height="280" :options="levelChartOption" />
</div>
</div>
<div class="chart-panel">
<div class="panel-header">
<span class="panel-icon">📊</span>
<h3>流量监控趋势</h3>
</div>
<div class="panel-body">
<Echart :height="280" :options="flowChartOption" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { Echart } from '@/components/Echart'
import * as CollectApi from '@/api/mes/collect'
import type { EChartsOption, SeriesOption } from 'echarts'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
defineOptions({ name: 'MesCollectFullscreen' })
const containerRef = ref<HTMLElement>()
// 响应式数据
const isConnected = ref(true)
// 时间显示
const currentDate = ref('')
const currentTime = ref('')
let clockTimer: ReturnType<typeof setInterval> | null = null
let dataUpdateTimer: ReturnType<typeof setInterval> | null = null
// 更新时间
const updateClock = () => {
const now = dayjs()
currentDate.value = now.format('YYYY年MM月DD日 dddd')
currentTime.value = now.format('HH:mm:ss')
}
// 设备数据
const feedPump = ref({
status: 'stopped',
setLevel: 0,
isAuto: false,
frequency: 0,
evaporationLevel: 0
})
const steamValve = ref({
status: 'stopped',
setTemperature: 0,
isAuto: false,
opening: 0,
alcoholConcentration: 0
})
const returnValve = ref({
status: 'stopped',
setFlow: 0,
isAuto: false,
opening: 0,
actualFlow: 0
})
const concentrate = ref({
status: 'stopped',
setFlow: 0,
isAuto: false,
opening: 0,
actualFlow: 0
})
const roSystem = ref({
status: 'stopped',
inletFlow: 0,
productFlow: 0,
recoveryRate: 0
})
// 图表配置 - 深色主题
const baseChartOption = (): EChartsOption => ({
backgroundColor: 'transparent',
grid: { left: 50, right: 20, bottom: 30, top: 50, containLabel: true },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', lineStyle: { color: 'rgba(0, 212, 255, 0.5)' } },
backgroundColor: 'rgba(0, 20, 40, 0.9)',
borderColor: 'rgba(0, 212, 255, 0.3)',
textStyle: { color: '#fff' }
},
legend: {
top: 10,
data: [] as string[],
textStyle: { color: 'rgba(255, 255, 255, 0.8)' }
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [] as string[],
axisLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.3)' } },
axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 10 },
splitLine: { show: false }
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.3)' } },
axisLabel: { color: 'rgba(255, 255, 255, 0.6)' },
splitLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.1)' } }
},
series: [] as SeriesOption[]
})
const levelChartOption = reactive(baseChartOption()) as EChartsOption
const flowChartOption = reactive(baseChartOption()) as EChartsOption
// 获取最新设备数据
const fetchLatestData = async () => {
try {
const data = await CollectApi.getLatestEquipmentData()
if (data) {
feedPump.value = {
status: data.feedPumpStatus || 'stopped',
setLevel: data.feedPumpSetLevel ?? 0,
isAuto: data.feedPumpIsAuto ?? false,
frequency: data.feedPumpFrequency ?? 0,
evaporationLevel: data.feedPumpEvaporationLevel ?? 0
}
steamValve.value = {
status: data.steamValveStatus || 'stopped',
setTemperature: data.steamValveSetTemperature ?? 0,
isAuto: data.steamValveIsAuto ?? false,
opening: data.steamValveOpening ?? 0,
alcoholConcentration: data.steamValveAlcoholConcentration ?? 0
}
returnValve.value = {
status: data.returnValveStatus || 'stopped',
setFlow: data.returnValveSetFlow ?? 0,
isAuto: data.returnValveIsAuto ?? false,
opening: data.returnValveOpening ?? 0,
actualFlow: data.returnValveActualFlow ?? 0
}
concentrate.value = {
status: data.concentrateStatus || 'stopped',
setFlow: data.concentrateSetFlow ?? 0,
isAuto: data.concentrateIsAuto ?? false,
opening: data.concentrateOpening ?? 0,
actualFlow: data.concentrateActualFlow ?? 0
}
roSystem.value = {
status: data.roSystemStatus || 'stopped',
inletFlow: data.roSystemInletFlow ?? 0,
productFlow: data.roSystemProductFlow ?? 0,
recoveryRate: data.roSystemRecoveryRate ?? 0
}
isConnected.value = true
}
} catch (error) {
console.error('获取设备数据失败:', error)
isConnected.value = false
}
}
// 获取趋势数据
const fetchTrendData = async () => {
try {
const params = {
beginTime: dayjs().subtract(1, 'hour').format('YYYY-MM-DD HH:mm:ss'),
endTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
const trend = await CollectApi.getTrendData(params)
if (trend) {
const times = trend.times || []
const toNums = (arr: any[]) => (arr || []).map((v) => (v === null || v === undefined ? null : Number(v)))
const commonStyle = { smooth: true, connectNulls: true, showSymbol: false, areaStyle: { opacity: 0.1 } }
const levelOption = levelChartOption as any
levelOption.xAxis.data = times
levelOption.legend.data = ['蒸发液位']
levelOption.series = [
{ name: '蒸发液位', type: 'line', ...commonStyle, data: toNums(trend.feedPumpEvaporationLevel), itemStyle: { color: '#00d4ff' }, areaStyle: { color: 'rgba(0, 212, 255, 0.2)' } }
]
const flowOption = flowChartOption as any
flowOption.xAxis.data = times
flowOption.legend.data = ['回料流量', '浓液流量', '进水流量', '产水流量']
flowOption.series = [
{ name: '回料流量', type: 'line', ...commonStyle, data: toNums(trend.returnValveActualFlow), itemStyle: { color: '#00ff88' } },
{ name: '浓液流量', type: 'line', ...commonStyle, data: toNums(trend.concentrateActualFlow), itemStyle: { color: '#ffaa00' } },
{ name: '进水流量', type: 'line', ...commonStyle, data: toNums(trend.roSystemInletFlow), itemStyle: { color: '#ff6b6b' } },
{ name: '产水流量', type: 'line', ...commonStyle, data: toNums(trend.roSystemProductFlow), itemStyle: { color: '#a855f7' } }
]
}
} catch (error) {
console.error('获取趋势数据失败:', error)
}
}
// 退出全屏
const exitFullscreen = () => {
window.close()
}
// 进入浏览器全屏
const enterBrowserFullscreen = () => {
const el = containerRef.value
if (el) {
if (el.requestFullscreen) {
el.requestFullscreen()
}
}
}
// 生命周期
onMounted(() => {
// 启动时钟
updateClock()
clockTimer = setInterval(updateClock, 1000)
// 获取数据
fetchLatestData()
fetchTrendData()
// 定时刷新数据
dataUpdateTimer = setInterval(() => {
fetchLatestData()
fetchTrendData()
}, 300000)
// 自动进入全屏
setTimeout(enterBrowserFullscreen, 500)
})
onUnmounted(() => {
if (clockTimer) {
clearInterval(clockTimer)
clockTimer = null
}
if (dataUpdateTimer) {
clearInterval(dataUpdateTimer)
dataUpdateTimer = null
}
})
</script>
<style scoped>
/* ==================== 全屏大屏样式 ==================== */
.fullscreen-container {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #0a1628 0%, #1a2a4a 50%, #0d1f3c 100%);
color: #fff;
font-family: 'Microsoft YaHei', sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ==================== 顶部标题栏 ==================== */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 30px;
background: linear-gradient(90deg, rgba(0, 50, 100, 0.8) 0%, rgba(0, 100, 150, 0.6) 50%, rgba(0, 50, 100, 0.8) 100%);
border-bottom: 2px solid;
border-image: linear-gradient(90deg, transparent, #00d4ff, transparent) 1;
position: relative;
flex-shrink: 0;
}
.dashboard-header::before {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.5), transparent);
filter: blur(2px);
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.logo-icon {
font-size: 36px;
filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.5));
}
.title-area h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
background: linear-gradient(90deg, #fff, #00d4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 30px rgba(0, 212, 255, 0.3);
}
.subtitle {
margin: 4px 0 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 2px;
}
.header-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.datetime-display {
text-align: center;
}
.datetime-display .date {
display: block;
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 4px;
}
.datetime-display .time {
display: block;
font-size: 36px;
font-weight: 700;
color: #00d4ff;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
font-family: 'Consolas', monospace;
}
.header-right {
display: flex;
align-items: center;
}
.status-panel {
display: flex;
align-items: center;
gap: 20px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ff4757;
box-shadow: 0 0 10px #ff4757;
animation: statusBlink 2s infinite;
}
.status-dot.online {
background: #00ff88;
box-shadow: 0 0 10px #00ff88;
}
@keyframes statusBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
}
.exit-btn {
padding: 8px 20px;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
border: 1px solid rgba(255, 107, 107, 0.5);
border-radius: 20px;
color: #fff;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.exit-btn:hover {
background: linear-gradient(135deg, #ff8080 0%, #ff6b6b 100%);
box-shadow: 0 0 20px rgba(255, 107, 107, 0.5);
}
.btn-icon {
font-size: 12px;
}
/* ==================== 主体内容区 ==================== */
.dashboard-body {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
overflow: hidden;
}
/* ==================== 设备卡片区 ==================== */
.equipment-section {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 15px;
flex-shrink: 0;
}
.equipment-card {
position: relative;
background: linear-gradient(180deg, rgba(0, 40, 80, 0.8) 0%, rgba(0, 20, 50, 0.9) 100%);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 10px;
overflow: hidden;
transition: all 0.3s;
}
.equipment-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.5), transparent);
}
.equipment-card.active {
border-color: rgba(0, 255, 136, 0.4);
box-shadow: 0 0 20px rgba(0, 255, 136, 0.2);
}
.equipment-card.active::before {
background: linear-gradient(90deg, transparent, #00ff88, transparent);
}
.card-glow {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0, 212, 255, 0.1) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.equipment-card:hover .card-glow {
opacity: 1;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 15px;
background: rgba(0, 212, 255, 0.1);
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
}
.card-icon {
font-size: 24px;
filter: drop-shadow(0 0 5px rgba(0, 212, 255, 0.5));
}
.card-title h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #fff;
}
.status-tag {
font-size: 11px;
margin-top: 2px;
display: block;
}
.status-tag.running {
color: #00ff88;
}
.status-tag.stopped {
color: #ff6b6b;
}
.card-body {
padding: 10px 15px;
}
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.data-item:last-child {
border-bottom: none;
}
.data-item.highlight {
background: rgba(0, 212, 255, 0.05);
margin: 0 -15px;
padding: 8px 15px;
border-radius: 5px;
}
.data-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.data-value {
display: flex;
align-items: baseline;
gap: 4px;
}
.data-value .number {
font-size: 18px;
font-weight: 700;
color: #fff;
font-family: 'Consolas', monospace;
}
.data-value .unit {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
}
.data-value.realtime .number {
color: #00d4ff;
}
.data-value.realtime .number.glow {
text-shadow: 0 0 10px rgba(0, 212, 255, 0.8);
animation: dataGlow 2s infinite;
}
.data-value.realtime .number.accent {
color: #00ff88;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.8);
}
@keyframes dataGlow {
0%, 100% { opacity: 1; text-shadow: 0 0 10px rgba(0, 212, 255, 0.8); }
50% { opacity: 0.8; text-shadow: 0 0 20px rgba(0, 212, 255, 1); }
}
.mode-text {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
}
.mode-text.auto {
background: rgba(0, 212, 255, 0.2);
color: #00d4ff;
}
.ro-card {
grid-column: span 1;
}
.ro-body {
display: flex;
flex-direction: column;
gap: 5px;
}
/* ==================== 图表区域 ==================== */
.charts-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
flex: 1;
min-height: 0;
}
.chart-panel {
background: linear-gradient(180deg, rgba(0, 40, 80, 0.8) 0%, rgba(0, 20, 50, 0.9) 100%);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 10px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
background: rgba(0, 212, 255, 0.1);
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
flex-shrink: 0;
}
.panel-icon {
font-size: 18px;
}
.panel-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #00d4ff;
}
.panel-body {
flex: 1;
padding: 10px;
min-height: 0;
}
/* ==================== 响应式 ==================== */
@media (max-width: 1600px) {
.equipment-section {
grid-template-columns: repeat(5, 1fr);
}
}
@media (max-width: 1200px) {
.equipment-section {
grid-template-columns: repeat(3, 1fr);
}
.charts-section {
grid-template-columns: 1fr;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
<template>
<Dialog title="能源消耗记录审核" v-model="dialogVisible" :width="600">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<!-- 基本信息展示 -->
<el-form-item label="编号">
<span>{{ recordData.code }}</span>
</el-form-item>
<el-form-item label="工单编号">
<span>{{ recordData.workOrderCode }}</span>
</el-form-item>
<el-form-item label="消耗时间">
<span>{{ formatDate(recordData.consumptionTime) }}</span>
</el-form-item>
<!-- 消耗量信息 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="煤消耗量">
<span>{{ recordData.coalConsumption }} </span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="蒸汽产量">
<span>{{ recordData.steamOutput }} </span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用电量">
<span>{{ recordData.electricityConsumption }} 千瓦时</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用水量">
<span>{{ recordData.waterConsumption }} 立方米</span>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">审核信息</el-divider>
<!-- 审核表单 -->
<el-form-item label="审核结果" prop="auditStatus">
<el-radio-group v-model="formData.auditStatus">
<el-radio :label="1">审核通过</el-radio>
<el-radio :label="2">审核拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" prop="auditRemark">
<el-input
v-model="formData.auditRemark"
type="textarea"
placeholder="请输入审核意见"
:rows="3"
/>
</el-form-item>
<!-- 煤消耗提醒 -->
<el-alert
v-if="formData.auditStatus === 1 && recordData.coalConsumption > 0"
title="提醒"
type="warning"
:closable="false"
show-icon
>
<template #default>
审核通过后将自动扣减煤库存 {{ recordData.coalConsumption }} 请确认库存充足
</template>
</el-alert>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { formatDate } from '@/utils/formatTime'
import { auditEnergyConsumption } from '@/api/mes/energy/consumption'
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const recordData = ref({
id: undefined,
code: '',
workOrderCode: '',
consumptionTime: '',
coalConsumption: 0,
steamOutput: 0,
electricityConsumption: 0,
waterConsumption: 0
})
const formData = ref({
id: undefined,
auditStatus: 1,
auditRemark: ''
})
const formRules = reactive({
auditStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }]
})
const formRef = ref()
/** 打开弹窗 */
const open = async (row: any) => {
dialogVisible.value = true
recordData.value = { ...row }
formData.value = {
id: row.id,
auditStatus: 1,
auditRemark: ''
}
formRef.value?.resetFields()
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
const valid = await formRef.value.validate()
if (!valid) return
formLoading.value = true
try {
await auditEnergyConsumption(formData.value)
message.success('审核成功')
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,97 @@
<template>
<Dialog title="能源消耗记录详情" v-model="dialogVisible" :width="800">
<el-descriptions :column="2" border v-loading="loading">
<el-descriptions-item label="编号">
{{ detailData.code || '-' }}
</el-descriptions-item>
<el-descriptions-item label="工单编号">
{{ detailData.workOrderCode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="消耗时间">
{{ formatDate(detailData.consumptionTime) || '-' }}
</el-descriptions-item>
<el-descriptions-item label="审核状态">
<dict-tag :type="DICT_TYPE.MES_ENERGY_AUDIT_STATUS" :value="detailData.auditStatus" />
</el-descriptions-item>
<el-descriptions-item label="煤消耗量">
{{ detailData.coalConsumption }}
</el-descriptions-item>
<el-descriptions-item label="蒸汽产量">
{{ detailData.steamOutput }}
</el-descriptions-item>
<el-descriptions-item label="用电量">
{{ detailData.electricityConsumption }} 千瓦时
</el-descriptions-item>
<el-descriptions-item label="用水量">
{{ detailData.waterConsumption }} 立方米
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ detailData.remark || '-' }}
</el-descriptions-item>
<!-- 审核信息 -->
<template v-if="detailData.auditStatus !== 0">
<el-descriptions-item label="审核人">
{{ detailData.auditorName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="审核时间">
{{ formatDate(detailData.auditTime) || '-' }}
</el-descriptions-item>
<el-descriptions-item label="审核意见" :span="2">
{{ detailData.auditRemark || '-' }}
</el-descriptions-item>
</template>
<el-descriptions-item label="创建时间">
{{ formatDate(detailData.createTime) || '-' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(detailData.updateTime) || '-' }}
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import { getEnergyConsumption } from '@/api/mes/energy/consumption'
const dialogVisible = ref(false)
const loading = ref(false)
const detailData = ref({
id: undefined,
code: '',
workOrderCode: '',
consumptionTime: '',
coalConsumption: 0,
steamOutput: 0,
electricityConsumption: 0,
waterConsumption: 0,
auditStatus: 0,
auditorName: '',
auditTime: '',
auditRemark: '',
remark: '',
createTime: '',
updateTime: ''
})
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
loading.value = true
try {
const data = await getEnergyConsumption(id)
detailData.value = data
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,293 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" :width="800">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="工单编号" prop="workOrderCode">
<el-select
v-model="formData.workOrderCode"
placeholder="请选择工单编号"
filterable
allow-create
default-first-option
:disabled="formType === 'view'"
@change="handleWorkOrderChange"
class="w-full"
>
<el-option
v-for="item in workOrderOptions"
:key="item.id"
:label="`${item.code} - ${item.productName}`"
:value="item.code"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="消耗时间" prop="consumptionTime">
<el-date-picker
v-model="formData.consumptionTime"
type="datetime"
placeholder="选择消耗时间"
:disabled="formType === 'view'"
value-format="YYYY-MM-DDTHH:mm:ss"
class="w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="煤消耗量" prop="coalConsumption">
<div class="flex items-center gap-2">
<el-input-number
v-model="formData.coalConsumption"
:min="0"
:step="0.01"
:precision="2"
:disabled="formType === 'view'"
class="flex-1"
/>
<span class="text-gray-500"></span>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="蒸汽产量" prop="steamOutput">
<div class="flex items-center gap-2">
<el-input-number
v-model="formData.steamOutput"
:min="0"
:step="0.01"
:precision="2"
:disabled="formType === 'view'"
class="flex-1"
/>
<span class="text-gray-500"></span>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用电量" prop="electricityConsumption">
<div class="flex items-center gap-2">
<el-input-number
v-model="formData.electricityConsumption"
:min="0"
:step="0.1"
:precision="1"
:disabled="formType === 'view'"
class="flex-1"
/>
<span class="text-gray-500">千瓦时</span>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用水量" prop="waterConsumption">
<div class="flex items-center gap-2">
<el-input-number
v-model="formData.waterConsumption"
:min="0"
:step="0.1"
:precision="1"
:disabled="formType === 'view'"
class="flex-1"
/>
<span class="text-gray-500">立方米</span>
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入备注"
:disabled="formType === 'view'"
:rows="3"
/>
</el-form-item>
<!-- 审核信息显示仅在查看模式下显示 -->
<template v-if="formType === 'view' && formData.auditStatus !== 0">
<el-divider content-position="left">审核信息</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="审核状态">
<dict-tag :type="DICT_TYPE.MES_ENERGY_AUDIT_STATUS" :value="formData.auditStatus" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="审核人">
<span>{{ formData.auditorName || '-' }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="审核时间">
<span>{{ formatDate(formData.auditTime) || '-' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审核意见">
<span>{{ formData.auditRemark || '-' }}</span>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="formType !== 'view'">
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import {
getEnergyConsumption,
createEnergyConsumption,
updateEnergyConsumption
} from '@/api/mes/energy/consumption'
import { YVHgetWorkOrderSimpleList } from '@/api/mes/production/workorder'
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref({
id: undefined,
code: '',
workOrderId: undefined,
workOrderCode: '',
consumptionTime: '',
coalConsumption: undefined,
steamOutput: undefined,
electricityConsumption: undefined,
waterConsumption: undefined,
remark: '',
auditStatus: 0,
auditorName: '',
auditTime: '',
auditRemark: ''
})
const formRules = reactive({
workOrderCode: [{ required: true, message: '工单编号不能为空', trigger: 'blur' }],
consumptionTime: [{ required: true, message: '消耗时间不能为空', trigger: 'blur' }],
coalConsumption: [{ required: true, message: '煤消耗量不能为空', trigger: 'blur' }],
steamOutput: [{ required: true, message: '蒸汽产量不能为空', trigger: 'blur' }],
electricityConsumption: [{ required: true, message: '用电量不能为空', trigger: 'blur' }],
waterConsumption: [{ required: true, message: '用水量不能为空', trigger: 'blur' }]
})
const formRef = ref()
const workOrderOptions = ref([])
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'create' ? '添加能源消耗记录' : type === 'update' ? '修改能源消耗记录' : '查看能源消耗记录'
formType.value = type
resetForm()
// 获取工单列表
await getWorkOrderList()
if (id) {
formLoading.value = true
try {
const data = await getEnergyConsumption(id)
Object.assign(formData.value, data)
} finally {
formLoading.value = false
}
} else {
// 新增时设置默认消耗时间为当前时间
formData.value.consumptionTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
}
}
/** 获取工单列表 */
const getWorkOrderList = async () => {
try {
const data = await YVHgetWorkOrderSimpleList({ status: 4 }) // 只获取已完成的工单
workOrderOptions.value = Array.isArray(data) ? data : []
} catch (error) {
console.error('获取工单列表失败', error)
message.error('获取工单列表失败')
}
}
/** 工单选择变更处理 */
const handleWorkOrderChange = (value: string) => {
const selectedWorkOrder = workOrderOptions.value.find((item: any) => item.code === value)
if (selectedWorkOrder) {
formData.value.workOrderId = selectedWorkOrder.id
formData.value.code = selectedWorkOrder.code
} else {
formData.value.workOrderId = undefined
formData.value.code = value
}
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
const valid = await formRef.value.validate()
if (!valid) return
formLoading.value = true
try {
const data = { ...formData.value }
if (formType.value === 'create') {
await createEnergyConsumption(data)
message.success(t('common.createSuccess'))
} else {
await updateEnergyConsumption(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
code: '',
workOrderId: undefined,
workOrderCode: '',
consumptionTime: '',
coalConsumption: undefined,
steamOutput: undefined,
electricityConsumption: undefined,
waterConsumption: undefined,
remark: '',
auditStatus: 0,
auditorName: '',
auditTime: '',
auditRemark: ''
}
formRef.value?.resetFields()
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,247 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="编号" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工单编号" prop="workOrderCode">
<el-input
v-model="queryParams.workOrderCode"
placeholder="请输入工单编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="审核状态" prop="auditStatus">
<el-select
v-model="queryParams.auditStatus"
placeholder="请选择审核状态"
clearable
class="!w-240px"
>
<el-option label="待审核" :value="0" />
<el-option label="已审核" :value="1" />
<el-option label="审核拒绝" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="消耗时间" prop="consumptionTime">
<el-date-picker
v-model="queryParams.consumptionTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:energy-consumption:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:energy-consumption:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="code" />
<el-table-column label="工单编号" align="center" prop="workOrderCode" />
<el-table-column label="消耗时间" align="center" prop="consumptionTime" width="180">
<template #default="scope">
<span>{{ formatDate(scope.row.consumptionTime) }}</span>
</template>
</el-table-column>
<el-table-column label="煤消耗量(吨)" align="center" prop="coalConsumption" />
<el-table-column label="蒸汽产量(吨)" align="center" prop="steamOutput" />
<el-table-column label="用电量(kWh)" align="center" prop="electricityConsumption" />
<el-table-column label="用水量(m³)" align="center" prop="waterConsumption" />
<el-table-column label="审核状态" align="center" prop="auditStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_ENERGY_AUDIT_STATUS" :value="scope.row.auditStatus" />
</template>
</el-table-column>
<el-table-column label="审核人" align="center" prop="auditorName" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" width="240">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('view', scope.row.id)"
v-hasPermi="['mes:energy-consumption:query']"
>
查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:energy-consumption:update']"
v-if="scope.row.auditStatus === 0"
>
修改
</el-button>
<el-button
link
type="primary"
@click="openAuditForm(scope.row)"
v-hasPermi="['mes:energy-consumption:audit']"
v-if="scope.row.auditStatus === 0"
>
审核
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:energy-consumption:delete']"
v-if="scope.row.auditStatus === 0"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<EnergyConsumptionForm ref="formRef" @success="getList" />
<!-- 审核弹窗 -->
<EnergyConsumptionAudit ref="auditRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { getEnergyConsumptionPage, deleteEnergyConsumption, exportEnergyConsumptionExcel } from '@/api/mes/energy/consumption'
import EnergyConsumptionForm from './EnergyConsumptionForm.vue'
import EnergyConsumptionAudit from './EnergyConsumptionAudit.vue'
defineOptions({ name: 'MesEnergyConsumption' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
workOrderCode: undefined,
auditStatus: undefined,
consumptionTime: []
})
const queryFormRef = ref()
const exportLoading = ref(false)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await getEnergyConsumptionPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 审核操作 */
const auditRef = ref()
const openAuditForm = (row: any) => {
auditRef.value.open(row)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await deleteEnergyConsumption(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await exportEnergyConsumptionExcel(queryParams)
download.excel(data, '能源消耗记录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,803 @@
<template>
<div class="stevia-report-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<h1 class="page-title">
<Icon icon="ep:document" class="title-icon" />
甜叶菊生产报表
</h1>
<span class="report-date">{{ formatDate(queryDate) }}</span>
</div>
<div class="header-actions">
<span class="filter-label">查询日期</span>
<el-date-picker
v-model="queryDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:clearable="false"
:disabled-date="disabledDate"
@change="handleDateChange"
style="width: 140px"
/>
<el-divider direction="vertical" />
<span class="filter-label">本期累计</span>
<el-date-picker
v-model="periodDateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:clearable="false"
@change="handleDateChange"
style="width: 240px"
/>
<el-button type="primary" :loading="loading" @click="loadReportData">
<Icon icon="ep:refresh" class="mr-1" />
刷新
</el-button>
<el-button type="success" @click="handleExport">
<Icon icon="ep:download" class="mr-1" />
导出Excel
</el-button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :rows="15" animated />
</div>
<!-- 报表内容 -->
<div v-else class="report-content">
<div class="report-table-wrapper">
<table class="stevia-report-table">
<thead>
<tr>
<th class="col-category"> </th>
<th class="col-item"></th>
<th class="col-unit">单位</th>
<th class="col-value"> </th>
<th class="col-value"> </th>
<th class="col-value">本期累计</th>
<th class="col-value">年累计</th>
</tr>
</thead>
<tbody>
<template v-for="(group, groupIndex) in groupedReportData" :key="groupIndex">
<tr v-for="(row, rowIndex) in group.rows" :key="`${groupIndex}-${rowIndex}`">
<!-- 分类列合并单元格 -->
<td
v-if="rowIndex === 0"
:rowspan="group.rows.length"
class="col-category category-cell"
>
{{ group.category }}
</td>
<!-- 项目名称 -->
<td class="col-item">{{ row.item }}</td>
<!-- 单位 -->
<td class="col-unit">{{ row.unit }}</td>
<!-- 日计 -->
<td class="col-value">{{ formatValue(row.daily) }}</td>
<!-- 月计 -->
<td class="col-value">{{ formatValue(row.monthly) }}</td>
<!-- 本期累计 -->
<td class="col-value">{{ formatValue(row.periodTotal) }}</td>
<!-- 年累计 -->
<td class="col-value">{{ formatValue(row.yearly) }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- 报表底部 -->
<div class="report-footer">
<span>报表生成时间{{ currentTime }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getSteviaReport } from '@/api/mes/home'
import { Icon } from '@/components/Icon'
import download from '@/utils/download'
defineOptions({ name: 'MESSteviaReport' })
// 响应式数据
const loading = ref(false)
const queryDate = ref<string>(formatDateToString(new Date()))
const periodDateRange = ref<[string, string]>([getYearStart(), formatDateToString(new Date())])
const reportData = ref<any[]>([])
const currentTime = ref<string>('')
// 获取年初日期
function getYearStart(): string {
const now = new Date()
return `${now.getFullYear()}-01-01`
}
// 格式化日期为字符串 YYYY-MM-DD
function formatDateToString(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 格式化日期显示
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
// 禁用未来日期
const disabledDate = (time: Date) => {
return time.getTime() > Date.now()
}
// 日期变化处理
const handleDateChange = () => {
loadReportData()
}
// 格式化数值
const formatValue = (value: any) => {
if (value === null || value === undefined || value === '') return ''
if (typeof value === 'number') {
// 保留合适的小数位
if (Number.isInteger(value)) {
return value.toLocaleString()
}
return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 3 })
}
return value
}
// 将报表数据按分类分组
const groupedReportData = computed(() => {
const groups: { category: string; rows: any[] }[] = []
let currentCategory = ''
let currentGroup: { category: string; rows: any[] } | null = null
for (const row of reportData.value) {
const category = row.category || ''
if (category && category !== currentCategory) {
// 开始新分组
currentCategory = category
currentGroup = { category, rows: [] }
groups.push(currentGroup)
}
if (currentGroup) {
currentGroup.rows.push(row)
} else {
// 如果没有分类,创建一个默认分组
currentGroup = { category: '', rows: [row] }
groups.push(currentGroup)
}
}
return groups
})
// 加载报表数据
const loadReportData = async () => {
try {
loading.value = true
const res = await getSteviaReport({
date: queryDate.value,
periodStartDate: periodDateRange.value[0],
periodEndDate: periodDateRange.value[1]
})
const data = res.data || res
reportData.value = data.reportRows || []
currentTime.value = new Date().toLocaleString('zh-CN')
} catch (error) {
console.error('加载报表数据失败:', error)
ElMessage.error('加载报表数据失败')
} finally {
loading.value = false
}
}
// 导出Excel (CSV格式兼容Excel打开)
const handleExport = () => {
if (reportData.value.length === 0) {
ElMessage.warning('暂无数据可导出')
return
}
try {
// 构建CSV数据
const csvRows: string[] = []
// 添加BOM头确保Excel正确识别UTF-8编码
const BOM = '\uFEFF'
// 添加标题行
csvRows.push('甜叶菊生产报表')
csvRows.push('报表日期:' + formatDate(queryDate.value))
csvRows.push('') // 空行
// 表头
csvRows.push(['项目', '子项', '单位', '日计', '月计', '本期累计', '年累计'].join(','))
// 数据行
for (const group of groupedReportData.value) {
for (let i = 0; i < group.rows.length; i++) {
const row = group.rows[i]
const rowData = [
i === 0 ? group.category : '',
row.item || '',
row.unit || '',
row.daily ?? '',
row.monthly ?? '',
row.periodTotal ?? '',
row.yearly ?? ''
]
// 处理可能包含逗号的字段
csvRows.push(rowData.map(cell => `"${cell}"`).join(','))
}
}
// 创建Blob并下载
const csvContent = BOM + csvRows.join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const fileName = `甜叶菊生产报表_${queryDate.value}.csv`
download.excel(blob, fileName)
ElMessage.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
}
}
// 页面挂载时加载数据
onMounted(() => {
loadReportData()
})
</script>
<style scoped lang="scss">
.stevia-report-container {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 84px);
display: flex;
flex-direction: column;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
.header-left {
display: flex;
align-items: center;
gap: 16px;
.page-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 22px;
font-weight: 600;
color: #1f2937;
.title-icon {
font-size: 26px;
color: #10b981;
}
}
.report-date {
padding: 4px 12px;
background: #f0fdf4;
border-radius: 4px;
color: #059669;
font-size: 14px;
}
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
.filter-label {
font-size: 14px;
color: #374151;
white-space: nowrap;
}
}
}
.loading-container {
flex: 1;
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.report-content {
flex: 1;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
.report-table-wrapper {
padding: 20px;
overflow-x: auto;
}
.stevia-report-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
th, td {
border: 1px solid #e5e7eb;
padding: 12px 16px;
text-align: center;
}
thead {
th {
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
color: white;
font-weight: 600;
white-space: nowrap;
}
}
tbody {
tr {
&:nth-child(even) {
background: #f9fafb;
}
&:hover {
background: #f0fdf4;
}
}
td {
color: #374151;
&.category-cell {
background: #f0fdf4;
font-weight: 600;
color: #059669;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
}
.col-category {
width: 100px;
min-width: 100px;
}
.col-item {
width: 120px;
min-width: 120px;
text-align: left;
padding-left: 20px;
}
.col-unit {
width: 60px;
min-width: 60px;
}
.col-value {
width: 100px;
min-width: 100px;
text-align: right;
font-family: 'Consolas', 'Monaco', monospace;
}
}
}
}
.report-footer {
margin-top: 16px;
padding: 12px 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
text-align: center;
color: #6b7280;
font-size: 13px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.stevia-report-container {
.page-header {
.header-actions {
flex-wrap: wrap;
gap: 8px;
.filter-label {
font-size: 13px;
}
.el-date-picker {
width: 120px !important;
}
.el-date-picker--daterange {
width: 200px !important;
}
}
}
}
}
@media (max-width: 992px) {
.stevia-report-container {
padding: 16px;
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
padding: 16px 20px;
.header-left {
justify-content: center;
text-align: center;
.page-title {
font-size: 20px;
.title-icon {
font-size: 24px;
}
}
.report-date {
font-size: 13px;
}
}
.header-actions {
width: 100%;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
.el-date-picker {
width: 140px !important;
}
.el-date-picker--daterange {
width: 240px !important;
}
.el-button {
min-width: 80px;
}
}
}
.report-content {
.report-table-wrapper {
padding: 16px;
.stevia-report-table {
font-size: 13px;
th, td {
padding: 10px 12px;
}
.col-category {
width: 80px;
min-width: 80px;
}
.col-item {
width: 100px;
min-width: 100px;
padding-left: 16px;
}
.col-unit {
width: 50px;
min-width: 50px;
}
.col-value {
width: 85px;
min-width: 85px;
}
}
}
}
}
}
@media (max-width: 768px) {
.stevia-report-container {
padding: 12px;
min-height: calc(100vh - 60px);
.page-header {
padding: 12px 16px;
margin-bottom: 16px;
.header-left {
gap: 12px;
.page-title {
font-size: 18px;
.title-icon {
font-size: 22px;
}
}
}
.header-actions {
gap: 8px;
.filter-label {
font-size: 12px;
min-width: fit-content;
}
.el-date-picker {
width: 120px !important;
font-size: 12px;
}
.el-date-picker--daterange {
width: 200px !important;
font-size: 12px;
}
.el-button {
padding: 8px 12px;
font-size: 12px;
min-width: 70px;
.mr-1 {
margin-right: 4px;
}
}
.el-divider--vertical {
display: none;
}
}
}
.report-content {
.report-table-wrapper {
padding: 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
.stevia-report-table {
min-width: 600px;
font-size: 12px;
th, td {
padding: 8px 10px;
white-space: nowrap;
}
thead th {
font-size: 12px;
position: sticky;
top: 0;
z-index: 10;
}
.col-category {
width: 70px;
min-width: 70px;
font-size: 11px;
}
.col-item {
width: 90px;
min-width: 90px;
padding-left: 12px;
font-size: 11px;
}
.col-unit {
width: 40px;
min-width: 40px;
font-size: 11px;
}
.col-value {
width: 75px;
min-width: 75px;
font-size: 11px;
}
}
}
}
.report-footer {
margin-top: 12px;
padding: 10px 16px;
font-size: 12px;
}
}
}
@media (max-width: 480px) {
.stevia-report-container {
padding: 8px;
.page-header {
padding: 10px 12px;
margin-bottom: 12px;
.header-left {
.page-title {
font-size: 16px;
.title-icon {
font-size: 20px;
}
}
.report-date {
font-size: 11px;
padding: 3px 8px;
}
}
.header-actions {
.filter-label {
font-size: 11px;
}
.el-date-picker {
width: 110px !important;
}
.el-date-picker--daterange {
width: 180px !important;
}
.el-button {
padding: 6px 10px;
font-size: 11px;
min-width: 60px;
}
}
}
.report-content {
.report-table-wrapper {
padding: 8px;
.stevia-report-table {
min-width: 550px;
font-size: 11px;
th, td {
padding: 6px 8px;
}
.col-category {
width: 60px;
min-width: 60px;
font-size: 10px;
}
.col-item {
width: 80px;
min-width: 80px;
padding-left: 10px;
font-size: 10px;
}
.col-unit {
width: 35px;
min-width: 35px;
font-size: 10px;
}
.col-value {
width: 65px;
min-width: 65px;
font-size: 10px;
}
}
}
}
.report-footer {
margin-top: 10px;
padding: 8px 12px;
font-size: 11px;
}
}
}
// 横屏优化
@media (max-width: 768px) and (orientation: landscape) {
.stevia-report-container {
.page-header {
.header-left {
flex-direction: row;
.page-title {
font-size: 16px;
}
}
.header-actions {
flex-direction: row;
justify-content: flex-start;
}
}
}
}
// 触摸设备优化
@media (hover: none) and (pointer: coarse) {
.stevia-report-container {
.page-header {
.header-actions {
.el-button {
min-height: 44px;
touch-action: manipulation;
}
.el-date-picker {
min-height: 44px;
}
}
}
.report-content {
.stevia-report-table {
tbody tr:hover {
background: inherit;
}
tbody tr:active {
background: #f0fdf4;
}
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,693 @@
<template>
<div class="daily-report-container">
<!-- 页面标题 -->
<div class="page-header">
<div class="header-content">
<h1 class="page-title">
<Icon icon="ep:data-analysis" class="title-icon" />
MES生产日报
</h1>
<div class="report-date">
<Icon icon="ep:calendar" />
{{ formatDate(reportData.summary?.reportDate) }}
</div>
</div>
<div class="query-actions">
<el-date-picker
v-model="queryDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:clearable="false"
:disabled-date="disabledDate"
@change="handleDateChange"
/>
<el-button
type="primary"
:icon="RefreshIcon"
:loading="loading"
@click="loadReportData"
>
刷新数据
</el-button>
</div>
</div>
<!-- 数据加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :rows="8" animated />
</div>
<!-- 报表内容 -->
<div v-else class="report-content">
<!-- 概览卡片 -->
<div class="overview-cards">
<div class="card-item">
<div class="card-icon material">
<Icon icon="ep:box" />
</div>
<div class="card-content">
<div class="card-value">{{ reportData.summary?.totalMaterialTypes || 0 }}</div>
<div class="card-label">物料种类</div>
</div>
</div>
<div class="card-item">
<div class="card-icon product">
<Icon icon="ep:goods" />
</div>
<div class="card-content">
<div class="card-value">{{ reportData.summary?.totalProductTypes || 0 }}</div>
<div class="card-label">产品种类</div>
</div>
</div>
<div class="card-item">
<div class="card-icon water">
<Icon icon="ep:coffee-cup" />
</div>
<div class="card-content">
<div class="card-value">{{ reportData.utilityConsumption?.waterConsumption || 0 }}</div>
<div class="card-label">用水量()</div>
</div>
</div>
<div class="card-item">
<div class="card-icon electricity">
<Icon icon="ep:lightning" />
</div>
<div class="card-content">
<div class="card-value">{{ reportData.utilityConsumption?.electricityConsumption || 0 }}</div>
<div class="card-label">用电量(kWh)</div>
</div>
</div>
</div>
<!-- 详细数据表格 -->
<div class="data-tables">
<!-- 物料消耗表 -->
<div class="table-section">
<div class="section-header">
<h2 class="section-title">
<Icon icon="ep:box" />
物料消耗明细
</h2>
<div class="section-actions">
<el-button size="small" @click="exportMaterialData">
<Icon icon="ep:download" />
导出
</el-button>
</div>
</div>
<el-table
:data="reportData.materialConsumption || []"
class="data-table"
stripe
border
empty-text="暂无物料消耗数据"
>
<el-table-column prop="materialCode" label="物料编码" width="120" />
<el-table-column prop="materialName" label="物料名称" min-width="150" />
<el-table-column prop="consumedQuantity" label="消耗数量" width="120" align="right">
<template #default="{ row }">
<span
class="quantity-value"
:class="{ 'zero-consumption': row.consumedQuantity === 0 || row.consumedQuantity === '0' }"
>
{{ formatNumber(row.consumedQuantity) }}
</span>
<el-tag
v-if="row.consumedQuantity === 0 || row.consumedQuantity === '0'"
type="info"
size="small"
class="zero-tag"
>
未消耗
</el-tag>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="80" align="center" />
</el-table>
</div>
<!-- 产量统计表 -->
<div class="table-section">
<div class="section-header">
<h2 class="section-title">
<Icon icon="ep:goods" />
产量统计明细
</h2>
<div class="section-actions">
<el-button size="small" @click="exportProductionData">
<Icon icon="ep:download" />
导出
</el-button>
</div>
</div>
<el-table
:data="reportData.productionOutput || []"
class="data-table"
stripe
border
empty-text="暂无产量数据"
>
<el-table-column prop="productCode" label="产品编码" width="120" />
<el-table-column prop="productName" label="产品名称" min-width="150" />
<el-table-column prop="outputQuantity" label="总产量" width="100" align="right">
<template #default="{ row }">
<span class="quantity-value">{{ formatNumber(row.outputQuantity) }}</span>
</template>
</el-table-column>
<el-table-column prop="qualifiedQuantity" label="合格数量" width="100" align="right">
<template #default="{ row }">
<span class="quantity-value qualified">{{ formatNumber(row.qualifiedQuantity) }}</span>
</template>
</el-table-column>
<el-table-column prop="unqualifiedQuantity" label="不合格数量" width="110" align="right">
<template #default="{ row }">
<span class="quantity-value unqualified">{{ formatNumber(row.unqualifiedQuantity) }}</span>
</template>
</el-table-column>
<el-table-column label="合格率" width="100" align="center">
<template #default="{ row }">
<el-tag
:type="getQualityTagType(calculateQualityRate(row))"
size="small"
>
{{ calculateQualityRate(row) }}%
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 水电消耗详情 -->
<div class="table-section">
<div class="section-header">
<h2 class="section-title">
<Icon icon="ep:lightning" />
水电消耗详情
</h2>
</div>
<div class="utility-grid">
<div class="utility-card water-card">
<div class="utility-icon">
<Icon icon="ep:coffee-cup" />
</div>
<div class="utility-info">
<div class="utility-value">{{ reportData.utilityConsumption?.waterConsumption || 0 }}</div>
<div class="utility-unit">{{ reportData.utilityConsumption?.waterUnit || '吨' }}</div>
<div class="utility-label">用水量</div>
</div>
</div>
<div class="utility-card electricity-card">
<div class="utility-icon">
<Icon icon="ep:lightning" />
</div>
<div class="utility-info">
<div class="utility-value">{{ reportData.utilityConsumption?.electricityConsumption || 0 }}</div>
<div class="utility-unit">{{ reportData.utilityConsumption?.electricityUnit || 'kWh' }}</div>
<div class="utility-label">用电量</div>
</div>
</div>
<!-- 环境数据 -->
<div v-if="reportData.utilityConsumption?.avgAirTemp" class="utility-card env-card">
<div class="utility-icon">
<Icon icon="ep:sunny" />
</div>
<div class="utility-info">
<div class="utility-value">{{ reportData.utilityConsumption.avgAirTemp }}°C</div>
<div class="utility-unit">{{ reportData.utilityConsumption.avgAirHumidity }}%</div>
<div class="utility-label">温湿度</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 报表生成时间 -->
<div class="report-footer">
<div class="footer-info">
<Icon icon="ep:clock" />
报表生成时间: {{ formatDateTime(reportData.summary?.reportTime) }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh as RefreshIcon } from '@element-plus/icons-vue'
import * as MesHomeApi from '@/api/mes/home/index'
// 响应式数据
const loading = ref(false)
const reportData = ref<any>({})
const queryDate = ref<string>(formatDateToString(new Date()))
// 格式化日期为字符串 YYYY-MM-DD
function formatDateToString(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 禁用未来日期
const disabledDate = (time: Date) => {
return time.getTime() > Date.now()
}
// 日期变化处理
const handleDateChange = () => {
loadReportData()
}
// 必须显示的物料列表即使消耗数量为0
const requiredMaterials = [
{ materialCode: 'HCL001', materialName: '盐酸', unit: '千克' },
{ materialCode: 'ALC001', materialName: '醇', unit: '千克' },
{ materialCode: 'FES001', materialName: '硫酸亚铁', unit: '千克' },
{ materialCode: 'NAOH001', materialName: '片碱', unit: '千克' },
{ materialCode: 'CAO001', materialName: '氧化钙', unit: '千克' },
{ materialCode: 'BACT001', materialName: '杀菌剂', unit: '千克' },
{ materialCode: 'COAL001', materialName: '煤', unit: '千克' }
]
// 处理物料消耗数据,确保必需物料都显示
const processMaterialConsumption = (materialConsumption: any[]) => {
const materialMap = new Map()
// 先添加实际消耗的物料
materialConsumption.forEach(item => {
const key = item.materialName // 使用物料名称作为唯一标识
materialMap.set(key, item)
})
// 添加必须显示的物料(如果不存在同名物料)
requiredMaterials.forEach(material => {
const key = material.materialName
if (!materialMap.has(key)) {
// 只有当不存在同名物料时才添加
materialMap.set(key, {
materialCode: material.materialCode,
materialName: material.materialName,
unit: material.unit,
consumedQuantity: 0
})
}
})
return Array.from(materialMap.values()).sort((a, b) => {
// 优先显示必需物料,按名称排序
const aIsRequired = requiredMaterials.some(m => m.materialName === a.materialName)
const bIsRequired = requiredMaterials.some(m => m.materialName === b.materialName)
if (aIsRequired && !bIsRequired) return -1
if (!aIsRequired && bIsRequired) return 1
return a.materialName.localeCompare(b.materialName, 'zh-CN')
})
}
// 加载报表数据
const loadReportData = async () => {
try {
loading.value = true
const data = await MesHomeApi.getDailyReport({ date: queryDate.value })
// 处理物料消耗数据
if (data.materialConsumption) {
data.materialConsumption = processMaterialConsumption(data.materialConsumption)
}
reportData.value = data
} catch (error) {
console.error('加载报表数据失败:', error)
ElMessage.error('加载报表数据失败')
} finally {
loading.value = false
}
}
// 格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
// 格式化日期时间
const formatDateTime = (dateTimeStr: string) => {
if (!dateTimeStr) return ''
const date = new Date(dateTimeStr)
return date.toLocaleString('zh-CN')
}
// 格式化数字
const formatNumber = (value: any) => {
if (value === null || value === undefined) return '0'
return Number(value).toLocaleString()
}
// 计算合格率
const calculateQualityRate = (row: any) => {
const total = (row.qualifiedQuantity || 0) + (row.unqualifiedQuantity || 0)
if (total === 0) return 0
return Math.round((row.qualifiedQuantity || 0) * 100 / total)
}
// 获取质量标签类型
const getQualityTagType = (rate: number) => {
if (rate >= 95) return 'success'
if (rate >= 85) return 'warning'
return 'danger'
}
// 导出物料数据
const exportMaterialData = () => {
ElMessage.info('导出功能开发中...')
}
// 导出产量数据
const exportProductionData = () => {
ElMessage.info('导出功能开发中...')
}
// 页面挂载时加载数据
onMounted(() => {
loadReportData()
})
</script>
<style scoped lang="scss">
.daily-report-container {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 84px);
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.header-content {
display: flex;
align-items: center;
gap: 24px;
.page-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 24px;
font-weight: 600;
color: #1f2937;
.title-icon {
font-size: 28px;
color: #3b82f6;
}
}
.report-date {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #f3f4f6;
border-radius: 6px;
color: #6b7280;
font-size: 14px;
}
}
.query-actions {
display: flex;
align-items: center;
gap: 12px;
}
}
.loading-container {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.overview-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
.card-item {
display: flex;
align-items: center;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 24px;
color: white;
&.material {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.product {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&.water {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.electricity {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
}
.card-content {
flex: 1;
.card-value {
font-size: 28px;
font-weight: 700;
color: #1f2937;
line-height: 1;
margin-bottom: 4px;
}
.card-label {
font-size: 14px;
color: #6b7280;
}
}
}
}
.data-tables {
display: flex;
flex-direction: column;
gap: 24px;
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
}
.data-table {
.quantity-value {
font-weight: 600;
&.qualified {
color: #059669;
}
&.unqualified {
color: #dc2626;
}
&.zero-consumption {
color: #9ca3af;
font-style: italic;
}
}
.zero-tag {
margin-left: 8px;
}
}
.utility-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 24px;
.utility-card {
display: flex;
align-items: center;
padding: 20px;
border-radius: 8px;
border: 1px solid #e5e7eb;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.utility-icon {
width: 40px;
height: 40px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 20px;
color: white;
}
.utility-info {
flex: 1;
.utility-value {
font-size: 24px;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
.utility-unit {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.utility-label {
font-size: 14px;
color: #6b7280;
}
}
&.water-card .utility-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.electricity-card .utility-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
&.env-card .utility-icon {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
}
}
}
}
.report-footer {
margin-top: 24px;
padding: 16px 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
.footer-info {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: #6b7280;
font-size: 14px;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.daily-report-container {
padding: 16px;
.page-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.query-actions {
width: 100%;
flex-direction: column;
align-items: stretch;
.el-date-picker {
width: 100%;
}
.el-button {
width: 100%;
}
}
}
.overview-cards {
grid-template-columns: 1fr;
}
}
}
</style>

View File

@@ -0,0 +1,246 @@
<template>
<div>
<ContentWrap title="温湿度监测">
<!-- 搜索栏 -->
<el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="80px">
<el-form-item label="点位" prop="pointName">
<el-input v-model="queryParams.pointName" class="!w-240px" clearable placeholder="请输入点位" @keyup.enter="getList" />
</el-form-item>
<el-form-item label="时间范围" prop="timeRange">
<el-date-picker
v-model="queryParams.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
:shortcuts="defaultShortcuts"
value-format="YYYY-MM-DD HH:mm:ss"
class="!w-420px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap title="趋势折线图">
<el-row :gutter="16">
<el-col :span="12">
<div class="mb-8px text-14px font-700">空气温度</div>
<Echart :height="260" :options="optAirTemp" />
</el-col>
<el-col :span="12">
<div class="mb-8px text-14px font-700">空气湿度</div>
<Echart :height="260" :options="optAirHumidity" />
</el-col>
<el-col :span="12" class="mt-12px">
<div class="mb-8px text-14px font-700">土壤温度</div>
<Echart :height="260" :options="optSoilTemp" />
</el-col>
<el-col :span="12" class="mt-12px">
<div class="mb-8px text-14px font-700">土壤湿度</div>
<Echart :height="260" :options="optSoilHumidity" />
</el-col>
</el-row>
</ContentWrap>
<ContentWrap title="数据列表">
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="pointName" label="点位" min-width="120" :show-overflow-tooltip="true" />
<el-table-column prop="airTemp" label="空气温度(°C)" min-width="120" />
<el-table-column prop="airHumidity" label="空气湿度(%)" min-width="120" />
<el-table-column prop="soilTemp" label="土壤温度(°C)" min-width="120" />
<el-table-column prop="soilHumidity" label="土壤湿度(%)" min-width="120" />
<el-table-column label="时间" min-width="170">
<template #default="scope">
{{ formatDate(scope.row.time) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" :show-overflow-tooltip="true" />
</el-table>
<div class="mt-12px flex justify-end">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="getList"
@current-change="getList"
/>
</div>
</ContentWrap>
</div>
</template>
<script lang="ts" setup>
import { ContentWrap } from '@/components/ContentWrap'
import { Echart } from '@/components/Echart'
import { defaultShortcuts, formatDate } from '@/utils/formatTime'
import * as HumidityApi from '@/api/mes/humidity'
import dayjs from 'dayjs'
defineOptions({ name: 'MesHumidityIndex' })
// 查询参数
const queryParams = reactive<any>({
pointName: undefined,
timeRange: getDefaultTimeRange() as any,
pageNo: 1,
pageSize: 10
})
// 列表数据
const loading = ref<boolean>(false)
const list = ref<HumidityApi.HumidityRecordVO[]>([])
const total = ref<number>(0)
// 定时器
const timer = ref<number | null>(null)
// 折线图配置(四张图:按点位分组,多条线)
import type { EChartsOption, SeriesOption } from 'echarts'
const baseOption = () => ({
grid: { left: 20, right: 20, bottom: 20, top: 60, containLabel: true },
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, padding: [5, 10] },
legend: { top: 20, data: [] as string[] },
xAxis: { type: 'category', boundaryGap: false, data: [] as string[] },
yAxis: { type: 'value' },
series: [] as SeriesOption[],
toolbox: { feature: { dataZoom: { yAxisIndex: false }, brush: { type: ['lineX', 'clear'] }, saveAsImage: { show: true } } }
}) as any
const optAirTemp = reactive(baseOption()) as unknown as EChartsOption
const optAirHumidity = reactive(baseOption()) as unknown as EChartsOption
const optSoilTemp = reactive(baseOption()) as unknown as EChartsOption
const optSoilHumidity = reactive(baseOption()) as unknown as EChartsOption
// 获取列表
const getList = async () => {
loading.value = true
try {
const params: any = {
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
pointName: queryParams.pointName
}
if (queryParams.timeRange && queryParams.timeRange.length === 2) {
params.beginTime = queryParams.timeRange[0]
params.endTime = queryParams.timeRange[1]
}
const data = await HumidityApi.getHumidityPage(params)
list.value = data.list || []
total.value = data.total || 0
// 同步刷新趋势
await getTrend()
} finally {
loading.value = false
}
}
// 获取折线图数据(按点位分组)
const getTrend = async () => {
const params: any = {
pointName: queryParams.pointName
}
if (queryParams.timeRange && queryParams.timeRange.length === 2) {
params.beginTime = queryParams.timeRange[0]
params.endTime = queryParams.timeRange[1]
}
const trend = await HumidityApi.getHumidityTrend(params)
const times: string[] = trend?.times || []
const points: any[] = trend?.points || []
// x 轴同步
;[optAirTemp, optAirHumidity, optSoilTemp, optSoilHumidity].forEach((opt: any) => {
if (opt.xAxis) opt.xAxis.data = times
})
// 图例(点位名)
const legend = points.map((p) => p.pointName || '未命名点位')
;[optAirTemp, optAirHumidity, optSoilTemp, optSoilHumidity].forEach((opt: any) => {
opt.legend.data = legend
})
// series 按点位生成
const toNums = (arr: any[]) => (arr || []).map((v) => (v === null || v === undefined || v === '' ? null : Number(v)))
const common = { smooth: true, connectNulls: true, showSymbol: false } as any
const sAirTemp: SeriesOption[] = points.map((p) => ({ name: p.pointName || '未命名点位', type: 'line', ...common, data: toNums(p.airTemp) }))
const sAirHumidity: SeriesOption[] = points.map((p) => ({ name: p.pointName || '未命名点位', type: 'line', ...common, data: toNums(p.airHumidity) }))
const sSoilTemp: SeriesOption[] = points.map((p) => ({ name: p.pointName || '未命名点位', type: 'line', ...common, data: toNums(p.soilTemp) }))
const sSoilHumidity: SeriesOption[] = points.map((p) => ({ name: p.pointName || '未命名点位', type: 'line', ...common, data: toNums(p.soilHumidity) }))
;(optAirTemp as any).series = sAirTemp
;(optAirHumidity as any).series = sAirHumidity
;(optSoilTemp as any).series = sSoilTemp
;(optSoilHumidity as any).series = sSoilHumidity
}
// 重置
const queryFormRef = ref()
const resetQuery = async () => {
queryParams.pointName = undefined
queryParams.timeRange = getDefaultTimeRange() as any
queryParams.pageNo = 1
queryParams.pageSize = 10
await getList()
}
// 默认时间范围:最近 24 小时
function getDefaultTimeRange(): [string, string] {
return [
dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'),
dayjs().format('YYYY-MM-DD HH:mm:ss')
]
}
// 定时获取数据
const startFetchTimer = () => {
// 清除可能存在的旧定时器
if (timer.value) {
clearInterval(timer.value)
}
// 立即执行一次
fetchLatestData()
// 设置新定时器每30分钟执行一次
timer.value = setInterval(() => {
fetchLatestData()
}, 30 * 60 * 1000) // 30分钟 = 30 * 60 * 1000毫秒
}
// 获取最新数据
const fetchLatestData = async () => {
try {
await HumidityApi.fetchAndCreateHumidity()
console.log('温湿度数据已自动更新')
// 刷新列表和趋势图
await getList()
} catch (error) {
console.error('自动获取温湿度数据失败:', error)
}
}
onMounted(async () => {
await getList()
startFetchTimer()
})
onUnmounted(() => {
// 组件卸载时清除定时器
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,376 @@
<template>
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="1000px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产品" prop="productId">
<el-select
v-model="formData.productId"
placeholder="请选择产品"
filterable
class="!w-100%"
:disabled="isViewMode"
@change="handleProductChange"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="`${item.name} (${item.barCode || '-'})`"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="BOM名称" prop="bomName">
<el-input v-model="formData.bomName" placeholder="请输入BOM名称" :disabled="isViewMode" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="BOM编码" prop="bomCode">
<el-input v-model="formData.bomCode" placeholder="自动生成" :disabled="true" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="版本号" prop="version">
<el-input v-model="formData.version" placeholder="如V1.0" :disabled="isViewMode" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="默认版本" prop="isDefault">
<el-radio-group v-model="formData.isDefault" :disabled="isViewMode">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status" :disabled="isViewMode">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" :disabled="isViewMode" />
</el-form-item>
</el-col>
</el-row>
<!-- BOM明细 -->
<el-divider content-position="left">BOM明细</el-divider>
<el-form-item v-if="!isViewMode">
<el-button type="primary" @click="addItem">
<Icon icon="ep:plus" class="mr-5px" /> 添加物料
</el-button>
</el-form-item>
<el-table :data="formData.items" border style="width: 100%">
<el-table-column label="物料" min-width="200">
<template #default="{ row, $index }">
<el-select
v-if="!isViewMode"
v-model="row.materialId"
placeholder="请选择物料"
filterable
class="!w-100%"
@change="(val: number) => handleMaterialChange(val, $index)"
>
<el-option
v-for="item in materialList"
:key="item.id"
:label="`${item.name} (${item.barCode || '-'})`"
:value="item.id"
/>
</el-select>
<span v-else>{{ row.materialName }} ({{ row.materialCode || '-' }})</span>
</template>
</el-table-column>
<el-table-column label="物料类型" width="120">
<template #default="{ row }">
<el-input v-if="!isViewMode" v-model="row.materialType" placeholder="类型" :disabled="true" />
<span v-else>{{ row.materialType || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="单位" width="80">
<template #default="{ row }">
<el-input v-if="!isViewMode" v-model="row.unit" placeholder="单位" />
<span v-else>{{ row.unit || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="单位用量" width="120">
<template #default="{ row }">
<el-input-number
v-if="!isViewMode"
v-model="row.unitQuantity"
:min="0"
:precision="4"
:step="0.1"
:controls="false"
class="!w-100%"
/>
<span v-else>{{ row.unitQuantity }}</span>
</template>
</el-table-column>
<el-table-column label="损耗率(%)" width="100">
<template #default="{ row }">
<el-input-number
v-if="!isViewMode"
v-model="row.lossRate"
:min="0"
:max="100"
:precision="2"
:controls="false"
class="!w-100%"
/>
<span v-else>{{ row.lossRate || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="单价" width="100">
<template #default="{ row }">
<el-input-number
v-if="!isViewMode"
v-model="row.unitPrice"
:min="0"
:precision="2"
:controls="false"
class="!w-100%"
/>
<span v-else>{{ row.unitPrice ? '¥' + row.unitPrice : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="必需" width="80">
<template #default="{ row }">
<el-checkbox v-if="!isViewMode" v-model="row.required" :true-label="1" :false-label="0" />
<el-tag v-else :type="row.required === 1 ? 'success' : 'info'" size="small">
{{ row.required === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="!isViewMode" label="操作" width="80" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" @click="removeItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">{{ isViewMode ? '关闭' : '取消' }}</el-button>
<el-button v-if="!isViewMode" type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import { createProductBom, updateProductBom, getProductBom, ProductBomItemVO } from '@/api/mes/product/bom'
import { getProductVOListByStatus } from '@/api/erp/product/product'
import { ProductCategoryApi } from '@/api/erp/product/category'
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formRef = ref()
const isViewMode = computed(() => formType.value === 'view')
const productList = ref<any[]>([])
const materialList = ref<any[]>([])
const categoryList = ref<any[]>([])
const formData = reactive({
id: undefined as number | undefined,
bomCode: '',
bomName: '',
productId: undefined as number | undefined,
productCode: '',
productName: '',
version: 'V1.0',
isDefault: 1,
remark: '',
status: 1,
items: [] as ProductBomItemVO[]
})
const formRules = {
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
bomName: [{ required: true, message: '请输入BOM名称', trigger: 'blur' }]
}
const resetForm = () => {
formData.id = undefined
formData.bomCode = ''
formData.bomName = ''
formData.productId = undefined
formData.productCode = ''
formData.productName = ''
formData.version = 'V1.0'
formData.isDefault = 1
formData.remark = ''
formData.status = 1
formData.items = []
formRef.value?.resetFields()
}
const open = async (type: string, id?: number) => {
dialogVisible.value = true
formType.value = type
if (type === 'create') {
dialogTitle.value = '新增产品BOM'
} else if (type === 'update') {
dialogTitle.value = '编辑产品BOM'
} else {
dialogTitle.value = '查看产品BOM'
}
resetForm()
// 加载产品列表和分类列表
try {
const [productData, categoryData] = await Promise.all([
getProductVOListByStatus(0),
ProductCategoryApi.getProductCategorySimpleList()
])
productList.value = productData || []
materialList.value = productData || []
categoryList.value = categoryData || []
} catch (error) {
console.error('加载数据失败:', error)
}
if (id) {
formLoading.value = true
try {
const data = await getProductBom(id)
formData.id = data.id
formData.bomCode = data.bomCode || ''
formData.bomName = data.bomName || ''
formData.productId = data.productId
formData.productCode = data.productCode || ''
formData.productName = data.productName || ''
formData.version = data.version || 'V1.0'
formData.isDefault = data.isDefault ?? 0
formData.remark = data.remark || ''
formData.status = data.status ?? 1
formData.items = data.items || []
} finally {
formLoading.value = false
}
}
}
const handleProductChange = (productId: number) => {
const product = productList.value.find((p) => p.id === productId)
if (product) {
formData.productCode = product.barCode || ''
formData.productName = product.name || ''
// 自动设置BOM名称
if (!formData.bomName) {
formData.bomName = product.name + ' BOM'
}
}
}
const handleMaterialChange = (materialId: number, index: number) => {
const material = materialList.value.find((m) => m.id === materialId)
if (material && formData.items[index]) {
formData.items[index].materialCode = material.barCode || ''
formData.items[index].materialName = material.name || ''
formData.items[index].unit = material.unitName || ''
if (material.purchasePrice) {
formData.items[index].unitPrice = material.purchasePrice
}
// 根据分类自动设置物料类型
if (material.categoryId) {
const category = categoryList.value.find((c) => c.id === material.categoryId)
formData.items[index].materialType = category?.name || ''
}
}
}
const addItem = () => {
formData.items.push({
materialId: undefined as any,
materialCode: '',
materialName: '',
materialType: '',
unit: '',
unitQuantity: 1,
lossRate: 0,
purchaseLeadTime: 7,
unitPrice: 0,
required: 1,
sort: formData.items.length,
remark: ''
})
}
const removeItem = (index: number) => {
formData.items.splice(index, 1)
}
const submitForm = async () => {
await formRef.value?.validate()
// 校验明细
if (formData.items.length === 0) {
ElMessage.warning('请至少添加一条BOM明细')
return
}
for (const item of formData.items) {
if (!item.materialId) {
ElMessage.warning('请选择物料')
return
}
if (!item.unitQuantity || item.unitQuantity <= 0) {
ElMessage.warning('单位用量必须大于0')
return
}
}
formLoading.value = true
try {
if (formType.value === 'create') {
await createProductBom(formData)
ElMessage.success('新增成功')
} else {
await updateProductBom(formData)
ElMessage.success('更新成功')
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
const handleClose = () => {
resetForm()
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,197 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:model="queryParams"
:inline="true"
label-width="80px"
class="-mb-15px"
>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="BOM编码" prop="bomCode">
<el-input
v-model="queryParams.bomCode"
placeholder="请输入BOM编码"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="BOM名称" prop="bomName">
<el-input
v-model="queryParams.bomName"
placeholder="请输入BOM名称"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-200px"
>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary"
@click="openForm('create')"
v-hasPermi="['mes:product-bom:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="BOM编码" align="center" prop="bomCode" width="180" />
<el-table-column label="BOM名称" align="center" prop="bomName" min-width="150" />
<el-table-column label="产品编码" align="center" prop="productCode" width="120" />
<el-table-column label="产品名称" align="center" prop="productName" min-width="150" />
<el-table-column label="版本" align="center" prop="version" width="80" />
<el-table-column label="默认版本" align="center" prop="isDefault" width="100">
<template #default="{ row }">
<el-tag :type="row.isDefault === 1 ? 'success' : 'info'">
{{ row.isDefault === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" fixed="right" width="200">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('view', scope.row.id)"
v-hasPermi="['mes:product-bom:query']"
>
查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:product-bom:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:product-bom:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ProductBomForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Icon } from '@/components/Icon'
import ProductBomForm from './ProductBomForm.vue'
import { getProductBomPage, deleteProductBom } from '@/api/mes/product/bom'
defineOptions({ name: 'MesProductBom' })
const loading = ref(true)
const total = ref(0)
const list = ref<any[]>([])
const queryFormRef = ref()
const formRef = ref()
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
productName: undefined,
bomCode: undefined,
bomName: undefined,
status: undefined
})
const getList = async () => {
loading.value = true
try {
const data = await getProductBomPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (id: number) => {
try {
await ElMessageBox.confirm('确认删除该BOM记录吗删除后明细数据也将被删除', '提示', {
type: 'warning'
})
await deleteProductBom(id)
ElMessage.success('删除成功')
getList()
} catch {}
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,487 @@
<template>
<ContentWrap>
<!-- 搜索栏 -->
<el-form :inline="true" :model="queryParams" class="-mb-15px" label-width="84px">
<el-form-item label="BOM编号">
<el-input
v-model="queryParams.code"
placeholder="请输入BOM编号"
class="!w-240px"
clearable
/>
</el-form-item>
<el-form-item label="BOM名称">
<el-input
v-model="queryParams.name"
placeholder="请输入BOM名称"
class="!w-240px"
clearable
/>
</el-form-item>
<el-form-item label="产品条码">
<el-input
v-model="queryParams.productBarCode"
placeholder="请输入产品条码"
class="!w-240px"
clearable
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" class="!w-240px" clearable>
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm('create')"
><Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
>
<el-button
type="danger"
plain
:disabled="selectionList.length === 0"
@click="handleBatchDelete"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table :data="list" v-loading="loading" :stripe="true" @selection-change="onSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column prop="code" label="BOM编号" min-width="140" />
<el-table-column prop="name" label="BOM名称" min-width="180" />
<el-table-column label="产品信息" min-width="200">
<template #default="{ row }">
<div>{{ row.productName }}</div>
<div class="text-gray-400 text-sm">{{ row.productBarCode }}</div>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="90" align="center" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{
row.status === 1 ? '启用' : '禁用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="itemCount" label="组件数" width="90" align="center" />
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="220" align="center">
<template #default="{ row }">
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" @click="openForm('update', row.id)"
><Icon icon="ep:edit"
/></el-button>
</el-tooltip>
<el-tooltip content="明细" placement="top">
<el-button link type="info" @click="openDetail(row)"><Icon icon="ep:list" /></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" @click="handleDelete(row.id)"
><Icon icon="ep:delete"
/></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="formDialogVisible" :title="formDialogTitle" width="700px" append-to-body>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="BOM编号" prop="code">
<el-input v-model="formData.code" placeholder="请输入BOM编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="版本" prop="version">
<el-input v-model="formData.version" placeholder="例如V1.0" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="BOM名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入BOM名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品条码" prop="productBarCode">
<el-input v-model="formData.productBarCode" placeholder="请输入成品条码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入成品名称" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="formSubmitting" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 明细弹窗只读演示用 -->
<el-dialog v-model="detailDialogVisible" title="BOM 明细" width="800px" append-to-body>
<div class="mb-2 text-gray-500">
<span>成品</span>
<span class="mr-2">{{ currentDetail?.productName }}</span>
<span class="text-gray-400">({{ currentDetail?.productBarCode }})</span>
</div>
<el-table :data="detailItems" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
<el-table-column prop="materialName" label="物料名称" min-width="160" />
<el-table-column prop="unit" label="单位" width="90" align="center" />
<el-table-column prop="quantity" label="用量" width="100" align="center" />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
</el-table>
<template #footer>
<el-button type="primary" @click="detailDialogVisible = false">知道了</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Icon } from '@/components/Icon'
// 可替换的 Service 层:未来直接改为真实接口调用即可
// 约定:列表分页、增删改查、明细查询
interface BomItemVO {
id: number
materialCode: string
materialName: string
unit: string
quantity: number
remark?: string
}
interface BomVO {
id: number
code: string
name: string
productBarCode: string
productName: string
version: string
status: 0 | 1
itemCount: number
remark?: string
updateTime: string
}
interface BomQuery {
pageNo: number
pageSize: number
code?: string
name?: string
productBarCode?: string
status?: number | undefined
}
interface BomPageResult {
list: BomVO[]
total: number
}
interface BomService {
getPage(params: BomQuery): Promise<BomPageResult>
get(id: number): Promise<BomVO | null>
getItems(id: number): Promise<BomItemVO[]>
create(data: Partial<BomVO>): Promise<void>
update(data: Partial<BomVO>): Promise<void>
delete(id: number): Promise<void>
deleteBatch(ids: number[]): Promise<void>
}
// Mock 数据源(可无缝替换为真实接口)
const mockDb = reactive({
boms: [
{
id: 1,
code: 'BOM-202501-001',
name: 'A产品标准BOM',
productBarCode: 'P-100001',
productName: 'A产品',
version: 'V1.0',
status: 1,
itemCount: 3,
remark: '首版',
updateTime: '2025-01-01 10:00:00'
},
{
id: 2,
code: 'BOM-202501-002',
name: 'B产品试制BOM',
productBarCode: 'P-100002',
productName: 'B产品',
version: 'V0.9',
status: 0,
itemCount: 2,
remark: '试制用',
updateTime: '2025-01-03 15:20:00'
}
] as BomVO[],
items: new Map<number, BomItemVO[]>([
[
1,
[
{ id: 11, materialCode: 'M-200001', materialName: '外壳', unit: '个', quantity: 1 },
{ id: 12, materialCode: 'M-200002', materialName: '主板', unit: '块', quantity: 1 },
{ id: 13, materialCode: 'M-200003', materialName: '螺丝', unit: '颗', quantity: 6 }
]
],
[
2,
[
{ id: 21, materialCode: 'M-300001', materialName: '外壳(B)', unit: '个', quantity: 1 },
{ id: 22, materialCode: 'M-300002', materialName: '主板(B)', unit: '块', quantity: 1 }
]
]
])
})
const mockService: BomService = {
async getPage(params: BomQuery): Promise<BomPageResult> {
const { pageNo, pageSize, code, name, productBarCode, status } = params
let data = [...mockDb.boms]
if (code) data = data.filter((b) => b.code.includes(code))
if (name) data = data.filter((b) => b.name.includes(name))
if (productBarCode) data = data.filter((b) => b.productBarCode.includes(productBarCode))
if (status !== undefined && status !== null && status !== ('' as any)) {
data = data.filter((b) => b.status === (status as 0 | 1))
}
const total = data.length
const start = (pageNo - 1) * pageSize
const end = start + pageSize
const pageList = data.slice(start, end)
await sleep(200)
return { list: pageList, total }
},
async get(id: number): Promise<BomVO | null> {
await sleep(120)
return mockDb.boms.find((b) => b.id === id) || null
},
async getItems(id: number): Promise<BomItemVO[]> {
await sleep(180)
return mockDb.items.get(id) || []
},
async create(data: Partial<BomVO>): Promise<void> {
await sleep(150)
const id = Math.max(0, ...mockDb.boms.map((b) => b.id)) + 1
const now = new Date().toISOString().replace('T', ' ').slice(0, 19)
mockDb.boms.unshift({
id,
code: data.code || `BOM-${id}`,
name: data.name || `未命名BOM-${id}`,
productBarCode: data.productBarCode || '',
productName: data.productName || '',
version: data.version || 'V1.0',
status: (data.status as 0 | 1) ?? 1,
itemCount: 0,
remark: data.remark || '',
updateTime: now
})
mockDb.items.set(id, [])
},
async update(data: Partial<BomVO>): Promise<void> {
await sleep(150)
if (!data.id) return
const idx = mockDb.boms.findIndex((b) => b.id === data.id)
if (idx >= 0) {
const now = new Date().toISOString().replace('T', ' ').slice(0, 19)
mockDb.boms[idx] = { ...mockDb.boms[idx], ...data, updateTime: now } as BomVO
}
},
async delete(id: number): Promise<void> {
await sleep(120)
const idx = mockDb.boms.findIndex((b) => b.id === id)
if (idx >= 0) mockDb.boms.splice(idx, 1)
mockDb.items.delete(id)
},
async deleteBatch(ids: number[]): Promise<void> {
await sleep(180)
for (const id of ids) {
const i = mockDb.boms.findIndex((b) => b.id === id)
if (i >= 0) mockDb.boms.splice(i, 1)
mockDb.items.delete(id)
}
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// 如果要替换真实接口,仅需在此处将 service 指向真实实现
const service: BomService = mockService
// 页面状态
const loading = ref(false)
const list = ref<BomVO[]>([])
const total = ref(0)
const selectionList = ref<BomVO[]>([])
const queryParams = reactive<BomQuery>({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
productBarCode: undefined,
status: undefined
})
const formDialogVisible = ref(false)
const formDialogTitle = ref('')
const formSubmitting = ref(false)
const formRef = ref()
const formData = reactive<Partial<BomVO>>({
id: undefined,
code: '',
name: '',
productBarCode: '',
productName: '',
version: 'V1.0',
status: 1,
remark: ''
})
const formRules = reactive({
code: [{ required: true, message: '请输入BOM编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入BOM名称', trigger: 'blur' }],
productBarCode: [{ required: true, message: '请输入产品条码', trigger: 'blur' }],
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
})
const detailDialogVisible = ref(false)
const currentDetail = ref<BomVO | null>(null)
const detailItems = ref<BomItemVO[]>([])
async function getList() {
loading.value = true
try {
const res = await service.getPage({ ...queryParams })
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
function handleQuery() {
queryParams.pageNo = 1
getList()
}
function resetQuery() {
queryParams.pageNo = 1
queryParams.code = undefined
queryParams.name = undefined
queryParams.productBarCode = undefined
queryParams.status = undefined
getList()
}
function onSelectionChange(rows: BomVO[]) {
selectionList.value = rows
}
function openForm(type: 'create' | 'update', id?: number) {
formDialogVisible.value = true
formDialogTitle.value = type === 'create' ? '新增BOM' : '编辑BOM'
if (type === 'create') {
Object.assign(formData, {
id: undefined,
code: '',
name: '',
productBarCode: '',
productName: '',
version: 'V1.0',
status: 1,
remark: ''
})
} else if (id) {
service.get(id).then((data) => {
if (data) Object.assign(formData, data)
})
}
}
function submitForm() {
;(formRef.value as any)?.validate(async (valid: boolean) => {
if (!valid) return
formSubmitting.value = true
try {
if (formData.id) {
await service.update(formData)
ElMessage.success('修改成功')
} else {
await service.create(formData)
ElMessage.success('新增成功')
}
formDialogVisible.value = false
getList()
} finally {
formSubmitting.value = false
}
})
}
async function handleDelete(id: number) {
try {
await ElMessageBox.confirm('确认删除该BOM吗', '提示', { type: 'warning' })
await service.delete(id)
ElMessage.success('删除成功')
getList()
} catch {}
}
async function handleBatchDelete() {
if (!selectionList.value.length) return
try {
await ElMessageBox.confirm(`确认删除选中的 ${selectionList.value.length} 条记录吗?`, '提示', {
type: 'warning'
})
await service.deleteBatch(selectionList.value.map((i) => i.id))
ElMessage.success('删除成功')
getList()
} catch {}
}
async function openDetail(row: BomVO) {
currentDetail.value = row
detailDialogVisible.value = true
detailItems.value = []
const items = await service.getItems(row.id)
detailItems.value = items
}
getList()
</script>
<style scoped></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
<template>
<Dialog title="查看领料单" v-model="dialogVisible" width="80%">
<Descriptions :schema="schema" :data="detailData" />
<el-divider content-position="left">领料明细</el-divider>
<el-table :data="detailData.items" border style="width: 100%" max-height="480">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="物料信息" min-width="240">
<template #default="{ row }">
<div class="text-13px text-gray-600">编码{{ row.materialCode }}</div>
<div class="text-13px">名称{{ row.materialName }}</div>
</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="80" />
<el-table-column label="计划数量" prop="planQuantity" width="120" />
<el-table-column label="实际数量" prop="actualQuantity" width="120" />
<el-table-column label="仓库" prop="warehouseName" width="140" />
<el-table-column label="过磅单号" prop="no" width="160" />
<el-table-column label="备注" prop="remark" min-width="160" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-tooltip content="过磅详情" placement="top" v-if="row.purchaseId">
<el-button link type="primary" @click="openWeighDetail(row.purchaseId)">
<Icon icon="ep:document" />
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</Dialog>
<!-- 过磅单详情弹窗 -->
<Dialog title="过磅单详情" v-model="weighDialogVisible" width="50%">
<el-descriptions :column="2" border>
<el-descriptions-item label="过磅单号">{{ weighDetail?.no || '-' }}</el-descriptions-item>
<el-descriptions-item label="入库状态">{{
weighDetail?.inStatus === 1 ? '已入库' : '未入库'
}}</el-descriptions-item>
<el-descriptions-item label="产品">{{
weighDetail?.productName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="车牌">{{
weighDetail?.vehicleNumber || '-'
}}</el-descriptions-item>
<el-descriptions-item label="毛重">{{
weighDetail?.grossWeight != null ? weighDetail.grossWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="皮重">{{
weighDetail?.tareWeight != null ? weighDetail.tareWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="净重">{{
weighDetail?.netWeight != null ? weighDetail.netWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="计划重量">{{
weighDetail?.plannedWeight != null ? weighDetail.plannedWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="杂质率">{{
weighDetail?.impurityRate != null ? (weighDetail.impurityRate * 100).toFixed(2) + '%' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="供应商">{{
weighDetail?.supplierName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="供应商类型">{{
weighDetail?.supplierName2 || '-'
}}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{
weighDetail?.idCardNumber || '-'
}}</el-descriptions-item>
<el-descriptions-item label="司机">{{ weighDetail?.driver || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员">{{
weighDetail?.administrator || '-'
}}</el-descriptions-item>
<el-descriptions-item label="过磅员">{{ weighDetail?.weigher || '-' }}</el-descriptions-item>
<el-descriptions-item label="产地证编号">{{
weighDetail?.originCertificateNumber || '-'
}}</el-descriptions-item>
<el-descriptions-item label="附件">
<template v-if="weighDetail?.fileUrl">
<a :href="weighDetail.fileUrl" target="_blank">查看附件</a>
</template>
<template v-else>-</template>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{
weighDetail?.remark || '-'
}}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{
weighDetail?.createTime ? formatDate(weighDetail.createTime) : '-'
}}</el-descriptions-item>
</el-descriptions>
<!-- 检验信息部分 - 仅当有检验数据时显示 -->
<template v-if="hasInspectionData">
<el-divider content-position="left">检验信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="取样日期">{{
weighDetail?.sampleDate ? formatDate(weighDetail.sampleDate) : '-'
}}</el-descriptions-item>
<el-descriptions-item label="取样地点">{{
weighDetail?.sampleLocation || '-'
}}</el-descriptions-item>
<el-descriptions-item label="代表重量">{{
weighDetail?.representativeWeight != null ? weighDetail.representativeWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="样品重量">{{
weighDetail?.sampleWeight != null ? weighDetail.sampleWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="杂质重量">{{
weighDetail?.impurityWeight != null ? weighDetail.impurityWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="沙土重量">{{
weighDetail?.sandWeight != null ? weighDetail.sandWeight + ' kg' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="感官">{{
weighDetail?.sensoryEvaluation || '-'
}}</el-descriptions-item>
<el-descriptions-item label="合格率">{{
weighDetail?.qualificationRate ? weighDetail.qualificationRate + '%' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="水分">{{
weighDetail?.moistureContent ? weighDetail.moistureContent + '%' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="含糖">{{
weighDetail?.sugarContent ? weighDetail.sugarContent + '%' : '-'
}}</el-descriptions-item>
<el-descriptions-item label="RA值">{{
weighDetail?.raValue || '-'
}}</el-descriptions-item>
<el-descriptions-item label="STV值">{{
weighDetail?.stvValue || '-'
}}</el-descriptions-item>
<el-descriptions-item label="化验员">{{
weighDetail?.labTechnician || '-'
}}</el-descriptions-item>
<el-descriptions-item label="检验备注" :span="2">{{
weighDetail?.inspectionRemark || '-'
}}</el-descriptions-item>
</el-descriptions>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { getMaterialRequisition } from '@/api/mes/production/material-requisition'
import { formatDate } from '@/utils/formatTime'
import { Icon } from '@/components/Icon'
import { WeighApi } from '@/api/erp/purchase/weigh'
const dialogVisible = ref(false)
const detailData = ref<any>({
items: []
})
const weighDialogVisible = ref(false)
const weighDetail = ref<any>(null)
// 判断是否有检验数据
const hasInspectionData = computed(() => {
if (!weighDetail.value) return false
return !!(
weighDetail.value.sampleDate ||
weighDetail.value.sampleLocation ||
weighDetail.value.representativeWeight ||
weighDetail.value.sampleWeight ||
weighDetail.value.impurityWeight ||
weighDetail.value.sandWeight ||
weighDetail.value.sensoryEvaluation ||
weighDetail.value.qualificationRate ||
weighDetail.value.moistureContent ||
weighDetail.value.sugarContent ||
weighDetail.value.raValue ||
weighDetail.value.stvValue ||
weighDetail.value.labTechnician ||
weighDetail.value.inspectionRemark
)
})
const schema = [
{
label: '领料单号',
field: 'code'
},
{
label: '工单编号',
field: 'orderCode'
},
{
label: '领料时间',
field: 'requisitionTime',
formatter: (val: string) => (val ? formatDate(val) : '')
},
{
label: '状态',
field: 'status',
formatter: (val: number) => {
const statusMap: Record<number, string> = {
0: '草稿',
1: '待审核',
2: '已审核',
3: '已领料',
4: '已取消'
}
return statusMap[val] || '未知'
}
},
{
label: '申请人',
field: 'applicantName'
},
{
label: '审批人',
field: 'approverName'
},
{
label: '备注',
field: 'remark'
},
{
label: '创建时间',
field: 'createTime',
formatter: (val: string) => (val ? formatDate(val) : '')
}
]
const open = async (id: number) => {
dialogVisible.value = true
const res = await getMaterialRequisition(id)
detailData.value = res
}
const openWeighDetail = async (purchaseId: number) => {
try {
const res: any = await WeighApi.getWeigh(purchaseId)
weighDetail.value = res?.data || res
weighDialogVisible.value = true
} catch (e) {
console.error('获取过磅单详情失败', e)
}
}
defineExpose({
open
})
</script>

View File

@@ -0,0 +1,357 @@
<template>
<doc-alert title="【MES】生产领料管理" url="https://doc.iocoder.cn/mes/material-requisition/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="领料单号" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入领料单号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工单编号" prop="orderCode">
<el-input
v-model="queryParams.orderCode"
placeholder="请输入工单编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option label="待审核" :value="1" />
<el-option label="已审核" :value="2" />
<el-option label="已领料" :value="3" />
<el-option label="已取消" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:material-requisition:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<!-- <el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['mes:material-requisition:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button> -->
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column type="selection" width="50" />
<el-table-column label="领料单号" align="center" prop="code" min-width="120" />
<el-table-column label="批次信息" align="center" prop="orderCode" min-width="120" />
<el-table-column label="领料时间" align="center" prop="requisitionTime" min-width="150" sortable>
<template #default="scope">
{{ formatDate(scope.row.requisitionTime) }}
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" min-width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="申请人" align="center" prop="applicantName" min-width="100" />
<el-table-column label="审批人" align="center" prop="approverName" min-width="100" />
<el-table-column label="备注" align="center" prop="remark" min-width="120" />
<el-table-column label="创建时间" align="center" prop="createTime" min-width="150" sortable>
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" min-width="160">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
@click="handleView(scope.row.id)"
v-hasPermi="['mes:material-requisition:query']"
>
<Icon icon="ep:view" />
</el-button>
</el-tooltip>
<el-tooltip content="编辑" placement="top">
<el-button
v-if="scope.row.status === 1"
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:material-requisition:update']"
>
<Icon icon="ep:edit" />
</el-button>
</el-tooltip>
<el-tooltip content="审核" placement="top">
<el-button
v-if="scope.row.status === 1"
link
type="success"
@click="handleApprove(scope.row.id)"
v-hasPermi="['mes:material-requisition:approve']"
>
<Icon icon="ep:check" />
</el-button>
</el-tooltip>
<el-tooltip content="确认领料" placement="top">
<el-button
v-if="scope.row.status === 2"
link
type="success"
@click="handleComplete(scope.row.id)"
v-hasPermi="['mes:material-requisition:complete']"
>
<Icon icon="ep:finished" />
</el-button>
</el-tooltip>
<el-tooltip content="反审核" placement="top">
<el-button
v-if="scope.row.status === 2"
link
type="warning"
@click="handleReverse(scope.row.id)"
v-hasPermi="['mes:material-requisition:reverse']"
>
<Icon icon="ep:refresh-left" />
</el-button>
</el-tooltip>
<!-- <el-tooltip content="删除" placement="top">
<el-button
v-if="[0, 4].includes(scope.row.status)"
link
type="danger"
@click="handleDelete([scope.row.id])"
v-hasPermi="['mes:material-requisition:delete']"
>
<Icon icon="ep:delete" />
</el-button>
</el-tooltip> -->
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<MaterialRequisitionForm ref="formRef" @success="getList" />
<!-- 查看弹窗 -->
<MaterialRequisitionView ref="viewRef" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatDate } from '@/utils/formatTime'
import {
getMaterialRequisitionPage,
approveMaterialRequisition,
reverseMaterialRequisition
} from '@/api/mes/production/material-requisition'
import { Icon } from '@/components/Icon'
import MaterialRequisitionForm from './MaterialRequisitionForm.vue'
import MaterialRequisitionView from './MaterialRequisitionView.vue'
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const currentUser = computed(() => userStore.user)
const loading = ref(true)
const list = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
orderCode: undefined,
status: undefined,
createTime: undefined
})
const queryFormRef = ref()
const formRef = ref()
const viewRef = ref()
const selectionList = ref<any[]>([])
const getStatusType = (status: number) => {
const types = {
1: 'warning',
2: 'success',
3: 'success',
4: 'danger'
}
return types[status]
}
const getStatusText = (status: number) => {
const texts = {
1: '待审核',
2: '已审核',
3: '已领料',
4: '已取消'
}
return texts[status]
}
const getList = async () => {
loading.value = true
try {
const res = await getMaterialRequisitionPage(queryParams)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields?.()
queryParams.code = undefined
queryParams.orderCode = undefined
queryParams.status = undefined
queryParams.createTime = undefined
handleQuery()
}
const openForm = (type: 'create' | 'update', id?: number) => {
console.log('打开表单:', type, id)
formRef.value.open(type, id)
}
const handleView = (id: number) => {
console.log('查看详情:', id)
viewRef.value.open(id)
}
const handleApprove = async (id: number) => {
try {
await ElMessageBox.confirm('确定要审核通过该领料单吗?', '提示', { type: 'warning' })
await approveMaterialRequisition({
id,
approved: true,
approverId: currentUser.value.id,
approverName: currentUser.value.nickname,
remark: ''
})
ElMessage.success('审核成功')
getList()
} catch {}
}
const handleComplete = async (id: number) => {
console.log('确认领料:', id)
formRef.value.openComplete(id)
}
const handleReverse = async (id: number) => {
try {
const { value: remark } = await ElMessageBox.prompt('请输入反审核原因', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValidator: (value) => {
if (!value) {
return '反审核原因不能为空'
}
return true
}
})
await reverseMaterialRequisition({
id,
reverserId: currentUser.value.id,
reverserName: currentUser.value.nickname,
remark
})
ElMessage.success('反审核成功')
getList()
} catch {}
}
const handleSelectionChange = (rows: any[]) => {
selectionList.value = rows
}
/** 行点击操作 */
const handleRowClick = (row: any, _column: any, event: MouseEvent) => {
// 检查是否点击了按钮、链接或其他交互元素
const target = event.target as HTMLElement
if (
target.tagName === 'BUTTON' ||
target.tagName === 'A' ||
target.tagName === 'I' ||
target.tagName === 'svg' ||
target.closest('button') ||
target.closest('a') ||
target.closest('.el-button') ||
target.closest('.el-checkbox')
) {
return
}
// 待审核状态打开编辑页面,其他状态打开查看页面
if (row.status === 1) {
openForm('update', row.id)
} else {
handleView(row.id)
}
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,417 @@
<template>
<doc-alert title="【MES】生产计划详情" url="https://doc.iocoder.cn/mes/production-plan/" />
<ContentWrap v-loading="loading">
<!-- 计划基本信息 -->
<el-descriptions title="计划基本信息" :column="3" border>
<el-descriptions-item label="计划编号">{{ planInfo.planCode }}</el-descriptions-item>
<el-descriptions-item label="计划名称">{{ planInfo.planName }}</el-descriptions-item>
<el-descriptions-item label="计划类型">
<el-tag :type="getPlanTypeTagType(planInfo.planType)">
{{ getPlanTypeText(planInfo.planType) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="计划周期">{{ planInfo.planPeriod }}</el-descriptions-item>
<el-descriptions-item label="开始日期">{{ planInfo.startDate }}</el-descriptions-item>
<el-descriptions-item label="结束日期">{{ planInfo.endDate }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(planInfo.status)">
{{ getStatusText(planInfo.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ planInfo.createTime }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{ planInfo.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 完成情况统计 -->
<el-card class="mt-20px" shadow="never">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold">完成情况统计</span>
<el-button
type="primary"
size="small"
@click="handleRefresh"
>
<Icon icon="ep:refresh" class="mr-5px" /> 刷新进度
</el-button>
</div>
</template>
<el-row :gutter="20">
<el-col :span="6">
<div class="stat-card">
<div class="stat-label">总计划数量</div>
<div class="stat-value text-blue-600">{{ planInfo.totalPlanQuantity || 0 }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-label">已完成数量</div>
<div class="stat-value text-green-600">{{ planInfo.totalCompletedQuantity || 0 }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-label">进行中数量</div>
<div class="stat-value text-orange-600">{{ planInfo.totalInProgressQuantity || 0 }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-label">整体完成率</div>
<div class="stat-value" :style="{ color: getProgressColor(planInfo.overallCompletionRate) }">
{{ planInfo.overallCompletionRate || 0 }}%
</div>
</div>
</el-col>
</el-row>
<el-progress
:percentage="planInfo.overallCompletionRate || 0"
:color="getProgressColor(planInfo.overallCompletionRate)"
class="mt-20px"
/>
</el-card>
<!-- 计划明细 -->
<el-card class="mt-20px" shadow="never">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold">计划明细</span>
<el-button type="primary" size="small" @click="handleQueryOrders">
<Icon icon="ep:search" class="mr-5px" /> 查询工单
</el-button>
</div>
</template>
<el-table :data="planInfo.items" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="产品信息" min-width="150" align="center">
<template #default="{ row }">
<div class="font-bold">{{ row.productName }}</div>
<div class="text-sm text-gray-500">{{ row.productCode }}</div>
</template>
</el-table-column>
<el-table-column label="工序路线" prop="routeName" min-width="120" align="center" />
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
<el-table-column label="已完成" prop="completedQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-green-600 font-bold">{{ row.completedQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="进行中" prop="inProgressQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-orange-600 font-bold">{{ row.inProgressQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="完成率" prop="completionRate" width="150" align="center">
<template #default="{ row }">
<el-progress
:percentage="row.completionRate || 0"
:color="getProgressColor(row.completionRate)"
/>
</template>
</el-table-column>
<el-table-column label="优先级" prop="priority" width="80" align="center" />
<el-table-column label="计划时间" min-width="180" align="center">
<template #default="{ row }">
<div>开始{{ row.plannedStartDate || '-' }}</div>
<div class="mt-5px">结束{{ row.plannedEndDate || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="实际时间" min-width="180" align="center">
<template #default="{ row }">
<div>开始{{ row.actualStartDate || '-' }}</div>
<div class="mt-5px">结束{{ row.actualEndDate || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
</el-table>
</el-card>
<!-- 工单列表 -->
<el-card class="mt-20px" shadow="never" v-if="showOrders">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold">工单列表</span>
<el-tag type="info"> {{ orderList.length }} 条工单</el-tag>
</div>
</template>
<el-table :data="orderList" border v-loading="orderLoading">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="工单编号" prop="code" min-width="150" align="center" />
<el-table-column label="工单名称" prop="name" min-width="150" align="center" />
<el-table-column label="产品信息" min-width="150" align="center">
<template #default="{ row }">
<div class="font-bold">{{ row.productName }}</div>
<div class="text-sm text-gray-500">{{ row.productCode }}</div>
</template>
</el-table-column>
<el-table-column label="工艺路线" prop="routeName" min-width="120" align="center" />
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
<el-table-column label="已生产" prop="producedQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-blue-600 font-bold">{{ row.producedQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="合格数" prop="qualifiedQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-green-600 font-bold">{{ row.qualifiedQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="不合格" prop="unqualifiedQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-red-600 font-bold">{{ row.unqualifiedQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="优先级" prop="priority" width="80" align="center" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getOrderStatusType(row.status)">
{{ getOrderStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="计划时间" min-width="180" align="center">
<template #default="{ row }">
<div>开始{{ row.planStartTime || '-' }}</div>
<div class="mt-5px">结束{{ row.planEndTime || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="实际时间" min-width="180" align="center">
<template #default="{ row }">
<div>开始{{ row.actualStartTime || '-' }}</div>
<div class="mt-5px">结束{{ row.actualEndTime || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
</el-table>
</el-card>
<!-- 操作按钮 -->
<div class="mt-20px text-center">
<el-button @click="goBack">返回</el-button>
<el-button
type="primary"
@click="handleEdit"
v-if="planInfo.status === 0"
v-hasPermi="['mes:production-plan:update']"
>
编辑计划
</el-button>
<el-button
type="success"
@click="handlePublish"
v-if="planInfo.status === 0"
v-hasPermi="['mes:production-plan:publish']"
>
发布计划
</el-button>
<!-- <el-button-->
<!-- type="warning"-->
<!-- @click="handleGenerateOrders"-->
<!-- v-if="planInfo.status >= 1 && planInfo.status <= 2"-->
<!-- v-hasPermi="['mes:production-plan:generate-orders']"-->
<!-- >-->
<!-- 生成工单-->
<!-- </el-button>-->
</div>
</ContentWrap>
<!-- 生成工单弹窗 -->
<GenerateOrderDialog ref="generateOrderRef" @success="getDetail" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Icon } from '@/components/Icon'
import GenerateOrderDialog from '../list/GenerateOrderDialog.vue'
import {
getProductionPlan,
publishProductionPlan,
updatePlanProgress
} from '@/api/mes/production/plan'
import { YVHgetWorkOrderPage } from '@/api/mes/production/workorder'
defineOptions({ name: 'MesProductionPlanDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const planInfo = ref<any>({})
const generateOrderRef = ref()
const showOrders = ref(false)
const orderLoading = ref(false)
const orderList = ref<any[]>([])
const planId = ref<number>(Number(route.query.id))
const getDetail = async () => {
loading.value = true
try {
const res = await getProductionPlan(planId.value)
planInfo.value = res
} catch (error) {
console.error('获取计划详情失败:', error)
ElMessage.error('获取计划详情失败')
} finally {
loading.value = false
}
}
const handleRefresh = async () => {
try {
await updatePlanProgress(planId.value)
ElMessage.success('进度更新成功')
getDetail()
} catch (error) {
console.error('更新进度失败:', error)
}
}
const handleEdit = () => {
router.push({
path: '/mes/production/plan/list',
query: { editId: planId.value }
})
}
const handlePublish = async () => {
try {
await ElMessageBox.confirm('确定要发布该计划吗?发布后将不能修改。', '提示', {
type: 'warning'
})
await publishProductionPlan(planId.value)
ElMessage.success('发布成功')
getDetail()
} catch {}
}
const handleGenerateOrders = () => {
generateOrderRef.value.open(planInfo.value)
}
const goBack = () => {
router.back()
}
const handleQueryOrders = async () => {
orderLoading.value = true
showOrders.value = true
try {
const res = await YVHgetWorkOrderPage({
sourceType: 2,
sourceId: planId.value,
pageNo: 1,
pageSize: 100
})
orderList.value = res.list || []
ElMessage.success(`查询成功,共找到 ${orderList.value.length} 条工单`)
} catch (error) {
console.error('查询工单失败:', error)
ElMessage.error('查询工单失败')
} finally {
orderLoading.value = false
}
}
const getOrderStatusText = (status: number) => {
const map: Record<number, string> = {
0: '草稿',
1: '待审核',
2: '已审核',
3: '生产中',
4: '已完成',
5: '已关闭',
6: '已取消',
7: '已入库'
}
return map[status] || '未知'
}
const getOrderStatusType = (status: number) => {
const map: Record<number, string> = {
0: 'info',
1: 'warning',
2: 'primary',
3: 'warning',
4: 'success',
5: 'danger',
6: 'info'
}
return map[status] || ''
}
const getPlanTypeText = (type: number) => {
const map: Record<number, string> = {
1: '年度',
2: '月度',
3: '周度'
}
return map[type] || '未知'
}
const getPlanTypeTagType = (type: number) => {
const map: Record<number, string> = {
1: 'danger',
2: 'warning',
3: 'info'
}
return map[type] || ''
}
const getStatusText = (status: number) => {
const map: Record<number, string> = {
0: '草稿',
1: '已发布',
2: '执行中',
3: '已完成',
4: '已关闭'
}
return map[status] || '未知'
}
const getStatusType = (status: number) => {
const map: Record<number, string> = {
0: 'info',
1: 'primary',
2: 'warning',
3: 'success',
4: 'danger'
}
return map[status] || ''
}
const getProgressColor = (percentage: number) => {
if (percentage < 30) return '#f56c6c'
if (percentage < 70) return '#e6a23c'
return '#67c23a'
}
onMounted(() => {
if (planId.value) {
getDetail()
}
})
</script>
<style scoped lang="scss">
.stat-card {
text-align: center;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
.stat-label {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<doc-alert title="【MES】生产执行情况" url="https://doc.iocoder.cn/mes/plan/" />
<ContentWrap>
<div
class="production-board"
:style="{
backgroundImage: backgroundImage ? `url(${backgroundImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
minHeight: '600px',
position: 'relative',
border: backgroundImage ? 'none' : '2px dashed #dcdfe6',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}"
>
<div v-if="!backgroundImage" class="upload-tip text-gray-400"> 请上传背景图片 </div>
</div>
<!-- 底部上传按钮 -->
<div class="upload-container">
<el-upload
class="upload-demo"
action="#"
:auto-upload="false"
:show-file-list="false"
accept="image/*"
:on-change="handleFileChange"
>
<template #trigger>
<el-button type="primary">
<Icon icon="ep:upload" class="mr-5px" />
{{ backgroundImage ? '更换背景图片' : '上传背景图片' }}
</el-button>
</template>
</el-upload>
<el-button v-if="backgroundImage" type="danger" @click="removeBackground">
<Icon icon="ep:delete" class="mr-5px" />
移除背景图片
</el-button>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const backgroundImage = ref('')
// 从localStorage加载背景图片
onMounted(() => {
const savedBackground = localStorage.getItem('productionBackground')
if (savedBackground) {
backgroundImage.value = savedBackground
}
})
// 处理文件选择
const handleFileChange = (file: any) => {
const isImage = file.raw.type.startsWith('image/')
const isLt2M = file.raw.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB')
return
}
// 读取文件为 base64
const reader = new FileReader()
reader.readAsDataURL(file.raw)
reader.onload = (e) => {
const base64 = e.target?.result as string
backgroundImage.value = base64
// 保存到localStorage
localStorage.setItem('productionBackground', base64)
ElMessage.success('背景图片设置成功')
}
}
// 移除背景图片
const removeBackground = () => {
backgroundImage.value = ''
localStorage.removeItem('productionBackground')
ElMessage.success('背景图片已移除')
}
</script>
<style scoped>
.production-board {
margin-bottom: 20px;
transition: all 0.3s;
}
.upload-tip {
font-size: 16px;
}
.upload-container {
display: flex;
gap: 10px;
justify-content: center;
padding: 20px 0;
border-top: 1px solid #eee;
}
.production-content {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
backdrop-filter: blur(10px);
}
.data-card {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
min-width: 200px;
}
.data-card h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: #303133;
}
.data-item {
display: flex;
flex-direction: column;
gap: 10px;
}
.data-item span {
color: #606266;
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<Dialog v-model="dialogVisible" title="生成月度计划" width="600px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-alert
title="提示"
type="info"
:closable="false"
class="mb-15px"
>
<template #default>
<div>从年度计划生成月度计划系统将根据所选月份和分配策略自动生成对应的月度计划</div>
</template>
</el-alert>
<el-form-item label="年度计划" prop="annualPlanId">
<div class="w-full">
<div class="font-bold">{{ annualPlan?.planCode }}</div>
<div class="text-sm text-gray-500">{{ annualPlan?.planName }}</div>
</div>
</el-form-item>
<el-form-item label="计划周期" prop="planPeriod">
<el-tag type="danger">{{ annualPlan?.planPeriod }}</el-tag>
</el-form-item>
<el-form-item label="选择月份" prop="months" required>
<el-checkbox-group v-model="formData.months">
<el-checkbox
v-for="month in availableMonths"
:key="month"
:label="month"
:disabled="existingMonths.includes(month)"
>
{{ month }}
<el-tag v-if="existingMonths.includes(month)" type="info" size="small" class="ml-5px">
已存在
</el-tag>
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="分配策略" prop="splitStrategy">
<el-radio-group v-model="formData.splitStrategy">
<el-radio label="average">平均分配</el-radio>
<el-radio label="proportional">按比例分配</el-radio>
</el-radio-group>
<div class="text-sm text-gray-500 mt-5px">
<div v-if="formData.splitStrategy === 'average'">
将年度计划数量平均分配到各月度计划
</div>
<div v-else>
根据各月工作日比例分配计划数量
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定生成
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { generateMonthlyPlans, type GenerateMonthlyPlanReqVO } from '@/api/mes/production/plan'
defineOptions({ name: 'GenerateMonthlyPlanDialog' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const annualPlan = ref<any>(null)
const existingMonths = ref<number[]>([])
const formData = reactive<GenerateMonthlyPlanReqVO>({
annualPlanId: 0,
months: [],
splitStrategy: 'average'
})
const availableMonths = computed(() => {
return Array.from({ length: 12 }, (_, i) => i + 1)
})
const rules = {
months: [{ required: true, message: '请选择要生成的月份', trigger: 'change' }],
splitStrategy: [{ required: true, message: '请选择分配策略', trigger: 'change' }]
}
const open = (plan: any) => {
annualPlan.value = plan
formData.annualPlanId = plan.id
formData.months = []
formData.splitStrategy = 'average'
if (plan.children && plan.children.length > 0) {
existingMonths.value = plan.children
.filter((child: any) => child.planType === 2)
.map((child: any) => {
const match = child.planPeriod?.match(/-(\d{2})$/)
return match ? parseInt(match[1]) : 0
})
.filter((month: number) => month > 0)
} else {
existingMonths.value = []
}
dialogVisible.value = true
}
const handleSubmit = async () => {
try {
await formRef.value?.validate()
if (formData.months.length === 0) {
ElMessage.warning('请至少选择一个月份')
return
}
submitLoading.value = true
await generateMonthlyPlans(formData)
ElMessage.success('月度计划生成成功')
dialogVisible.value = false
emit('success')
} catch (error) {
console.error('生成月度计划失败:', error)
} finally {
submitLoading.value = false
}
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,173 @@
<template>
<el-dialog
title="从计划生成工单"
v-model="visible"
width="800px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
<el-alert
title="提示"
type="info"
:closable="false"
class="mb-20px"
>
<template #default>
<div>计划名称{{ planInfo.planName }}</div>
<div>计划周期{{ planInfo.planPeriod }}</div>
<div>总计划数量{{ planInfo.totalPlanQuantity }}</div>
</template>
</el-alert>
<el-form-item label="选择产品" prop="itemIds">
<el-table
:data="planItems"
@selection-change="handleSelectionChange"
border
max-height="300"
>
<el-table-column type="selection" width="50" />
<el-table-column label="产品名称" prop="productName" min-width="150" />
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
<el-table-column label="已完成" prop="completedQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-green-600">{{ row.completedQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="进行中" prop="inProgressQuantity" width="100" align="center">
<template #default="{ row }">
<span class="text-orange-600">{{ row.inProgressQuantity || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="完成率" prop="completionRate" width="120" align="center">
<template #default="{ row }">
<el-progress
:percentage="row.completionRate || 0"
:format="() => `${row.completionRate || 0}%`"
/>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item label="拆分策略" prop="splitStrategy">
<el-radio-group v-model="form.splitStrategy">
<el-radio label="single">单个工单不拆分</el-radio>
<el-radio label="weekly">按周拆分</el-radio>
<el-radio label="daily">按天拆分</el-radio>
</el-radio-group>
<div class="text-sm text-gray-500 mt-5px">
<div v-if="form.splitStrategy === 'single'">将整个计划明细生成为一个工单</div>
<div v-if="form.splitStrategy === 'weekly'">按周拆分每周生成一个工单</div>
<div v-if="form.splitStrategy === 'daily'">按天拆分每天生成一个工单</div>
</div>
</el-form-item>
<el-form-item label="自动审核" prop="autoApprove">
<el-switch v-model="form.autoApprove" />
<span class="text-sm text-gray-500 ml-10px">
开启后生成的工单将自动审核通过
</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">
生成工单
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { generateWorkOrders, getProductionPlan } from '@/api/mes/production/plan'
const emits = defineEmits(['success'])
const visible = ref(false)
const loading = ref(false)
const formLoading = ref(false)
const formRef = ref()
const planInfo = ref<any>({})
const planItems = ref<any[]>([])
const selectedItems = ref<any[]>([])
const form = reactive<any>({
planId: undefined,
itemIds: [],
splitStrategy: 'daily',
batchSize: undefined,
autoApprove: false
})
const rules = {
itemIds: [{ required: true, message: '请选择要生成工单的产品', trigger: 'change' }],
splitStrategy: [{ required: true, message: '请选择拆分策略', trigger: 'change' }]
}
const open = async (plan: any) => {
visible.value = true
formLoading.value = true
try {
// 重置表单
Object.assign(form, {
planId: plan.id,
itemIds: [],
splitStrategy: 'daily',
batchSize: undefined,
autoApprove: false
})
// 获取计划详情
const res = await getProductionPlan(plan.id)
planInfo.value = res
planItems.value = res.items || []
} catch (error) {
console.error('加载计划详情失败:', error)
ElMessage.error('加载数据失败,请重试')
visible.value = false
} finally {
formLoading.value = false
}
}
const handleSelectionChange = (selection: any[]) => {
selectedItems.value = selection
form.itemIds = selection.map((item) => item.id)
}
const submitForm = () => {
if (!form.itemIds || form.itemIds.length === 0) {
ElMessage.warning('请至少选择一个产品')
return
}
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const res = await generateWorkOrders(form)
ElMessage.success(`成功生成 ${res.length} 个工单`)
visible.value = false
emits('success')
} catch (error) {
console.error('生成工单失败:', error)
} finally {
loading.value = false
}
})
}
const handleClose = () => {
formRef.value?.resetFields()
selectedItems.value = []
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,356 @@
<template>
<el-dialog
title="生产计划分析"
v-model="visible"
width="1000px"
@close="handleClose"
:close-on-click-modal="false"
>
<div v-loading="loading">
<!-- 基本信息卡片 -->
<el-card shadow="never" class="mb-15px">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold">计划概览</span>
<el-tag type="primary">{{ analysisData?.planCode }}</el-tag>
</div>
</template>
<el-row :gutter="20">
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">计划名称</div>
<div class="stat-value text-blue-600">{{ analysisData?.planName }}</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">计划总数量</div>
<div class="stat-value text-gray-600">{{ analysisData?.totalPlanQuantity }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">已完成数量</div>
<div class="stat-value text-green-600">{{ analysisData?.totalCompletedQuantity }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">剩余数量</div>
<div class="stat-value text-orange-600">{{ analysisData?.totalRemainingQuantity }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">预计生产周期</div>
<div class="stat-value text-blue-500">{{ analysisData?.estimatedProductionHours }} 小时</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">预计交货日期</div>
<div class="stat-value text-purple-600">{{ analysisData?.estimatedDeliveryDate }}</div>
</div>
</el-col>
</el-row>
<el-alert type="info" :closable="false" class="mt-10px">
<template #title>
<span> 以下物料需求分析基于<strong>剩余未完成数量</strong>计算</span>
</template>
</el-alert>
</el-card>
<!-- 风险提示 -->
<el-alert
v-if="analysisData?.riskWarnings && analysisData.riskWarnings.length > 0"
title="风险提示"
type="warning"
:closable="false"
class="mb-15px"
>
<template #default>
<div v-for="(warning, index) in analysisData.riskWarnings" :key="index" class="mb-5px">
{{ warning }}
</div>
</template>
</el-alert>
<!-- 采购建议 -->
<el-card shadow="never" class="mb-15px">
<template #header>
<span class="font-bold">采购建议</span>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="建议采购提前天数">
<el-tag type="warning">{{ analysisData?.suggestedPurchaseLeadDays }} </el-tag>
</el-descriptions-item>
<el-descriptions-item label="建议采购日期">
<el-tag type="danger">{{ analysisData?.suggestedPurchaseDate }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 辅料需求汇总 -->
<el-card shadow="never" class="mb-15px">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold">辅料需求汇总</span>
<el-tag type="info"> {{ analysisData?.materialRequirements?.length || 0 }} 种物料</el-tag>
</div>
</template>
<el-table :data="analysisData?.materialRequirements" border max-height="300">
<el-table-column label="物料编码" prop="materialCode" width="100" align="center" />
<el-table-column label="物料名称" prop="materialName" min-width="120" align="center" />
<el-table-column label="类型" prop="materialType" width="90" align="center" />
<el-table-column label="需求数量" width="100" align="center">
<template #default="{ row }">
{{ row.requiredQuantity }} {{ row.unit }}
</template>
</el-table-column>
<el-table-column label="当前库存" width="100" align="center">
<template #default="{ row }">
{{ row.currentStock }} {{ row.unit }}
</template>
</el-table-column>
<el-table-column label="库存缺口" width="100" align="center">
<template #default="{ row }">
<span :class="getGapClass(row.stockGap)">
{{ row.stockGap > 0 ? '+' : '' }}{{ row.stockGap }} {{ row.unit }}
</span>
</template>
</el-table-column>
<el-table-column label="库存状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="getStockStatusType(row.stockStatus)" size="small">
{{ getStockStatusText(row.stockStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="建议采购" width="100" align="center">
<template #default="{ row }">
<span class="text-orange-600 font-bold" v-if="row.suggestedPurchaseQuantity > 0">
{{ row.suggestedPurchaseQuantity }} {{ row.unit }}
</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="采购提前期" width="90" align="center">
<template #default="{ row }">
{{ row.purchaseLeadTime }}
</template>
</el-table-column>
<el-table-column label="预计金额" width="100" align="center">
<template #default="{ row }">
<span v-if="row.estimatedPurchaseAmount > 0" class="text-red-600">
¥{{ row.estimatedPurchaseAmount }}
</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
</el-table>
<div class="mt-10px text-right" v-if="totalPurchaseAmount > 0">
<span class="text-gray-500">预计采购总金额</span>
<span class="text-red-600 font-bold text-lg">¥{{ totalPurchaseAmount.toFixed(2) }}</span>
</div>
</el-card>
<!-- 产品明细分析 -->
<el-card shadow="never" class="mb-15px">
<template #header>
<span class="font-bold">产品明细分析</span>
</template>
<el-collapse v-model="activeProducts">
<el-collapse-item
v-for="(product, index) in analysisData?.productAnalysisList"
:key="index"
:name="index"
>
<template #title>
<div class="flex items-center gap-10px">
<span class="font-bold">{{ product.productName }}</span>
<el-tag size="small">{{ product.planQuantity }} {{ product.unit || '件' }}</el-tag>
<el-tag size="small" type="success">已完成 {{ product.completedQuantity }}</el-tag>
<el-tag size="small" type="warning">剩余 {{ product.remainingQuantity }}</el-tag>
<el-tag size="small" type="info">{{ product.estimatedHours }} 小时</el-tag>
</div>
</template>
<div class="pl-20px">
<el-descriptions :column="3" border size="small" class="mb-10px">
<el-descriptions-item label="产品编码">{{ product.productCode }}</el-descriptions-item>
<el-descriptions-item label="工序路线">{{ product.routeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="预计周期">{{ product.estimatedHours }} 小时</el-descriptions-item>
<el-descriptions-item label="计划开始">{{ product.plannedStartDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划结束">{{ product.plannedEndDate || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="text-sm text-gray-500 mb-5px">该产品辅料需求</div>
<el-table :data="product.materialRequirements" border size="small">
<el-table-column label="物料名称" prop="materialName" min-width="100" />
<el-table-column label="需求数量" width="100" align="center">
<template #default="{ row }">
{{ row.requiredQuantity }} {{ row.unit }}
</template>
</el-table-column>
<el-table-column label="库存状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="getStockStatusType(row.stockStatus)" size="small">
{{ getStockStatusText(row.stockStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-collapse-item>
</el-collapse>
</el-card>
<!-- 描述性分析文本 -->
<el-card shadow="never">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold">分析报告</span>
<el-button type="primary" size="small" @click="copyAnalysisText">
<Icon icon="ep:document-copy" class="mr-5px" /> 复制报告
</el-button>
</div>
</template>
<pre class="analysis-text">{{ analysisData?.analysisDescription }}</pre>
</el-card>
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
<el-button type="primary" @click="handleExport">
<Icon icon="ep:download" class="mr-5px" /> 导出报告
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import { analyzeProductionPlan, type ProductionPlanAnalysisVO } from '@/api/mes/production/plan'
const visible = ref(false)
const loading = ref(false)
const analysisData = ref<ProductionPlanAnalysisVO | null>(null)
const activeProducts = ref<number[]>([0])
const totalPurchaseAmount = computed(() => {
if (!analysisData.value?.materialRequirements) return 0
return analysisData.value.materialRequirements.reduce((sum, item) => {
return sum + (item.estimatedPurchaseAmount || 0)
}, 0)
})
const open = async (planId: number) => {
visible.value = true
loading.value = true
try {
analysisData.value = await analyzeProductionPlan(planId)
} catch (error) {
console.error('获取分析数据失败:', error)
ElMessage.error('获取分析数据失败')
visible.value = false
} finally {
loading.value = false
}
}
const handleClose = () => {
analysisData.value = null
activeProducts.value = [0]
}
const getStockStatusType = (status: string) => {
const map: Record<string, string> = {
sufficient: 'success',
warning: 'warning',
shortage: 'danger'
}
return map[status] || 'info'
}
const getStockStatusText = (status: string) => {
const map: Record<string, string> = {
sufficient: '充足',
warning: '预警',
shortage: '短缺'
}
return map[status] || '未知'
}
const getGapClass = (gap: number) => {
if (gap <= 0) return 'text-green-600'
return 'text-red-600'
}
const copyAnalysisText = async () => {
if (!analysisData.value?.analysisDescription) return
try {
await navigator.clipboard.writeText(analysisData.value.analysisDescription)
ElMessage.success('分析报告已复制到剪贴板')
} catch (error) {
ElMessage.error('复制失败,请手动复制')
}
}
const handleExport = () => {
if (!analysisData.value?.analysisDescription) return
const blob = new Blob([analysisData.value.analysisDescription], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `生产计划分析报告_${analysisData.value.planCode}_${new Date().toISOString().slice(0, 10)}.txt`
link.click()
URL.revokeObjectURL(url)
ElMessage.success('报告导出成功')
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.stat-item {
text-align: center;
padding: 10px;
.stat-label {
font-size: 12px;
color: #909399;
margin-bottom: 5px;
}
.stat-value {
font-size: 16px;
font-weight: bold;
}
}
.analysis-text {
white-space: pre-wrap;
word-wrap: break-word;
font-family: inherit;
font-size: 14px;
line-height: 1.8;
color: #303133;
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin: 0;
max-height: 400px;
overflow-y: auto;
}
:deep(.el-collapse-item__header) {
font-size: 14px;
}
:deep(.el-collapse-item__content) {
padding-bottom: 15px;
}
</style>

View File

@@ -0,0 +1,407 @@
<template>
<el-dialog
:title="formType === 'create' ? '新增生产计划' : '编辑生产计划'"
v-model="visible"
width="1200px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划编号" prop="planCode">
<el-input v-model="form.planCode" placeholder="请输入计划编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划名称" prop="planName">
<el-input v-model="form.planName" placeholder="请输入计划名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="计划类型" prop="planType">
<el-select v-model="form.planType" placeholder="请选择计划类型" class="!w-full">
<el-option label="年度计划" :value="1" />
<el-option label="月度计划" :value="2" />
<el-option label="周度计划" :value="3" />
<el-option label="日计划" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="计划周期" prop="planPeriod">
<el-input v-model="form.planPeriod" placeholder="如:2024-01" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="父计划" prop="parentPlanId">
<el-select
v-model="form.parentPlanId"
placeholder="请选择父计划"
clearable
filterable
class="!w-full"
>
<el-option
v-for="item in filteredParentPlans"
:key="item.id"
:label="`${item.planName} (${item.planCode})`"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
v-model="form.startDate"
type="date"
placeholder="选择开始日期"
value-format="YYYY-MM-DD"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker
v-model="form.endDate"
type="date"
placeholder="选择结束日期"
value-format="YYYY-MM-DD"
class="!w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
<!-- 计划明细 -->
<el-divider content-position="left">计划明细</el-divider>
<el-button type="primary" @click="handleAddItem" class="mb-10px">
<Icon icon="ep:plus" class="mr-5px" /> 添加产品
</el-button>
<el-table :data="form.items" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="产品" prop="productName" min-width="150" align="center">
<template #default="{ row, $index }">
<el-select
v-model="row.productId"
placeholder="请选择产品"
filterable
@change="handleProductChange(row, $index)"
class="!w-full"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<span class="text-gray-400 ml-10px">({{ item.barCode }})</span>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="计划数量" prop="planQuantity" width="120" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.planQuantity"
:min="1"
:precision="0"
controls-position="right"
class="!w-full"
/>
</template>
</el-table-column>
<el-table-column label="工序路线" prop="routeName" min-width="150" align="center">
<template #default="{ row }">
<el-select
v-model="row.routeId"
placeholder="请选择工序路线"
filterable
@change="handleRouteChange(row)"
class="!w-full"
>
<el-option
v-for="item in routeOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="优先级" prop="priority" width="100" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.priority"
:min="1"
:max="10"
controls-position="right"
class="!w-full"
/>
</template>
</el-table-column>
<el-table-column label="计划开始日期" prop="plannedStartDate" width="150" align="center">
<template #default="{ row }">
<el-date-picker
v-model="row.plannedStartDate"
type="date"
placeholder="开始日期"
value-format="YYYY-MM-DD"
class="!w-full"
/>
</template>
</el-table-column>
<el-table-column label="计划结束日期" prop="plannedEndDate" width="150" align="center">
<template #default="{ row }">
<el-date-picker
v-model="row.plannedEndDate"
type="date"
placeholder="结束日期"
value-format="YYYY-MM-DD"
class="!w-full"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" @click="handleDeleteItem($index)">
<Icon icon="ep:delete" />
</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import {
createProductionPlan,
updateProductionPlan,
getProductionPlan,
getProductionPlanPage
} from '@/api/mes/production/plan'
import { ProductApi } from '@/api/erp/product/product'
import { YVHgetProcessRouteList } from '@/api/mes/production/process-route'
const emits = defineEmits(['success'])
const visible = ref(false)
const loading = ref(false)
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const formRef = ref()
const productOptions = ref<any[]>([])
const routeOptions = ref<any[]>([])
const parentPlanOptions = ref<any[]>([])
const form = reactive<any>({
id: undefined,
planCode: '',
planName: '',
planType: 2,
planPeriod: '',
startDate: '',
endDate: '',
parentPlanId: undefined,
remark: '',
items: []
})
// 根据计划类型过滤可选的父计划
// 层级关系: 年度计划(1) > 月度计划(2) > 周度计划(3) > 日计划(4)
const filteredParentPlans = computed(() => {
if (!form.planType) return []
// 年度计划没有父计划
if (form.planType === 1) {
return []
}
// 月度计划只能选择年度计划作为父计划
if (form.planType === 2) {
return parentPlanOptions.value.filter(plan => plan.planType === 1)
}
// 周度计划只能选择月度计划作为父计划
if (form.planType === 3) {
return parentPlanOptions.value.filter(plan => plan.planType === 2)
}
// 日计划只能选择周度计划作为父计划
if (form.planType === 4) {
return parentPlanOptions.value.filter(plan => plan.planType === 3)
}
return []
})
const rules = {
planCode: [{ required: true, message: '请输入计划编号', trigger: 'blur' }],
planName: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
planType: [{ required: true, message: '请选择计划类型', trigger: 'change' }],
planPeriod: [{ required: true, message: '请输入计划周期', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
}
// 监听计划类型变化,自动清除不符合条件的父计划
watch(() => form.planType, () => {
// 如果当前选择的父计划不在过滤后的列表中,则清空
if (form.parentPlanId && !filteredParentPlans.value.find(p => p.id === form.parentPlanId)) {
form.parentPlanId = undefined
}
})
const open = async (type: 'create' | 'update', id?: number) => {
formType.value = type
visible.value = true
formLoading.value = true
try {
// 重置表单
Object.assign(form, {
id: undefined,
planCode: type === 'create' ? generatePlanCode() : '',
planName: '',
planType: 2,
planPeriod: getCurrentPeriod(),
startDate: '',
endDate: '',
parentPlanId: undefined,
remark: '',
items: []
})
// 并行获取产品列表、工序路线列表和父计划列表
const [products, routes, plans] = await Promise.all([
ProductApi.getProductSimpleList().catch(() => []),
YVHgetProcessRouteList().catch(() => []),
getProductionPlanPage({ pageNo: 1, pageSize: 100 }).then(res => res.list || []).catch(() => [])
])
productOptions.value = products ? products.filter((p) => p.categoryName?.includes('成品')) : []
routeOptions.value = routes || []
// 过滤掉当前编辑的计划,避免循环引用
parentPlanOptions.value = plans.filter(p => p.id !== id)
// 如果是编辑模式,获取计划详情
if (type === 'update' && id) {
const res = await getProductionPlan(id)
Object.assign(form, res)
}
} catch (error) {
console.error('初始化表单失败:', error)
ElMessage.error('加载数据失败,请重试')
visible.value = false
} finally {
formLoading.value = false
}
}
const handleAddItem = () => {
form.items.push({
productId: undefined,
productCode: '',
productName: '',
planQuantity: 1,
routeId: undefined,
routeName: '',
priority: 5,
plannedStartDate: form.startDate,
plannedEndDate: form.endDate,
remark: ''
})
}
const handleDeleteItem = (index: number) => {
form.items.splice(index, 1)
}
const handleProductChange = (row: any, index: number) => {
const product = productOptions.value.find((item) => item.id === row.productId)
if (product) {
row.productCode = product.barCode
row.productName = product.name
// 自动匹配工序路线
const route = routeOptions.value.find((r) => r.name === product.name || r.name.includes(product.name))
if (route) {
row.routeId = route.id
row.routeName = route.name
}
}
}
const handleRouteChange = (row: any) => {
const route = routeOptions.value.find((item) => item.id === row.routeId)
if (route) {
row.routeName = route.name
}
}
const submitForm = () => {
if (!form.items || form.items.length === 0) {
ElMessage.warning('请至少添加一个产品')
return
}
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (formType.value === 'create') {
await createProductionPlan(form)
ElMessage.success('新增成功')
} else {
await updateProductionPlan(form)
ElMessage.success('修改成功')
}
visible.value = false
emits('success')
} catch (error) {
console.error('保存计划失败:', error)
} finally {
loading.value = false
}
})
}
const handleClose = () => {
formRef.value?.resetFields()
}
const generatePlanCode = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
return `PLAN${year}${month}001`
}
const getCurrentPeriod = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
return `${year}-${month}`
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,531 @@
<template>
<doc-alert title="【MES】生产计划管理" url="https://doc.iocoder.cn/mes/production-plan/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="84px"
>
<el-form-item label="计划编号" prop="planCode">
<el-input
v-model="queryParams.planCode"
placeholder="请输入计划编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="计划名称" prop="planName">
<el-input
v-model="queryParams.planName"
placeholder="请输入计划名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="计划类型" prop="planType">
<el-select
v-model="queryParams.planType"
placeholder="请选择计划类型"
clearable
class="!w-240px"
>
<el-option label="年度计划" :value="1" />
<el-option label="月度计划" :value="2" />
<el-option label="周度计划" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="计划周期" prop="planPeriod">
<el-input
v-model="queryParams.planPeriod"
placeholder="如:2024-01"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="计划状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
multiple
class="!w-240px"
>
<el-option label="草稿" :value="0" />
<el-option label="已发布" :value="1" />
<el-option label="执行中" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已关闭" :value="4" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:production-plan:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增计划
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="treeList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
:tree-props="{ children: 'children' }"
:default-expand-all="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column label="计划信息" align="left" min-width="200">
<template #default="{ row }">
<div class="font-bold">{{ row.planCode }}</div>
<div class="mt-5px text-gray-500">{{ row.planName }}</div>
</template>
</el-table-column>
<el-table-column label="计划类型" align="center" width="100">
<template #default="{ row }">
<el-tag :type="getPlanTypeTagType(row.planType)">
{{ getPlanTypeText(row.planType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="计划周期" align="center" prop="planPeriod" width="120" />
<el-table-column label="计划时间" align="center" min-width="200">
<template #default="{ row }">
<div>开始{{ row.startDate }}</div>
<div class="mt-5px">结束{{ row.endDate }}</div>
</template>
</el-table-column>
<el-table-column label="计划数量" align="center" width="120">
<template #default="{ row }">
<div class="font-bold text-blue-600">{{ row.totalPlanQuantity || 0 }}</div>
</template>
</el-table-column>
<el-table-column label="完成情况" align="center" min-width="180">
<template #default="{ row }">
<div class="mb-5px">
<span class="text-sm text-gray-500">完成</span>
<span class="font-bold text-green-600">{{ row.totalCompletedQuantity || 0 }}</span>
<span class="text-sm text-gray-500 ml-10px">进行中</span>
<span class="font-bold text-orange-600">{{ row.totalInProgressQuantity || 0 }}</span>
</div>
<el-progress
:percentage="row.overallCompletionRate || 0"
:color="getProgressColor(row.overallCompletionRate)"
/>
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="480">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row.id)">
<Icon icon="ep:view" class="mr-5px" /> 详情
</el-button>
<el-button link type="success" @click="handleAnalyze(scope.row.id)">
<Icon icon="ep:data-analysis" class="mr-5px" /> 分析
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:production-plan:update']"
:disabled="scope.row.status === 0 || scope.row.hasApprovalRecords"
>
<Icon icon="ep:edit" class="mr-5px" /> 编辑
</el-button>
<el-button
link
type="success"
@click="handlePublish(scope.row.id)"
v-if="scope.row.status === 0"
v-hasPermi="['mes:production-plan:publish']"
>
<Icon icon="ep:upload" class="mr-5px" /> 发布
</el-button>
<el-button
link
type="success"
@click="handleSubmitApproval(scope.row.id)"
v-hasPermi="['mes:production-plan:submit-approval']"
v-if="scope.row.status === 0 && !scope.row.hasApprovalRecords"
>
提交审批
</el-button>
<el-button
link
type="info"
@click="handleViewApproval(scope.row.id)"
v-hasPermi="['mes:production-plan:query-approval']"
v-if="scope.row.hasApprovalRecords"
>
审批记录
</el-button>
<el-button
link
type="primary"
@click="handleProcessApproval(scope.row.id)"
v-hasPermi="['mes:approval-record:process']"
v-if="scope.row.hasApprovalRecords && scope.row.status !== 1"
>
处理审批
</el-button>
<el-button
link
type="primary"
@click="handleGenerateMonthlyPlans(scope.row)"
v-if="scope.row.planType === 1 && scope.row.status >= 1"
v-hasPermi="['mes:production-plan:generate-monthly-plans']"
>
<Icon icon="ep:calendar" class="mr-5px" /> 生成月度计划
</el-button>
<el-button
link
type="warning"
@click="handleGenerateOrders(scope.row)"
v-if="scope.row.status >= 1 && scope.row.status <= 2 && !scope.row.hasChildPlans && !scope.row.hasGeneratedOrders"
v-hasPermi="['mes:production-plan:generate-orders']"
>
<Icon icon="ep:document-add" class="mr-5px" /> 生成工单
</el-button>
<el-tooltip
content="该计划有下级计划,请在下级计划中生成工单"
placement="top"
v-if="scope.row.status >= 1 && scope.row.status <= 2 && scope.row.hasChildPlans"
>
<el-button
link
type="info"
disabled
>
<Icon icon="ep:document-add" class="mr-5px" /> 生成工单
</el-button>
</el-tooltip>
<el-button
link
type="info"
@click="handleUpdateProgress(scope.row.id)"
v-if="scope.row.status === 2"
v-hasPermi="['mes:production-plan:update-progress']"
>
<Icon icon="ep:refresh" class="mr-5px" /> 更新进度
</el-button>
<el-button
link
type="danger"
@click="handleClose(scope.row.id)"
v-if="scope.row.status === 2 || scope.row.status === 3"
v-hasPermi="['mes:production-plan:close']"
>
<Icon icon="ep:close" class="mr-5px" /> 关闭
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-if="scope.row.status === 0"
v-hasPermi="['mes:production-plan:delete']"
:disabled="scope.row.hasApprovalRecords"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ProductionPlanForm ref="formRef" @success="getList" />
<!-- 生成工单弹窗 -->
<GenerateOrderDialog ref="generateOrderRef" @success="getList" />
<!-- 生成月度计划弹窗 -->
<GenerateMonthlyPlanDialog ref="generateMonthlyPlanRef" @success="getList" />
<!-- 计划分析弹窗 -->
<PlanAnalysisDialog ref="planAnalysisRef" />
<!-- 提交审批对话框 -->
<SubmitApprovalDialog ref="submitApprovalDialogRef" @success="getList" />
<!-- 审批记录对话框 -->
<ApprovalRecordsDialog ref="approvalDialogRef" />
<!-- 处理审批对话框 -->
<ProcessApprovalDialog ref="processApprovalDialogRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Icon } from '@/components/Icon'
import ProductionPlanForm from './ProductionPlanForm.vue'
import GenerateOrderDialog from './GenerateOrderDialog.vue'
import GenerateMonthlyPlanDialog from './GenerateMonthlyPlanDialog.vue'
import PlanAnalysisDialog from './PlanAnalysisDialog.vue'
import {
getProductionPlanPage,
deleteProductionPlan,
publishProductionPlan,
closeProductionPlan,
updatePlanProgress
} from '@/api/mes/production/plan'
import { SubmitApprovalDialog, ApprovalRecordsDialog, ProcessApprovalDialog } from '@/components/Approval'
import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval/index'
defineOptions({ name: 'MesProductionPlanList' })
const router = useRouter()
const loading = ref(true)
const total = ref(0)
const list = ref<any[]>([])
const treeList = ref<any[]>([])
const queryFormRef = ref()
const formRef = ref()
const generateOrderRef = ref()
const generateMonthlyPlanRef = ref()
const planAnalysisRef = ref()
const selectionList = ref<any[]>([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
planCode: undefined,
planName: undefined,
planType: undefined,
planPeriod: undefined,
status: [] as number[]
})
const getList = async () => {
loading.value = true
try {
const res = await getProductionPlanPage(queryParams)
// 递归处理树形结构,为每个节点(包括子节点)检查审批记录
const processTreeNode = async (item: any): Promise<any> => {
try {
const records = await ApprovalRecordApi.getApprovalRecordListByBiz(item.id, 'mes_production_plan')
const processedItem = {
...item,
hasApprovalRecords: records && records.length > 0
}
// 如果有子节点,递归处理
if (item.children && item.children.length > 0) {
processedItem.children = await Promise.all(
item.children.map(child => processTreeNode(child))
)
}
return processedItem
} catch (error) {
console.error('获取审批记录失败', error)
const processedItem = {
...item,
hasApprovalRecords: false
}
// 如果有子节点,递归处理
if (item.children && item.children.length > 0) {
processedItem.children = await Promise.all(
item.children.map(child => processTreeNode(child))
)
}
return processedItem
}
}
// 处理整个树形结构
const listWithApprovalStatus = await Promise.all(
res.list.map(item => processTreeNode(item))
)
list.value = listWithApprovalStatus
total.value = res.total
// 使用处理后的数据作为树形结构
treeList.value = listWithApprovalStatus
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.planCode = undefined
queryParams.planName = undefined
queryParams.planType = undefined
queryParams.planPeriod = undefined
queryParams.status = []
handleQuery()
}
/** 提交审批 */
const submitApprovalDialogRef = ref()
const handleSubmitApproval = (id: number) => {
submitApprovalDialogRef.value.open(String(id), 'mes_production_plan')
}
/** 查看审批记录 */
const approvalDialogRef = ref()
const handleViewApproval = (id: number) => {
approvalDialogRef.value.open(String(id), 'mes_production_plan')
}
/** 处理审批 */
const processApprovalDialogRef = ref()
const handleProcessApproval = (id: number) => {
processApprovalDialogRef.value.open(String(id), 'mes_production_plan')
}
const openForm = (type: 'create' | 'update', id?: number) => {
formRef.value.open(type, id)
}
const openDetail = (id: number) => {
// 跳转到详情页面
router.push({
path: '/mes/production/plan/detail',
query: { id: id.toString() }
})
}
const handleDelete = async (id: number) => {
try {
await ElMessageBox.confirm('确定要删除该计划吗?', '提示', { type: 'warning' })
await deleteProductionPlan(id)
ElMessage.success('删除成功')
getList()
} catch {}
}
const handlePublish = async (id: number) => {
try {
await ElMessageBox.confirm('确定要发布该计划吗?发布后将不能修改。', '提示', {
type: 'warning'
})
await publishProductionPlan(id)
ElMessage.success('发布成功')
getList()
} catch {}
}
const handleClose = async (id: number) => {
try {
await ElMessageBox.confirm('确定要关闭该计划吗?', '提示', { type: 'warning' })
await closeProductionPlan(id)
ElMessage.success('关闭成功')
getList()
} catch {}
}
const handleGenerateOrders = (row: any) => {
generateOrderRef.value.open(row)
}
const handleGenerateMonthlyPlans = (row: any) => {
generateMonthlyPlanRef.value.open(row)
}
const handleAnalyze = (id: number) => {
planAnalysisRef.value.open(id)
}
const handleUpdateProgress = async (id: number) => {
try {
await updatePlanProgress(id)
ElMessage.success('进度更新成功')
getList()
} catch {}
}
const handleSelectionChange = (selection: any[]) => {
selectionList.value = selection
}
const getPlanTypeText = (type: number) => {
const map: Record<number, string> = {
1: '年度',
2: '月度',
3: '周度'
}
return map[type] || '未知'
}
const getPlanTypeTagType = (type: number) => {
const map: Record<number, string> = {
1: 'danger',
2: 'warning',
3: 'info'
}
return map[type] || ''
}
const getStatusText = (status: number) => {
const map: Record<number, string> = {
0: '草稿',
1: '已发布',
2: '执行中',
3: '已完成',
4: '已关闭'
}
return map[status] || '未知'
}
const getStatusType = (status: number) => {
const map: Record<number, string> = {
0: 'info',
1: 'primary',
2: 'warning',
3: 'success',
4: 'danger'
}
return map[status] || ''
}
const getProgressColor = (percentage: number) => {
if (percentage < 30) return '#f56c6c'
if (percentage < 70) return '#e6a23c'
return '#67c23a'
}
getList()
</script>

View File

@@ -0,0 +1,839 @@
<template>
<doc-alert title="【MES】工序记录" url="https://doc.iocoder.cn/mes/process-record/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="mb-4" :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-form-item label="批次号">
<el-input
v-model="queryParams.batchNo"
placeholder="请输入批次号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工序类型">
<el-select
v-model="selectedProcessType"
placeholder="请选择工序类型"
clearable
class="!w-240px"
@change="handleProcessTypeChange"
>
<el-option
v-for="item in processTypes"
:key="item.code"
:label="`${item.name} (${item.code})`"
:value="item.code"
>
<div class="flex items-center">
<span class="mr-1">{{ item.name }}</span>
<span class="text-gray-400 text-sm">({{ item.code }})</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option label="未开始" :value="0" />
<el-option label="进行中" :value="2" />
<el-option label="完成" :value="1" />
<el-option label="异常" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 数据展示部分 -->
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>工序记录列表</span>
</div>
</template>
<el-table :data="filteredTableData" border style="width: 100%">
<!-- 基础信息列 -->
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="batchNo" label="批次号" width="120" />
<el-table-column label="工序信息" min-width="100">
<template #default="{ row }">
<div class="process-info">
<span class="process-name">{{ row.processName }}</span>
<span class="process-code">{{ row.processCode }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作员" width="100" />
<el-table-column label="处理时间" width="120">
<template #default="{ row }">
{{ formatDateTime(row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<!-- 动态工序数据列 -->
<template v-if="selectedProcessType">
<template v-for="field in getTableColumns(filteredTableData[0])" :key="field.prop">
<el-table-column
:prop="'processData.' + field.prop + '.value'"
:label="field.label"
min-width="120"
>
<template #default="{ row }">
<span>
{{ formatFieldValue(row.processData[field.prop]) }}
</span>
</template>
</el-table-column>
</template>
</template>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<!-- 操作列 -->
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" @click="handleViewProcessData(row)"> 查看/编辑 </el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 工序数据详情对话框 -->
<el-dialog
v-model="dialogVisible"
:title="currentProcess?.processName + ' - 工序数据'"
width="600px"
>
<el-form
v-if="currentProcess"
ref="formRef"
:model="editingProcessData"
label-width="140px"
:rules="formRules"
>
<template v-for="(field, key) in currentProcess.processData" :key="key">
<el-form-item :label="field.label" :required="field.required" :prop="key">
<!-- 选择器类型 -->
<template v-if="field.type === 'select'">
<el-select
v-model="editingProcessData[key]"
:disabled="!field.editable"
style="width: 100%"
>
<el-option
v-for="option in field.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<!-- 数字类型 -->
<template v-else-if="field.type === 'number'">
<div class="number-input-with-unit">
<el-input-number
v-model="editingProcessData[key]"
:disabled="!field.editable"
:min="field.rules?.min"
:max="field.rules?.max"
:step="field.rules?.step"
:precision="2"
style="width: 100%"
/>
<span v-if="field.unit" class="unit-label">{{ field.unit }}</span>
</div>
</template>
<!-- 字符串类型 -->
<template v-else>
<el-input
v-model="editingProcessData[key]"
:disabled="!field.editable"
:placeholder="field.placeholder"
/>
</template>
</el-form-item>
</template>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveProcessData"> 保存 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { Icon } from '@/components/Icon'
// 接口定义
interface ProcessRecord {
id: number
processName: string
processCode: string
startTime: string
endTime: string
operator: string
status: number
remark: string
processData: Record<string, any>
batchNo: string // Added batchNo to the interface
}
interface ProcessField {
value: any
label: string
editable: boolean
required?: boolean
type: string
rules?: {
min?: number
max?: number
step?: number
}
unit?: string
showInTable?: boolean
order?: number
options?: Array<{
label: string
value: string | number
}>
placeholder?: string
multiple?: boolean
}
interface QueryParams {
pageNo: number
pageSize: number
batchNo?: string
status?: number
}
// 响应式变量定义
const loading = ref(true)
const total = ref(0)
const queryFormRef = ref<FormInstance>()
const selectedProcessType = ref('')
const dialogVisible = ref(false)
const currentProcess = ref<ProcessRecord | null>(null)
const editingProcessData = reactive<Record<string, any>>({})
const formRef = ref<FormInstance>()
// 查询参数
const queryParams = reactive<QueryParams>({
pageNo: 1,
pageSize: 10,
batchNo: undefined,
status: undefined
})
// 工序类型列表
const processTypes = [
{ name: '焊接工序', code: 'WELDING' },
{ name: '热处理工序', code: 'HEAT_TREATMENT' },
{ name: '装配工序', code: 'ASSEMBLY' },
{ name: '质量检验', code: 'QUALITY_CHECK' }
]
// 查询操作
const handleQuery = () => {
// TODO: 实现查询逻辑
console.log('查询参数:', queryParams)
}
// 重置操作
const resetQuery = () => {
queryFormRef.value?.resetFields?.()
queryParams.batchNo = undefined
queryParams.status = undefined
selectedProcessType.value = ''
handleQuery()
}
// 模拟数据实际应该从API获取
const mockData = {
code: 0,
data: [
{
processData: {
weldingTemperature: {
value: 200.5,
label: '焊接温度',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 1000,
step: 0.1
},
unit: '°C',
showInTable: true,
order: 1
},
inspectionResult: {
value: '合格',
label: '检验结果',
editable: true,
required: true,
type: 'select',
options: [
{ label: '合格', value: '合格' },
{ label: '不合格', value: '不合格' }
],
showInTable: true,
order: 3
},
weldingMethod: {
value: '电弧焊',
label: '焊接方法',
editable: false,
type: 'string',
showInTable: true,
order: 2
},
weldingPressure: {
value: 2.4,
label: '焊接压力',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 10,
step: 0.1
},
unit: 'MPa',
showInTable: false
},
weldingMaterial: {
value: '不锈钢',
label: '焊接材料',
editable: true,
type: 'string',
placeholder: '请输入焊接材料',
showInTable: false
}
},
processName: '焊接工序',
processCode: 'WELDING',
startTime: '2024-03-19 10:00:00',
remark: '焊接质量良好',
id: 1,
batchNo: 'B202403190001',
endTime: '2024-03-19 11:30:00',
operator: '张三',
status: 1
},
{
processData: {
surfaceHardness: {
value: 58,
label: '表面硬度',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 100,
step: 1
},
unit: 'HRC',
showInTable: true,
order: 2
},
deformation: {
value: 0.02,
label: '变形量',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 1,
step: 0.01
},
unit: 'mm',
showInTable: false
},
holdingTime: {
value: 120,
label: '保温时间',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 300,
step: 1
},
unit: 'min',
showInTable: true,
order: 3
},
heatingTemperature: {
value: 800,
label: '加热温度',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 1200,
step: 1
},
unit: '°C',
showInTable: true,
order: 1
},
coolingMethod: {
value: '空冷',
label: '冷却方式',
editable: true,
type: 'select',
options: [
{ label: '空冷', value: '空冷' },
{ label: '水冷', value: '水冷' },
{ label: '油冷', value: '油冷' }
],
showInTable: false
}
},
processName: '热处理工序',
processCode: 'HEAT_TREATMENT',
startTime: '2024-03-19 13:00:00',
remark: '热处理完成',
id: 2,
batchNo: 'B202403190001',
endTime: '2024-03-19 15:00:00',
operator: '李四',
status: 1
},
{
processData: {
assemblyParts: {
value: ['轴承', '轴套', '螺栓'],
label: '装配零件',
editable: true,
required: true,
type: 'select',
multiple: true,
options: [
{ label: '轴承', value: '轴承' },
{ label: '轴套', value: '轴套' },
{ label: '螺栓', value: '螺栓' },
{ label: '螺母', value: '螺母' },
{ label: '垫片', value: '垫片' }
],
showInTable: true,
order: 1
},
gapValue: {
value: 0.03,
label: '间隙值',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 0.1,
step: 0.01
},
unit: 'mm',
showInTable: true,
order: 2
},
torque: {
value: 45.5,
label: '扭矩',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 100,
step: 0.5
},
unit: 'N·m',
showInTable: true,
order: 3
},
testResult: {
value: '通过',
label: '测试结果',
editable: true,
required: true,
type: 'select',
options: [
{ label: '通过', value: '通过' },
{ label: '不通过', value: '不通过' },
{ label: '待复检', value: '待复检' }
],
showInTable: true,
order: 4
},
alignmentError: {
value: 0.02,
label: '对准误差',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 0.1,
step: 0.01
},
unit: 'mm',
showInTable: true,
order: 5
}
},
processName: '装配工序',
processCode: 'ASSEMBLY',
startTime: '2024-03-19 15:30:00',
remark: '装配完成,测试通过',
id: 3,
batchNo: 'B202403190002',
endTime: '2024-03-19 17:00:00',
operator: '王五',
status: 1
},
{
processData: {
inspectionMethod: {
value: '目视检查',
label: '检验方法',
editable: true,
type: 'select',
options: [
{ label: '目视检查', value: '目视检查' },
{ label: '尺寸测量', value: '尺寸测量' },
{ label: '无损检测', value: '无损检测' }
],
showInTable: true,
order: 1
},
surfaceQuality: {
value: '良好',
label: '表面质量',
editable: true,
required: true,
type: 'select',
options: [
{ label: '优秀', value: '优秀' },
{ label: '良好', value: '良好' },
{ label: '合格', value: '合格' },
{ label: '不合格', value: '不合格' }
],
showInTable: true,
order: 2
},
dimensionError: {
value: 0.05,
label: '尺寸误差',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 1,
step: 0.01
},
unit: 'mm',
showInTable: true,
order: 3
},
defectCount: {
value: 0,
label: '缺陷数量',
editable: true,
required: true,
type: 'number',
rules: {
min: 0,
max: 100,
step: 1
},
showInTable: true,
order: 4
}
},
processName: '质量检验',
processCode: 'QUALITY_CHECK',
startTime: '2024-03-19 17:30:00',
remark: '产品质量符合要求',
id: 4,
batchNo: 'B202403190002',
endTime: '2024-03-19 18:00:00',
operator: '赵六',
status: 1
}
],
msg: ''
}
const tableData = ref<ProcessRecord[]>(mockData.data)
// 根据选中的工序类型筛选数据
const filteredTableData = computed(() => {
if (!selectedProcessType.value) {
return tableData.value
}
return tableData.value.filter((item) => item.processCode === selectedProcessType.value)
})
// 生成表单验证规则
const formRules = computed(() => {
if (!currentProcess.value) return {}
const rules: Record<string, any[]> = {}
Object.entries(currentProcess.value.processData).forEach(([key, field]) => {
if (field.required) {
rules[key] = [
{
required: true,
message: `请输入${field.label}`,
trigger: 'blur'
}
]
}
if (field.type === 'number' && field.rules) {
if (!rules[key]) rules[key] = []
rules[key].push({
type: 'number',
min: field.rules.min,
max: field.rules.max,
message: `${field.label}必须在 ${field.rules.min}${field.rules.max} 之间`,
trigger: 'blur'
})
}
})
return rules
})
// 格式化标签名称
const formatLabel = (key: string) => {
const fieldData = currentProcess.value?.processData[key]
return fieldData?.label || key
}
// 查看/编辑工序数据
const handleViewProcessData = (row: ProcessRecord) => {
currentProcess.value = row
// 深拷贝processData以供编辑只复制value值
const processDataValues = Object.entries(row.processData).reduce(
(acc, [key, field]) => {
acc[key] = field.value
return acc
},
{} as Record<string, any>
)
Object.assign(editingProcessData, processDataValues)
dialogVisible.value = true
}
// 保存工序数据
const handleSaveProcessData = async () => {
if (!currentProcess.value || !formRef.value) return
try {
// 表单验证
await formRef.value.validate()
// 更新表格中的数据保持label不变只更新value
const index = tableData.value.findIndex((item) => item.id === currentProcess.value!.id)
if (index !== -1) {
const updatedProcessData = Object.entries(editingProcessData).reduce(
(acc, [key, value]) => {
acc[key] = {
...currentProcess.value!.processData[key],
value
}
return acc
},
{} as Record<string, any>
)
tableData.value[index].processData = updatedProcessData
// 将整个表格数据序列化并输出到控制台
const formattedData = {
code: 0,
data: tableData.value,
msg: ''
}
console.log('序列化后的JSON数据')
console.log(JSON.stringify(formattedData, null, 2))
ElMessage.success('保存成功')
dialogVisible.value = false
}
} catch (error) {
ElMessage.error('请检查表单填写是否正确')
}
}
// 工序类型变化处理
const handleProcessTypeChange = (value: string) => {
// 可以在这里添加其他处理逻辑
console.log('选中的工序类型:', value)
}
// 获取当前工序需要显示的列
const getProcessColumns = computed(() => {
if (!currentProcess.value) return []
const processData = currentProcess.value.processData
return Object.entries(processData)
.filter(([_, field]) => field.showInTable)
.sort((a, b) => (a[1].order || 0) - (b[1].order || 0))
.map(([key, field]) => ({
prop: key,
label: field.label,
formatter: (row: any) => {
const value = row.processData[key].value
const unit = row.processData[key].unit
if (Array.isArray(value)) {
return value.join('、')
}
return unit ? `${value}${unit}` : value
}
}))
})
// 获取表格列配置
const getTableColumns = (row: any) => {
if (!row) return []
return Object.entries(row.processData)
.filter(([_, field]) => (field as ProcessField).showInTable)
.sort((a, b) => ((a[1] as ProcessField).order || 0) - ((b[1] as ProcessField).order || 0))
.map(([key, field]) => ({
prop: key,
label: (field as ProcessField).label
}))
}
// 格式化字段值
const formatFieldValue = (field: ProcessField | undefined) => {
if (!field) return ''
const { value, unit } = field
if (Array.isArray(value)) {
return value.join('、')
}
return unit ? `${value}${unit}` : value
}
// 在 script 部分添加新的方法
const formatDateTime = (time: string) => {
if (!time) return '-'
return time
}
const getStatusType = (status: number) => {
const statusMap: Record<number, 'success' | 'warning' | 'info' | 'danger'> = {
0: 'info', // 未开始
1: 'success', // 完成
2: 'warning', // 进行中
3: 'danger' // 异常
}
return statusMap[status] || 'info'
}
const getStatusText = (status: number) => {
const statusMap: Record<number, string> = {
0: '未开始',
1: '完成',
2: '进行中',
3: '异常'
}
return statusMap[status] || '未知'
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.mb-4 {
margin-bottom: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.number-input-with-unit {
display: flex;
align-items: center;
gap: 8px;
}
.unit-label {
color: #606266;
font-size: 14px;
white-space: nowrap;
}
:deep(.el-form-item__label) {
word-break: break-word;
white-space: pre-wrap;
line-height: 1.2;
padding-right: 12px;
}
.process-code {
color: #909399;
font-size: 13px;
margin-left: 8px;
}
.process-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.process-name {
font-size: 14px;
color: var(--el-text-color-primary);
}
.process-code {
font-size: 12px;
color: var(--el-text-color-secondary);
}
:deep(.el-form-item) {
margin-bottom: 18px;
margin-right: 16px;
}
:deep(.el-form-item__content) {
display: flex;
align-items: center;
}
:deep(.el-select) {
width: 240px;
}
</style>

View File

@@ -0,0 +1,201 @@
# 工序记录字段定义说明
## 1. 字段基本属性
每个字段都需要包含以下基本属性:
- value: 字段值
- label: 字段中文名称
- type: 字段类型
- editable: 是否可编辑
- showInTable: 是否在表格中显示
- order: 表格中的显示顺序(数字越小越靠前)
## 2. 字段类型说明
### 2.1 字符串类型 (string)
```json
{
"value": "电弧焊",
"label": "焊接方法",
"type": "string",
"editable": true,
"placeholder": "请输入焊接方法",
"required": true
}
```
### 2.2 数字类型 (number)
```json
{
"value": 200.5,
"label": "焊接温度",
"type": "number",
"editable": true,
"required": true,
"rules": {
"min": 0,
"max": 1000,
"step": 0.1
},
"unit": "°C"
}
```
### 2.3 选择类型 (select)
```json
{
"value": "合格",
"label": "检验结果",
"type": "select",
"editable": true,
"required": true,
"options": [
{ "label": "合格", "value": "合格" },
{ "label": "不合格", "value": "不合格" }
]
}
```
### 2.4 多选类型 (select with multiple)
```json
{
"value": ["轴承", "轴套"],
"label": "装配零件",
"type": "select",
"multiple": true,
"editable": true,
"required": true,
"options": [
{ "label": "轴承", "value": "轴承" },
{ "label": "轴套", "value": "轴套" },
{ "label": "螺栓", "value": "螺栓" }
]
}
```
## 3. 字段规则说明
### 3.1 必填规则
- required: true/false表示字段是否必填
### 3.2 数字类型规则
- min: 最小值
- max: 最大值
- step: 步进值(每次增减的数值)
### 3.3 显示规则
- showInTable: 是否在表格中显示该字段
- order: 在表格中的显示顺序从1开始
- placeholder: 输入框提示文本
## 4. 完整示例
### 4.1 焊接工序示例
```json
{
"processData": {
"weldingTemperature": {
"value": 200.5,
"label": "焊接温度",
"type": "number",
"editable": true,
"required": true,
"rules": {
"min": 0,
"max": 1000,
"step": 0.1
},
"unit": "°C",
"showInTable": true,
"order": 1
},
"weldingMethod": {
"value": "电弧焊",
"label": "焊接方法",
"type": "string",
"editable": false,
"showInTable": true,
"order": 2
},
"inspectionResult": {
"value": "合格",
"label": "检验结果",
"type": "select",
"editable": true,
"required": true,
"options": [
{ "label": "合格", "value": "合格" },
{ "label": "不合格", "value": "不合格" }
],
"showInTable": true,
"order": 3
}
}
}
```
### 4.2 装配工序示例
```json
{
"processData": {
"assemblyParts": {
"value": ["轴承", "轴套"],
"label": "装配零件",
"type": "select",
"multiple": true,
"editable": true,
"required": true,
"options": [
{ "label": "轴承", "value": "轴承" },
{ "label": "轴套", "value": "轴套" },
{ "label": "螺栓", "value": "螺栓" }
],
"showInTable": true,
"order": 1
},
"torque": {
"value": 45.5,
"label": "扭矩",
"type": "number",
"editable": true,
"required": true,
"rules": {
"min": 0,
"max": 100,
"step": 0.5
},
"unit": "N·m",
"showInTable": true,
"order": 2
}
}
}
```
## 5. 注意事项
1. 字段命名规则:
- 使用驼峰命名法
- 名称应具有描述性
- 避免使用特殊字符
2. 数值类型注意事项:
- 必须设置合理的最大最小值
- step值要考虑实际精度需求
- 单位必须准确填写
3. 选择类型注意事项:
- options中的value值要唯一
- label要简洁明了
- 多选时value必须是数组
4. 显示规则注意事项:
- 表格显示的字段数量要适中
- order值不要重复
- 重要字段优先显示
5. 编辑权限注意事项:
- 关键字段建议设置为不可编辑
- 必填字段要有明确的提示
- 考虑字段间的依赖关系

View File

@@ -0,0 +1,431 @@
# 工序字段配置指南
## 1. 基本结构
工序字段配置采用 JSON 格式,每个字段都是一个键值对,其中:
-key字段的唯一标识符建议使用驼峰命名法
-value字段的配置对象包含该字段的所有属性
基本结构示例:
```json
{
"fieldName": {
"type": "string",
"label": "字段中文名",
"order": 1,
...
}
}
```
## 2. 字段通用属性
每个字段都支持以下通用属性:
| 属性名 | 类型 | 必填 | 说明 | 示例 |
| --- | --- | --- | --- | --- |
| type | string | 是 | 字段类型,支持 "string"、"number"、"select" | "type": "string" |
| label | string | 是 | 字段的显示名称(中文) | "label": "焊接方法" |
| order | number | 否 | 显示顺序,数字越小越靠前 | "order": 1 |
| editable | boolean | 是 | 是否可编辑 | "editable": true |
| required | boolean | 否 | 是否必填 | "required": true |
| showInTable | boolean | 否 | 是否在主表格中显示 | "showInTable": true |
| placeholder | string | 否 | 输入框占位文本 | "placeholder": "请输入焊接方法" |
## 3. 字段类型特有属性
### 3.1 字符串类型 (string)
```json
{
"weldingMethod": {
"type": "string",
"label": "焊接方法",
"order": 1,
"editable": true,
"required": true
}
}
```
### 3.2 数字类型 (number)
数字类型特有属性:
| 属性名 | 类型 | 必填 | 说明 | 示例 |
| ------ | ------ | ---- | -------- | ------------ |
| unit | string | 否 | 单位 | "unit": "°C" |
| rules | object | 否 | 数值规则 | 见下方示例 |
rules 对象包含:
- min: 最小值
- max: 最大值
- step: 步进值
示例:
```json
{
"temperature": {
"type": "number",
"label": "温度",
"unit": "°C",
"rules": {
"min": 0,
"max": 1000,
"step": 0.1
},
"required": true
}
}
```
### 3.3 选择器类型 (select)
选择器类型特有属性:
| 属性名 | 类型 | 必填 | 说明 | 示例 |
| -------- | ------- | ---- | ------------ | ---------------- |
| options | array | 是 | 选项列表 | 见下方示例 |
| multiple | boolean | 否 | 是否支持多选 | "multiple": true |
options 数组中的每个选项包含:
- label: 选项显示文本
- value: 选项值
示例:
```json
{
"inspectionResult": {
"type": "select",
"label": "检验结果",
"options": [
{ "label": "合格", "value": "合格" },
{ "label": "不合格", "value": "不合格" }
],
"multiple": false,
"required": true
}
}
```
## 4. 完整示例
### 4.1 焊接工序配置示例
```json
{
"weldingMethod": {
"type": "string",
"label": "焊接方法",
"order": 2,
"editable": true,
"required": true,
"showInTable": true
},
"weldingTemperature": {
"type": "number",
"label": "焊接温度",
"order": 1,
"unit": "°C",
"rules": {
"max": 1000,
"min": 0,
"step": 0.1
},
"editable": true,
"required": true,
"showInTable": true
},
"inspectionResult": {
"type": "select",
"label": "检验结果",
"order": 3,
"options": [
{ "label": "合格", "value": "合格" },
{ "label": "不合格", "value": "不合格" }
],
"editable": true,
"required": true,
"showInTable": true
}
}
```
### 4.2 装配工序配置示例
```json
{
"assemblyParts": {
"type": "select",
"label": "装配零件",
"order": 1,
"options": [
{ "label": "轴承", "value": "轴承" },
{ "label": "轴套", "value": "轴套" },
{ "label": "螺栓", "value": "螺栓" }
],
"multiple": true,
"editable": true,
"required": true,
"showInTable": true
},
"gapValue": {
"type": "number",
"label": "间隙值",
"order": 2,
"unit": "mm",
"rules": {
"max": 1,
"min": 0,
"step": 0.01
},
"editable": true,
"required": true,
"showInTable": true
},
"alignmentError": {
"type": "number",
"label": "对中误差",
"order": 3,
"unit": "mm",
"rules": {
"max": 0.1,
"min": 0,
"step": 0.001
},
"editable": true,
"required": true,
"showInTable": true
}
}
```
### 4.3 番茄酱生产工序配置示例
```json
{
"rawMaterialBatch": {
"type": "string",
"label": "原料批次号",
"order": 1,
"editable": true,
"required": true,
"showInTable": true,
"placeholder": "请输入原料批次号"
},
"cookingTemperature": {
"type": "number",
"label": "加热温度",
"order": 2,
"unit": "°C",
"rules": {
"max": 120,
"min": 60,
"step": 1
},
"editable": true,
"required": true,
"showInTable": true
},
"cookingTime": {
"type": "number",
"label": "加热时间",
"order": 3,
"unit": "min",
"rules": {
"max": 120,
"min": 30,
"step": 5
},
"editable": true,
"required": true,
"showInTable": true
},
"seasonings": {
"type": "select",
"label": "调味料",
"order": 4,
"options": [
{ "label": "食用盐", "value": "salt" },
{ "label": "白砂糖", "value": "sugar" },
{ "label": "白胡椒粉", "value": "pepper" },
{ "label": "香辛料", "value": "spices" }
],
"multiple": true,
"editable": true,
"required": true,
"showInTable": true
},
"consistency": {
"type": "select",
"label": "稠度检测",
"order": 5,
"options": [
{ "label": "合格", "value": "qualified" },
{ "label": "过稀", "value": "thin" },
{ "label": "过稠", "value": "thick" }
],
"editable": true,
"required": true,
"showInTable": true
},
"ph": {
"type": "number",
"label": "pH值",
"order": 6,
"rules": {
"max": 7,
"min": 3,
"step": 0.1
},
"editable": true,
"required": true,
"showInTable": true
}
}
```
### 4.4 甜菊糖生产工序配置示例
```json
{
"extractionTemperature": {
"type": "number",
"label": "提取温度",
"order": 1,
"unit": "°C",
"rules": {
"max": 80,
"min": 40,
"step": 1
},
"editable": true,
"required": true,
"showInTable": true
},
"extractionTime": {
"type": "number",
"label": "提取时间",
"order": 2,
"unit": "min",
"rules": {
"max": 180,
"min": 60,
"step": 5
},
"editable": true,
"required": true,
"showInTable": true
},
"solventType": {
"type": "select",
"label": "溶剂类型",
"order": 3,
"options": [
{ "label": "纯净水", "value": "water" },
{ "label": "乙醇", "value": "ethanol" },
{ "label": "混合溶剂", "value": "mixed" }
],
"editable": true,
"required": true,
"showInTable": true
},
"solventConcentration": {
"type": "number",
"label": "溶剂浓度",
"order": 4,
"unit": "%",
"rules": {
"max": 100,
"min": 0,
"step": 1
},
"editable": true,
"required": true,
"showInTable": true
},
"purityTest": {
"type": "number",
"label": "纯度检测",
"order": 5,
"unit": "%",
"rules": {
"max": 100,
"min": 90,
"step": 0.1
},
"editable": true,
"required": true,
"showInTable": true
},
"crystalForm": {
"type": "select",
"label": "结晶形态",
"order": 6,
"options": [
{ "label": "针状", "value": "needle" },
{ "label": "片状", "value": "flake" },
{ "label": "粒状", "value": "granular" }
],
"editable": true,
"required": true,
"showInTable": true
},
"moistureContent": {
"type": "number",
"label": "水分含量",
"order": 7,
"unit": "%",
"rules": {
"max": 10,
"min": 0,
"step": 0.1
},
"editable": true,
"required": true,
"showInTable": true
}
}
```
## 5. 注意事项
1. 字段命名规范:
- 使用驼峰命名法
- 避免使用特殊字符
- 建议使用英文
2. 数值类型注意事项:
- 必须设置合理的最大值和最小值
- step 建议根据精度需求设置
- 单位使用通用符号,如 °C、mm、MPa 等
3. 选择器类型注意事项:
- options 中的 value 建议使用简单的字符串或数字
- 多选时注意设置 multiple: true
- 选项建议不要太多,否则影响用户体验
4. 显示规则:
- order 值越小越靠前显示
- showInTable 为 true 的字段会显示在主表格中
- 建议重要字段设置 showInTable: true
5. 编辑权限:
- editable 为 false 的字段用户无法修改
- 关键参数建议设置 editable: false
- required 为 true 的字段必须填写
6. 字段数量:
- 建议每个工序的字段数量控制在 10 个以内
- 必填字段不要太多,影响操作效率

View File

@@ -0,0 +1,135 @@
<template>
<el-dialog
:title="formType === 'create' ? '新增工序' : '编辑工序'"
v-model="visible"
width="500px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" v-loading="formLoading">
<el-form-item label="工序编码" prop="code">
<el-input v-model="form.code" placeholder="请输入工序编码" />
</el-form-item>
<el-form-item label="工序名称" prop="name">
<el-input v-model="form.name" placeholder="请输入工序名称" />
</el-form-item>
<el-form-item label="工序描述" prop="description">
<el-input v-model="form.description" placeholder="请输入工序描述" />
</el-form-item>
<el-form-item label="工序类型" prop="type">
<el-input v-model.number="form.type" placeholder="请输入工序类型(数字)" />
</el-form-item>
<el-form-item label="标准工时(分钟)" prop="standardTime">
<el-input v-model.number="form.standardTime" placeholder="请输入标准工时" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import {
YVHcreateProcessOperation,
YVHupdateProcessOperation,
YVHgetProcessOperation
} from '@/api/mes/production/process-operation'
// 生成工序编码
const generateOperationCode = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const random = Math.floor(Math.random() * 1000)
.toString()
.padStart(3, '0')
return `OP${year}${month}${day}${random}`
}
const emits = defineEmits(['success'])
const visible = ref(false)
const loading = ref(false)
const formLoading = ref(false) // 表单加载状态
const formType = ref<'create' | 'update'>('create')
const form = reactive<any>({})
const formRef = ref()
const rules = {
code: [{ required: true, message: '请输入工序编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入工序名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
const open = async (type: 'create' | 'update', id?: number) => {
formType.value = type
visible.value = true
formLoading.value = true // 开始加载,禁用表单
try {
// 重置表单
Object.assign(form, {
code: type === 'create' ? generateOperationCode() : '',
name: '',
description: '',
type: 1, // 默认工序类型为1
standardTime: 30, // 默认标准工时30分钟
status: 1,
remark: ''
})
if (type === 'update' && id) {
const res = await YVHgetProcessOperation(id)
// 直接使用接口返回的数据,不进行解构
Object.assign(form, res)
}
} catch (error) {
console.error('初始化表单失败:', error)
ElMessage.error('加载数据失败,请重试')
visible.value = false
} finally {
formLoading.value = false // 加载完成,启用表单
}
}
const submitForm = () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (formType.value === 'create') {
await YVHcreateProcessOperation(form)
ElMessage.success('新增成功')
} else {
await YVHupdateProcessOperation(form)
ElMessage.success('修改成功')
}
visible.value = false
emits('success')
} finally {
loading.value = false
}
})
}
const handleClose = () => {
formRef.value?.resetFields()
}
// 对外暴露 open 方法
defineExpose({ open })
</script>

View File

@@ -0,0 +1,211 @@
<template>
<doc-alert title="【MES】工序管理" url="https://doc.iocoder.cn/mes/process-operation/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="工序编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入工序编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工序名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入工序名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:process-operation:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['mes:process-operation:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column type="selection" width="50" />
<el-table-column width="80" label="工序ID" align="center" prop="id" />
<el-table-column min-width="120" label="工序编码" align="center" prop="code" />
<el-table-column min-width="120" label="工序名称" align="center" prop="name" />
<el-table-column min-width="180" label="工序描述" align="center" prop="description" />
<el-table-column min-width="120" label="工序类型" align="center" prop="type" />
<el-table-column min-width="120" label="标准工时(分钟)" align="center" prop="standardTime" />
<el-table-column min-width="100" label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column min-width="120" label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:process-operation:update']"
>编辑</el-button
>
<el-button
link
type="danger"
@click="handleDelete([scope.row.id])"
v-hasPermi="['mes:process-operation:delete']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ProcessOperationForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
YVHgetProcessOperationPage,
YVHdeleteProcessOperation
} from '@/api/mes/production/process-operation'
import { Icon } from '@/components/Icon'
import ProcessOperationForm from './ProcessOperationForm.vue'
const loading = ref(true)
const list = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
status: undefined
})
const queryFormRef = ref()
const formRef = ref()
const selectionList = ref<any[]>([])
const getList = async () => {
loading.value = true
try {
const res = await YVHgetProcessOperationPage(queryParams)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields?.()
queryParams.code = undefined
queryParams.name = undefined
queryParams.status = undefined
handleQuery()
}
const openForm = (type: 'create' | 'update', id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (ids: number[]) => {
if (!ids || ids.length === 0) return
try {
await ElMessageBox.confirm('确定要删除选中的工序吗?', '提示', { type: 'warning' })
for (const id of ids) {
await YVHdeleteProcessOperation(id)
}
ElMessage.success('删除成功')
getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {}
}
const handleSelectionChange = (rows: any[]) => {
selectionList.value = rows
}
/** 行点击操作 */
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
// 检查是否点击了按钮、链接或其他交互元素
const target = event.target as HTMLElement
if (
target.tagName === 'BUTTON' ||
target.tagName === 'A' ||
target.tagName === 'I' ||
target.tagName === 'svg' ||
target.closest('button') ||
target.closest('a') ||
target.closest('.el-button') ||
target.closest('.el-checkbox')
) {
return
}
// 直接打开编辑页面
openForm('update', row.id)
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,302 @@
<template>
<el-dialog
:title="formType === 'create' ? '新增工序路线与工位设置' : '编辑工序路线与工位设置'"
v-model="visible"
width="800px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
<!-- 基本信息 -->
<el-form-item label="路线编码" prop="code">
<el-input v-model="form.code" placeholder="请输入工序路线编码" />
</el-form-item>
<el-form-item label="路线名称" prop="name">
<el-input v-model="form.name" placeholder="请输入工序路线名称" />
</el-form-item>
<el-form-item label="路线描述" prop="description">
<el-input v-model="form.description" placeholder="请输入工序路线描述" type="textarea" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<!-- 工序列表 -->
<el-divider>工序列表</el-divider>
<div class="mb-10px">
<el-button type="primary" @click="handleAddOperation">
<Icon icon="ep:plus" class="mr-5px" />添加工序
</el-button>
</div>
<el-table :data="form.operations" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="工序" min-width="150" align="center">
<template #default="{ row }">
<el-select
v-model="row.operationId"
placeholder="请选择工序"
@change="handleOperationChange($event, row)"
style="width: 100%"
>
<el-option
v-for="item in operationOptions"
:key="item.id"
:label="`${item.name} (${item.code})`"
:value="item.id"
>
<span>{{ item.name }}</span>
<span class="text-gray-400 ml-10px">({{ item.code }})</span>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="实际工时(分钟)" min-width="100" align="center">
<template #default="{ row }">
<el-input
v-model.number="row.requiredTime"
placeholder="输入分钟数"
style="width: 100px"
@blur="validateRequiredTime(row)"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="请输入备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="{ $index }">
<el-button
link
type="primary"
:disabled="$index === 0"
@click="moveOperation($index, 'up')"
>
<Icon icon="ep:arrow-up" />
</el-button>
<el-button
link
type="primary"
:disabled="$index === form.operations.length - 1"
@click="moveOperation($index, 'down')"
>
<Icon icon="ep:arrow-down" />
</el-button>
<el-button link type="danger" @click="removeOperation($index)">
<Icon icon="ep:delete" />
</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
YVHcreateProcessRoute,
YVHupdateProcessRoute,
YVHgetProcessRoute
} from '@/api/mes/production/process-route'
import { YVHgetProcessOperationList } from '@/api/mes/production/process-operation'
import { Icon } from '@/components/Icon'
// 生成工序路线编码
const generateRouteCode = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const random = Math.floor(Math.random() * 1000)
.toString()
.padStart(3, '0')
return `RT${year}${month}${day}${random}`
}
const emits = defineEmits(['success'])
const visible = ref(false)
const loading = ref(false)
const formLoading = ref(false) // 表单加载状态
const formType = ref<'create' | 'update'>('create')
const operationOptions = ref<any[]>([])
const form = reactive<any>({
operations: []
})
const formRef = ref()
const rules = {
code: [{ required: true, message: '请输入工序路线编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入工序路线名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
operations: [{ required: true, message: '请添加至少一个工序', trigger: 'change' }]
}
// 获取工序选项
const getOperationOptions = async () => {
try {
const res = await YVHgetProcessOperationList()
operationOptions.value = res
return res
} catch (error) {
console.error('获取工序列表失败:', error)
ElMessage.error('获取工序列表失败')
return []
}
}
const open = async (type: 'create' | 'update', id?: number) => {
formType.value = type
visible.value = true
formLoading.value = true // 开始加载,禁用表单
try {
// 重置表单
Object.assign(form, {
code: type === 'create' ? generateRouteCode() : '',
name: '',
description: '',
status: 1,
remark: '',
operations: []
})
// 获取工序选项
await getOperationOptions()
if (type === 'update' && id) {
const res = await YVHgetProcessRoute(id)
Object.assign(form, res)
} else if (type === 'create' && operationOptions.value.length > 0) {
// 创建时默认添加一个工序
handleAddOperation()
}
} catch (error) {
console.error('初始化表单失败:', error)
ElMessage.error('加载数据失败,请重试')
visible.value = false
} finally {
formLoading.value = false // 加载完成,启用表单
}
}
const handleAddOperation = () => {
// 找到一个尚未添加的工序
const usedOperationIds = form.operations.map((op: any) => op.operationId)
const availableOperation = operationOptions.value.find((op) => !usedOperationIds.includes(op.id))
const newOperation = {
operationId: availableOperation?.id,
operationCode: availableOperation?.code || '',
operationName: availableOperation?.name || '',
sequence: form.operations.length + 1,
requiredTime: availableOperation?.standardTime || 30,
remark: ''
}
form.operations.push(newOperation)
// 如果选择了工序,自动填充相关信息
if (availableOperation) {
handleOperationChange(availableOperation.id, newOperation)
}
}
const handleOperationChange = (operationId: number, row: any) => {
const operation = operationOptions.value.find((item) => item.id === operationId)
if (operation) {
row.operationCode = operation.code
row.operationName = operation.name
// 如果requiredTime为0或未设置则使用标准工时
if (!row.requiredTime) {
row.requiredTime = operation.standardTime || 30
}
}
}
const moveOperation = (index: number, direction: 'up' | 'down') => {
const operations = form.operations
if (direction === 'up' && index > 0) {
;[operations[index], operations[index - 1]] = [operations[index - 1], operations[index]]
} else if (direction === 'down' && index < operations.length - 1) {
;[operations[index], operations[index + 1]] = [operations[index + 1], operations[index]]
}
// 更新序号
operations.forEach((item: any, idx: number) => {
item.sequence = idx + 1
})
}
const removeOperation = (index: number) => {
form.operations.splice(index, 1)
// 更新序号
form.operations.forEach((item: any, idx: number) => {
item.sequence = idx + 1
})
}
const submitForm = () => {
if (form.operations.length === 0) {
ElMessage.warning('请至少添加一个工序')
return
}
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (formType.value === 'create') {
await YVHcreateProcessRoute(form)
ElMessage.success('新增成功')
} else {
await YVHupdateProcessRoute(form)
ElMessage.success('修改成功')
}
visible.value = false
emits('success')
} finally {
loading.value = false
}
})
}
const handleClose = () => {
formRef.value?.resetFields()
}
// 对外暴露 open 方法
defineExpose({ open })
// 在script部分添加验证函数
const validateRequiredTime = (row: any) => {
// 确保输入的是有效的非负整数
if (isNaN(row.requiredTime) || row.requiredTime < 0) {
row.requiredTime = 30 // 默认值
} else {
// 确保是整数
row.requiredTime = Math.floor(Number(row.requiredTime))
}
}
</script>
<style scoped>
.el-divider {
margin: 16px 0;
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<doc-alert title="【MES】工序路线管理" url="https://doc.iocoder.cn/mes/process-route/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="路线编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入工序路线编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="路线名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入工序路线名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:process-route:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['mes:process-route:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column type="selection" width="50" />
<el-table-column width="80" label="路线ID" align="center" prop="id" />
<el-table-column min-width="120" label="路线编码" align="center" prop="code" />
<el-table-column min-width="120" label="路线名称" align="center" prop="name" />
<el-table-column min-width="180" label="路线描述" align="center" prop="description" />
<el-table-column min-width="100" label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column min-width="120" label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:process-route:update']"
>编辑</el-button
>
<el-button
link
type="danger"
@click="handleDelete([scope.row.id])"
v-hasPermi="['mes:process-route:delete']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ProcessRouteForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { YVHgetProcessRoutePage, YVHdeleteProcessRoute } from '@/api/mes/production/process-route'
import { Icon } from '@/components/Icon'
import ProcessRouteForm from './ProcessRouteForm.vue'
const loading = ref(true)
const list = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
status: undefined
})
const queryFormRef = ref()
const formRef = ref()
const selectionList = ref<any[]>([])
const getList = async () => {
loading.value = true
try {
const res = await YVHgetProcessRoutePage(queryParams)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields?.()
queryParams.code = undefined
queryParams.name = undefined
queryParams.status = undefined
handleQuery()
}
const openForm = (type: 'create' | 'update', id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (ids: number[]) => {
if (!ids || ids.length === 0) return
try {
await ElMessageBox.confirm('确定要删除选中的工序路线吗?', '提示', { type: 'warning' })
for (const id of ids) {
await YVHdeleteProcessRoute(id)
}
ElMessage.success('删除成功')
getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {}
}
const handleSelectionChange = (rows: any[]) => {
selectionList.value = rows
}
/** 行点击操作 */
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
// 检查是否点击了按钮、链接或其他交互元素
const target = event.target as HTMLElement
if (
target.tagName === 'BUTTON' ||
target.tagName === 'A' ||
target.tagName === 'I' ||
target.tagName === 'svg' ||
target.closest('button') ||
target.closest('a') ||
target.closest('.el-button') ||
target.closest('.el-checkbox')
) {
return
}
// 直接打开编辑页面
openForm('update', row.id)
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,409 @@
<template>
<el-dialog
:title="formType === 'create' ? '新增工单' : '编辑工单'"
v-model="visible"
width="900px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
<el-tabs v-model="activeTab">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="basic">
<el-form-item label="产品" prop="productId">
<el-select
v-model="form.productId"
placeholder="请选择产品"
class="!w-240px"
@change="handleProductChange"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<span class="text-gray-400 ml-10px">({{ item.barCode }})</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="工单编号" prop="code">
<el-input v-model="form.code" placeholder="请输入工单编号" />
</el-form-item>
<!-- <el-form-item label="班次" prop="shift">-->
<!-- <el-input v-model="form.shift" placeholder="请输入班次" @change="handleShiftChange" />-->
<!-- </el-form-item>-->
<el-form-item label="工单名称" prop="name">
<el-input v-model="form.name" placeholder="请输入工单名称" />
</el-form-item>
<el-form-item label="工序路线" prop="routeId">
<el-select
v-model="form.routeId"
placeholder="请选择工序路线"
class="!w-240px"
@change="handleRouteChange"
>
<el-option
v-for="item in routeOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<span class="text-gray-400 ml-10px">({{ item.code }})</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="计划数量" prop="planQuantity">
<el-input-number
v-model="form.planQuantity"
:min="0.01"
:precision="2"
:step="0.1"
placeholder="请输入计划数量"
/>
</el-form-item>
<!-- <el-form-item label="优先级" prop="priority">-->
<!-- <el-input-number-->
<!-- v-model="form.priority"-->
<!-- :min="1"-->
<!-- :max="10"-->
<!-- :precision="0"-->
<!-- placeholder="请输入优先级(1-10)"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="计划时间" prop="planTime">
<el-date-picker
v-model="form.planTime"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-time="[
new Date(
new Date().setHours(
new Date().getHours(),
new Date().getMinutes(),
new Date().getSeconds()
)
),
new Date(
new Date().setHours(
new Date().getHours(),
new Date().getMinutes(),
new Date().getSeconds()
)
)
]"
class="!w-380px"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-tab-pane>
<!-- 工序信息 -->
<el-tab-pane label="工序信息" name="process">
<el-table :data="form.operations" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="工序编码" prop="operationCode" min-width="120" align="center" />
<el-table-column label="工序名称" prop="operationName" min-width="120" align="center" />
<el-table-column
label="工时(分钟)"
prop="requiredTime"
min-width="120"
align="center"
/>
</el-table>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import {
YVHcreateWorkOrder,
YVHupdateWorkOrder,
YVHgetWorkOrder
} from '@/api/mes/production/workorder'
import { YVHgetProcessRoute, YVHgetProcessRouteList } from '@/api/mes/production/process-route'
import { ProductApi } from '@/api/erp/product/product'
const emits = defineEmits(['success'])
const visible = ref(false)
const loading = ref(false)
const formLoading = ref(false) // 表单加载状态
const formType = ref<'create' | 'update'>('create')
const activeTab = ref('basic')
const formRef = ref()
// 产品选项
const productOptions = ref<any[]>([])
// 工序路线选项
const routeOptions = ref<any[]>([])
const form = reactive<any>({
id: undefined,
code: '',
name: '',
shift: '', // 添加班次字段
productId: undefined,
productCode: '',
productName: '',
routeId: undefined,
routeName: '',
planQuantity: 1,
priority: 5,
planTime: [],
status: 0,
remark: '',
operations: []
})
const rules = {
code: [{ required: true, message: '请输入工单编号', trigger: 'blur' }],
shift: [{ required: true, message: '请选择班次', trigger: 'change' }],
name: [{ required: true, message: '请输入工单名称', trigger: 'blur' }],
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
routeId: [{ required: true, message: '请选择工序路线', trigger: 'change' }],
planQuantity: [{ required: true, message: '请输入计划数量', trigger: 'blur' }],
planTime: [{ required: true, message: '请选择计划时间', trigger: 'change' }]
}
// 生成工单编号
const generateOrderCode = (productCode: string = '') => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${productCode}${year}${month}${day}`
}
const open = async (type: 'create' | 'update', id?: number) => {
formType.value = type
visible.value = true
activeTab.value = 'basic'
formLoading.value = true // 开始加载,禁用表单
try {
// 重置表单
Object.assign(form, {
id: undefined,
code: type === 'create' ? generateOrderCode() : '',
name: '',
shift: '', // 重置班次
productId: undefined,
productCode: '',
productName: '',
routeId: undefined,
routeName: '',
planQuantity: 1,
priority: 5,
planTime:
type === 'create'
? [new Date().setHours(8,0,0,0,), new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000).setHours(8,0,0,0,)]
: [],
status: 0,
remark: '',
operations: []
})
// 并行获取产品列表和工序路线列表
const [products, routes] = await Promise.all([
ProductApi.getProductSimpleList().catch((error) => {
console.error('获取产品列表失败:', error)
return []
}),
YVHgetProcessRouteList().catch((error) => {
console.error('获取工序路线列表失败:', error)
return []
})
])
// 过滤产品列表只保留categoryName包含"成品"的产品
productOptions.value = products
? products.filter((product) => product.categoryName?.includes('成品'))
: []
routeOptions.value = routes || []
// 如果是编辑模式,获取工单详情
if (type === 'update' && id) {
const res = await YVHgetWorkOrder(id)
Object.assign(form, {
...res,
id: res.id, // 确保id被正确设置
planTime: [res.planStartTime, res.planEndTime]
})
}
} catch (error) {
console.error('初始化表单失败:', error)
ElMessage.error('加载数据失败,请重试')
visible.value = false
} finally {
formLoading.value = false // 加载完成,启用表单
}
}
const handleProductChange = async (productId: number) => {
const product = productOptions.value.find((item) => item.id === productId)
if (product) {
form.productCode = product.barCode
form.productName = product.name
// 根据产品名称设置工单编号前缀
let productCode = 'X'
if (product.name.includes('番茄')) {
productCode = 'X8'
}
form.code = generateOrderCode(productCode)
// 更新工单名称
const today = new Date()
const dateStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`
form.name = `${product.name}生产工单-${dateStr}`
// 尝试自动匹配与产品名称相关的工序路线
if (routeOptions.value.length > 0) {
// 首先尝试完全匹配产品名称
const exactMatch = routeOptions.value.find((route) => route.name === product.name)
if (exactMatch) {
form.routeId = exactMatch.id
handleRouteChange(exactMatch.id)
return
}
// 其次尝试包含产品名称的工序路线
const containsMatch = routeOptions.value.find((route) => route.name.includes(product.name))
if (containsMatch) {
form.routeId = containsMatch.id
handleRouteChange(containsMatch.id)
return
}
// 最后尝试产品名称包含工序路线名称的情况
const reverseMatch = routeOptions.value.find((route) => product.name.includes(route.name))
if (reverseMatch) {
form.routeId = reverseMatch.id
handleRouteChange(reverseMatch.id)
}
}
}
}
const handleRouteChange = async (routeId: number) => {
if (!routeId) return
formLoading.value = true // 加载工序路线详情时禁用表单
try {
const route = await YVHgetProcessRoute(routeId)
form.routeName = route.name
form.operations = route.operations.map((item: any) => ({
operationId: item.operationId,
operationCode: item.operationCode,
operationName: item.operationName,
requiredTime: item.requiredTime,
sequence: item.sequence
}))
} catch (error) {
console.error('获取工序路线详情失败:', error)
form.routeName = ''
form.operations = []
ElMessage.error('获取工序路线详情失败')
} finally {
formLoading.value = false // 加载完成,启用表单
}
}
const handleShiftChange = () => {
if (form.productId && form.shift) {
// 更新工单编号,加入班次信息
let productCode = 'X'
if (form.productName.includes('番茄')) {
productCode = 'X8'
}
form.code = `${generateOrderCode(productCode)}${form.shift}`
// 更新工单名称
const today = new Date()
const dateStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`
form.name = `${form.productName}生产工单-${dateStr}-${form.shift}`
}
}
const submitForm = () => {
if (!form.operations.length) {
ElMessage.warning('请选择工序路线')
return
}
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const data = {
id: form.id, // 编辑时需要
code: form.code,
name: form.name,
shift: form.shift, // 添加班次
productId: form.productId,
productCode: form.productCode,
productName: form.productName,
planQuantity: form.planQuantity,
priority: form.priority,
routeId: form.routeId,
routeName: form.routeName,
planStartTime: form.planTime[0],
planEndTime: form.planTime[1],
status: form.status, // 使用表单中保存的状态
remark: form.remark,
operations: form.operations.map((op: any) => ({
operationId: op.operationId,
operationCode: op.operationCode,
operationName: op.operationName,
requiredTime: op.requiredTime,
sequence: op.sequence
// 不包含 remark 字段,因为后端 API 不支持
}))
}
if (formType.value === 'create') {
data.status = 0 // 新建时固定为草稿状态
await YVHcreateWorkOrder(data)
ElMessage.success('新增成功')
} else {
await YVHupdateWorkOrder(data) // 编辑时直接传入所有数据
ElMessage.success('修改成功')
}
visible.value = false
emits('success')
} catch (error) {
console.error('保存工单失败:', error)
} finally {
loading.value = false
}
})
}
const handleClose = () => {
formRef.value?.resetFields()
}
defineExpose({ open })
</script>
<style scoped>
.el-tabs :deep(.el-tabs__content) {
padding: 20px 0;
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<el-dialog v-model="visibleProxy" title="报工单" width="800px" append-to-body>
<el-form label-width="120px">
<template v-if="Object.keys(detailProcessData).length > 0">
<div class="mb-4">
<div class="text-lg font-bold mb-2">工序参数</div>
<template v-for="(field, key) in detailProcessData" :key="key">
<el-form-item :label="field.label">
<div class="text-gray-600">{{ formatDetailFieldValue(field) }}</div>
</el-form-item>
</template>
</div>
</template>
<el-form-item label="操作人">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.workerName || '-'
}}</div>
</el-form-item>
<el-form-item label="设备">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.equipmentName || '-'
}}</div>
</el-form-item>
<el-form-item label="开始时间">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.startTime || '-'
}}</div>
</el-form-item>
<el-form-item label="结束时间">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.endTime || '-'
}}</div>
</el-form-item>
<el-form-item label="投入数量">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.inputQuantity || '-'
}}</div>
</el-form-item>
<el-form-item label="产出数量">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.outputQuantity || '-'
}}</div>
</el-form-item>
<el-form-item label="合格数量">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.qualifiedQuantity || '-'
}}</div>
</el-form-item>
<el-form-item label="不合格数量">
<div class="text-gray-600">{{
currentDetailOperation?.currentExecution?.unqualifiedQuantity || '-'
}}</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleProxy = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
visible: boolean
currentDetailOperation: any
detailProcessData: Record<string, any>
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (val: boolean) => emit('update:visible', val)
})
const formatDetailFieldValue = (field: any) => {
if (!field || field.value === undefined) return '-'
const { value, type, unit, options } = field
if (type === 'select' && options) {
const option = options.find((opt: any) => opt.value === value)
return option ? option.label : value
}
if (type === 'number' && unit) {
return `${value} ${unit}`
}
if (Array.isArray(value)) {
return value.join('、')
}
return value
}
</script>
<style scoped>
.text-lg {
font-size: 16px;
}
.font-bold {
font-weight: bold;
}
.mb-2 {
margin-bottom: 8px;
}
.mb-4 {
margin-bottom: 16px;
}
.text-gray-600 {
color: #606266;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
<template>
<el-card class="mb-4">
<template #header>
<div class="flex justify-between items-center">
<span>工序进度</span>
<div class="text-gray-400"> 总进度{{ progressData?.completionRate || 0 }}% </div>
</div>
</template>
<div class="flex justify-between items-center">
<div class="text-center">
<div class="text-2xl font-bold">{{ progressData?.totalOperations || 0 }}</div>
<div class="text-gray-400">总工序</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-success">{{
progressData?.completedOperations || 0
}}</div>
<div class="text-gray-400">已完成</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-warning">{{
progressData?.inProgressOperations || 0
}}</div>
<div class="text-gray-400">进行中</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-info">{{ progressData?.pendingOperations || 0 }}</div>
<div class="text-gray-400">未开始</div>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
interface ProgressData {
completionRate?: number
totalOperations?: number
completedOperations?: number
inProgressOperations?: number
pendingOperations?: number
}
const props = defineProps<{ progressData: ProgressData | null }>()
</script>

View File

@@ -0,0 +1,224 @@
<template>
<el-dialog v-model="visibleProxy" title="进度记录历史" width="1000px" append-to-body>
<el-table :data="progressHistoryList" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<!-- 动态工序参数列 -->
<template v-for="column in processDataColumns" :key="column.key">
<el-table-column :label="column.label" :min-width="column.minWidth" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.processData">
{{ formatProcessDataValue(JSON.parse(row.processData)[column.key]) }}
</template>
</template>
</el-table-column>
</template>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<el-table-column label="记录时间" prop="createTime" width="180" align="center" />
<!-- 操作列编辑进度记录 -->
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button
link
type="primary"
@click="openEdit(row)"
v-hasPermi="['mes:production-order:operations']"
>编辑</el-button
>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="visibleProxy = false">关闭</el-button>
</template>
</el-dialog>
<!-- 编辑进度记录弹窗 -->
<el-dialog v-model="editDialogVisible" title="编辑进度记录" width="600px" append-to-body>
<el-form ref="editFormRef" :model="editForm" label-width="120px">
<template v-if="Object.keys(editFieldsConfig).length > 0">
<div class="mb-6 bg-gray-50 p-4 rounded">
<div class="text-base font-bold mb-4 text-gray-700">工序参数</div>
<template v-for="(field, key) in editFieldsConfig" :key="key">
<el-form-item :label="field.label" class="mb-4">
<!-- 选择器类型 -->
<template v-if="field.type === 'select'">
<el-select
v-model="editForm.processData[key]"
:disabled="field.editable === false"
:multiple="field.multiple"
:placeholder="'请选择' + field.label"
class="!w-[280px]"
>
<el-option
v-for="option in field.options || []"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<!-- 数字类型 -->
<template v-else-if="field.type === 'number'">
<div class="flex items-center">
<el-input-number
v-model="editForm.processData[key]"
:disabled="field.editable === false"
:min="field.rules?.min ?? 0"
:max="field.rules?.max ?? Infinity"
:step="field.rules?.step ?? 1"
controls-position="right"
class="!w-[180px]"
/>
<span v-if="field.unit" class="ml-2 text-gray-600">{{ field.unit }}</span>
</div>
</template>
<!-- 字符串类型 -->
<template v-else>
<el-input
v-model="editForm.processData[key]"
:disabled="field.editable === false"
:placeholder="field.placeholder || '请输入' + field.label"
class="!w-[280px]"
/>
</template>
</el-form-item>
</template>
</div>
</template>
<el-form-item label="备注">
<el-input
v-model="editForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
class="!w-[380px]"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEdit" :loading="editSubmitting">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { YVHupdateOperationProgress } from '@/api/mes/production/order-operation'
const props = defineProps<{
visible: boolean
progressHistoryList: any[]
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'updated'): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (val: boolean) => emit('update:visible', val)
})
const processDataColumns = computed(() => {
if (!props.progressHistoryList?.length)
return [] as Array<{ label: string; key: string; minWidth: string }>
const firstRecord = props.progressHistoryList[0]
if (!firstRecord?.processData) return []
const processData = JSON.parse(firstRecord.processData) as Record<
string,
{ label: string; order?: number; [k: string]: any }
>
const entries = Object.entries(processData) as Array<
[string, { label: string; order?: number; [k: string]: any }]
>
return entries
.sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))
.map(([key, field]) => ({ label: field.label, key, minWidth: '120' }))
})
const formatProcessDataValue = (field: any) => {
if (!field) return ''
if (field.type === 'select' && field.options) {
if (Array.isArray(field.value)) {
return field.value
.map((v: any) => field.options.find((opt: any) => opt.value === v)?.label || v)
.join('、')
} else {
const option = field.options.find((opt: any) => opt.value === field.value)
return option ? option.label : field.value
}
}
if (field.type === 'number' && field.unit) {
return `${field.value}${field.unit}`
}
return field.value
}
// 编辑相关
const editDialogVisible = ref(false)
const editSubmitting = ref(false)
const editFormRef = ref()
const editingRecordId = ref<number | null>(null)
const editFieldsConfig = ref<Record<string, any>>({})
const editForm = reactive({
processData: {} as Record<string, any>,
remark: ''
})
const openEdit = (row: any) => {
editingRecordId.value = row.id
editForm.remark = row.remark || ''
const parsed = row.processData ? JSON.parse(row.processData) : {}
editFieldsConfig.value = parsed
editForm.processData = Object.entries(parsed).reduce(
(acc: Record<string, any>, [key, field]: [string, any]) => {
acc[key] = field?.value ?? ''
return acc
},
{}
)
editDialogVisible.value = true
}
const submitEdit = async () => {
if (!editingRecordId.value) return
try {
editSubmitting.value = true
const processDataWithConfig = Object.entries(editForm.processData).reduce(
(acc, [key, value]) => {
const fieldConfig = editFieldsConfig.value[key]
acc[key] = {
...fieldConfig,
value
}
return acc
},
{} as Record<string, any>
)
await YVHupdateOperationProgress({
id: editingRecordId.value,
processData: JSON.stringify(processDataWithConfig),
remark: editForm.remark
})
ElMessage.success('更新成功')
editDialogVisible.value = false
emit('updated')
} catch (error) {
console.error('更新进度记录失败:', error)
ElMessage.error('更新失败')
} finally {
editSubmitting.value = false
}
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<el-dialog v-model="visibleProxy" title="开始工序" width="500px" append-to-body>
<el-form ref="startFormRef" :model="startForm" :rules="startRules" label-width="100px">
<el-form-item label="操作工人" prop="workerName">
<el-input
v-model="startForm.workerName"
placeholder="请输入操作工人姓名"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="设备" prop="equipmentId">
<el-select v-model="startForm.equipmentId" placeholder="请选择设备" class="!w-240px">
<el-option
v-for="item in equipmentOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="生产数量" prop="inputQuantity">
<el-input-number
v-model="startForm.inputQuantity"
:min="0.01"
:precision="2"
:step="0.1"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="startForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleProxy = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { useUserStore } from '@/store/modules/user'
interface EquipmentOption {
id: number
name: string
}
const props = defineProps<{
visible: boolean
equipmentOptions: EquipmentOption[]
maxQuantity?: number
defaultInputQuantity?: number
submitting?: boolean
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(
e: 'submit',
payload: {
workerName: string
equipmentId?: number
equipmentName?: string
inputQuantity: number
remark: string
}
): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (val: boolean) => emit('update:visible', val)
})
const startFormRef = ref()
const startForm = reactive({
workerName: '',
equipmentId: undefined as number | undefined,
equipmentName: '',
inputQuantity: 1,
remark: ''
})
const startRules = {
workerName: [{ required: true, message: '请输入操作工人姓名', trigger: 'blur' }],
inputQuantity: [{ required: true, message: '请输入投入数量', trigger: 'blur' }]
}
// 当前登录用户,用于默认填充工人姓名
const userStore = useUserStore()
const currentNickname = computed(() => userStore.user?.nickname || '')
watch(
() => props.visible,
(val) => {
if (val) {
// reset form when open
startForm.workerName = currentNickname.value || ''
startForm.equipmentId = undefined
startForm.equipmentName = ''
startForm.inputQuantity = props.defaultInputQuantity ?? 1
startForm.remark = ''
// 清理校验
startFormRef.value?.clearValidate?.()
}
}
)
const handleSubmit = () => {
startFormRef.value?.validate((valid: boolean) => {
if (!valid) return
// derive equipmentName
const eq = props.equipmentOptions?.find((e) => e.id === startForm.equipmentId)
const equipmentName = eq ? eq.name : ''
emit('submit', {
workerName: startForm.workerName,
equipmentId: startForm.equipmentId,
equipmentName,
inputQuantity: startForm.inputQuantity,
remark: startForm.remark
})
})
}
</script>

View File

@@ -0,0 +1,700 @@
<template>
<doc-alert title="【MES】生产工单管理" url="https://doc.iocoder.cn/mes/workorder/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="84px"
>
<el-form-item label="工单编号" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入工单编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工单状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
multiple
class="!w-240px"
>
<el-option label="草稿" :value="0" />
<el-option label="待审核" :value="1" />
<el-option label="已审核" :value="2" />
<el-option label="生产中" :value="3" />
<el-option label="已完成" :value="4" />
<el-option label="已关闭" :value="5" />
<el-option label="已取消" :value="6" />
<el-option label="已入库" :value="7" />
</el-select>
</el-form-item>
<el-form-item label="计划日期" prop="planTime">
<el-date-picker
v-model="queryParams.planTime"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:production-order:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['mes:production-order:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column type="selection" width="50" />
<el-table-column label="批次信息" align="center" min-width="150">
<template #default="{ row }">
<div>{{ row.code }}</div>
<div class="mt-5px">
<el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="产品信息" align="center" min-width="150">
<template #default="{ row }">
<div>{{ row.productName }}</div>
<div class="text-gray-400 text-sm">{{ row.productCode }}</div>
</template>
</el-table-column>
<el-table-column label="生产数量" align="center" min-width="120">
<template #default="{ row }">
<div>计划{{ row.planQuantity }}</div>
<div class="mt-5px text-gray-400">完成{{ row.producedQuantity || 0 }}</div>
</template>
</el-table-column>
<el-table-column label="计划时间" align="center" min-width="200">
<template #default="{ row }">
<div>
<el-tooltip
:content="formatFullDateTime(row.planStartTime)"
placement="top"
:disabled="!row.planStartTime"
>
<span>开始{{ formatDateTime(row.planStartTime) }}</span>
</el-tooltip>
</div>
<div class="mt-5px">
<el-tooltip
:content="formatFullDateTime(row.planEndTime)"
placement="top"
:disabled="!row.planEndTime"
>
<span>结束{{ formatDateTime(row.planEndTime) }}</span>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column label="实际时间" align="center" min-width="200">
<template #default="{ row }">
<div>
<el-tooltip
:content="formatFullDateTime(row.actualStartTime)"
placement="top"
:disabled="!row.actualStartTime"
>
<span
>开始{{
row.actualStartTime ? formatDateTime(row.actualStartTime) : '未开始'
}}</span
>
</el-tooltip>
</div>
<div class="mt-5px">
<el-tooltip
:content="formatFullDateTime(row.actualEndTime)"
placement="top"
:disabled="!row.actualEndTime"
>
<span
>结束{{ row.actualEndTime ? formatDateTime(row.actualEndTime) : '未结束' }}</span
>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column
label="备注"
align="center"
prop="remark"
min-width="150"
show-overflow-tooltip
/>
<el-table-column label="操作" align="center" fixed="right" width="200">
<template #default="scope">
<el-tooltip content="编辑" placement="top">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:production-order:update']"
v-if="scope.row.status === 0"
>
<!-- <Icon icon="ep:edit" />-->
编辑
</el-button>
</el-tooltip>
<el-tooltip content="开始生产" placement="top">
<el-button
link
type="primary"
@click="handleStart(scope.row.id)"
v-if="scope.row.status === 2"
v-hasPermi="['mes:production-order:start']"
>
<!-- <Icon icon="ep:video-play" />-->
开始生产
</el-button>
</el-tooltip>
<el-tooltip content="完成" placement="top">
<el-button
link
type="success"
@click="handleComplete(scope.row)"
v-if="scope.row.status === 3"
v-hasPermi="['mes:production-order:complete']"
>
<!-- <Icon icon="ep:check" />-->
完成
</el-button>
</el-tooltip>
<el-tooltip content="取消" placement="top">
<!-- <el-button-->
<!-- link-->
<!-- type="danger"-->
<!-- @click="handleCancel(scope.row.id)"-->
<!-- v-if="[0, 1, 2].includes(scope.row.status)"-->
<!-- v-hasPermi="['mes:production-order:cancel']"-->
<!-- >-->
<!--&lt;!&ndash; <Icon icon="ep:close" />&ndash;&gt;-->
<!-- 取消-->
<!-- </el-button>-->
</el-tooltip>
<el-tooltip content="提交审核" placement="top">
<el-button
link
type="primary"
@click="handleSubmit(scope.row.id)"
v-if="scope.row.status === 0"
v-hasPermi="['mes:production-order:submit']"
>
<!-- <Icon icon="ep:upload" />-->
提交
</el-button>
</el-tooltip>
<!-- <el-tooltip content="审核通过" placement="top">-->
<!-- <el-button-->
<!-- link-->
<!-- type="success"-->
<!-- @click="handleApprove(scope.row.id)"-->
<!-- v-if="scope.row.status === 1"-->
<!-- v-hasPermi="['mes:production-order:approve']"-->
<!-- >-->
<!--&lt;!&ndash; <Icon icon="ep:circle-check" />&ndash;&gt;-->
<!-- 审核-->
<!-- </el-button>-->
<!-- </el-tooltip>-->
<!-- <el-tooltip content="拒绝" placement="top">-->
<!-- <el-button-->
<!-- link-->
<!-- type="danger"-->
<!-- @click="handleReject(scope.row.id)"-->
<!-- v-if="scope.row.status === 1"-->
<!-- v-hasPermi="['mes:production-order:reject']"-->
<!-- >-->
<!--&lt;!&ndash; <Icon icon="ep:circle-close" />&ndash;&gt;-->
<!-- 拒绝-->
<!-- </el-button>-->
<!-- </el-tooltip>-->
<!-- <el-tooltip content="关闭" placement="top">-->
<!-- <el-button-->
<!-- link-->
<!-- type="danger"-->
<!-- @click="handleClose(scope.row.id)"-->
<!-- v-if="scope.row.status === 4"-->
<!-- v-hasPermi="['mes:production-order:close']"-->
<!-- >-->
<!-- <Icon icon="ep:switch" />-->
<!-- </el-button>-->
<!-- </el-tooltip>-->
<!-- 入库功能已停用按钮隐藏 -->
<!-- <el-tooltip content="入库" placement="top">-->
<!-- <el-button-->
<!-- link-->
<!-- type="success"-->
<!-- @click="handleProductionIn(scope.row)"-->
<!-- v-if="false && scope.row.status === 4"-->
<!-- v-hasPermi="['erp:production-in:create']"-->
<!-- >-->
<!-- <Icon icon="ep:box" />-->
<!-- </el-button>-->
<!-- </el-tooltip>-->
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
@click="handleDelete([scope.row.id])"
v-if="[0, 6].includes(scope.row.status)"
v-hasPermi="['mes:production-order:delete']"
>
<!-- <Icon icon="ep:delete" />-->
删除
</el-button>
</el-tooltip>
<el-tooltip content="工序执行" placement="top">
<el-button
link
type="primary"
@click="handleShowOperations(scope.row.id, scope.row.status)"
v-hasPermi="['mes:production-order:operations']"
>
<!-- <Icon icon="ep:list" />-->
详情
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<WorkOrderForm ref="formRef" @success="getList" />
<!-- 工序执行弹窗 -->
<OperationExecutionDialog ref="operationExecutionRef" />
<!-- 生产入库弹窗 -->
<el-dialog v-model="inboundDialogVisible" title="生产入库" width="520px" append-to-body>
<el-form label-width="120px">
<el-form-item label="选择仓库">
<el-select
v-model="selectedWarehouseId"
placeholder="请选择或输入仓库编号"
filterable
allow-create
default-first-option
clearable
class="!w-320px"
>
<el-option
v-for="w in warehouseOptions"
:key="w.id"
:label="w.name + '' + w.id + ''"
:value="w.id"
/>
</el-select>
</el-form-item>
<el-form-item label="产品条码">
<span>{{
(currentInboundRow &&
(currentInboundRow.productCode ||
currentInboundRow.productBarCode ||
currentInboundRow.barCode)) ||
'-'
}}</span>
</el-form-item>
<el-form-item label="入库数量">
<span>{{
(currentInboundRow &&
(currentInboundRow.producedQuantity || currentInboundRow.planQuantity)) ||
0
}}</span>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="inboundDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="inboundSubmitting" @click="confirmInbound"
>确定</el-button
>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Icon } from '@/components/Icon'
import WorkOrderForm from './WorkOrderForm.vue'
import OperationExecutionDialog from './components/OperationExecutionDialog.vue'
import { WarehouseApi } from '@/api/erp/stock/warehouse'
import {
YVHgetWorkOrderPage,
YVHdeleteWorkOrder,
YVHstartWorkOrder,
YVHcompleteWorkOrder,
YVHcancelWorkOrder,
YVHsubmitWorkOrder,
YVHapproveWorkOrder,
YVHrejectWorkOrder,
YVHcloseWorkOrder
} from '@/api/mes/production/workorder'
import { YVHgetWorkOrderOperationList } from '@/api/mes/production/order-operation'
import request from '@/config/axios'
// 隐藏旧的 ERP 入库逻辑,改为调用 MES 入库(按产品条码)接口
const loading = ref(true)
const total = ref(0)
const list = ref<any[]>([])
const queryFormRef = ref()
const formRef = ref()
const operationExecutionRef = ref()
const selectionList = ref<any[]>([])
// 入库弹窗相关
const inboundDialogVisible = ref(false)
const inboundSubmitting = ref(false)
const warehouseOptions = ref<Array<{ id: number; name: string }>>([])
const selectedWarehouseId = ref<string | number | undefined>(undefined)
const currentInboundRow = ref<any>(null)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
productName: undefined,
status: [] as number[],
planTime: [] as string[]
})
const getStatusType = (status: number) => {
switch (status) {
case 0: // 草稿
return 'warning' // 橙色
case 1: // 待审核
return 'primary' // 蓝色
case 2: // 已审核
return 'success' // 绿色
case 3: // 生产中
return 'warning' // 浅蓝色
case 4: // 已完成
return 'success' // 绿色
case 5: // 已关闭
return 'danger' // 红色
case 6: // 已取消
return 'warning' // 橙色
case 7: // 已入库
return 'success' // 绿色
default:
return 'info'
}
}
const getStatusText = (status: number) => {
switch (status) {
case 0:
return '草稿'
case 1:
return '待审核'
case 2:
return '已审核'
case 3:
return '生产中'
case 4:
return '已完成'
case 5:
return '已关闭'
case 6:
return '已取消'
case 7:
return '已入库'
default:
return '未知'
}
}
const getList = async () => {
loading.value = true
try {
// 处理时间范围参数
const params = {
...queryParams,
planTime: queryParams.planTime
? queryParams.planTime.map((date: string, index: number) => {
// 开始时间设置为当天 00:00:00结束时间设置为当天 23:59:59
return index === 0 ? `${date} 00:00:00` : `${date} 23:59:59`
})
: undefined
}
const res = await YVHgetWorkOrderPage(params)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.code = undefined
queryParams.productName = undefined
queryParams.status = []
queryParams.planTime = []
handleQuery()
}
const openForm = (type: 'create' | 'update', id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (ids: number[]) => {
if (!ids || ids.length === 0) return
try {
await ElMessageBox.confirm('确定要删除选中的工单吗?', '提示', { type: 'warning' })
for (const id of ids) {
await YVHdeleteWorkOrder(id)
}
ElMessage.success('删除成功')
getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {}
}
const handleStart = async (id: number) => {
try {
await ElMessageBox.confirm('确定要开始生产该工单吗?', '提示', { type: 'warning' })
await YVHstartWorkOrder({ id })
ElMessage.success('操作成功')
getList()
} catch {}
}
const handleComplete = async (row: any) => {
try {
const result = await ElMessageBox.prompt('请输入实际完成数量', '完成工单', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'number',
inputValue: '',
inputValidator: (value) => {
if (value === undefined || value === null || String(value).trim() === '') {
return '实际完成数量不能为空'
}
const num = Number(value)
// if (!Number.isFinite(num) || !Number.isInteger(num)) {
// return '请输入整数'
// }
if (num < 0) {
return '实际完成数量不能小于 0'
}
return true
}
})
const producedQuantity = Number(result.value)
await YVHcompleteWorkOrder({ id: row.id, producedQuantity })
ElMessage.success('操作成功')
getList()
} catch {}
}
const handleCancel = async (id: number) => {
try {
await ElMessageBox.confirm('确定要取消该工单吗?', '提示', { type: 'warning' })
await YVHcancelWorkOrder({ id })
ElMessage.success('操作成功')
getList()
} catch {}
}
const handleSubmit = async (id: number) => {
try {
await ElMessageBox.confirm('确定要提交该工单审核吗?', '提示', { type: 'warning' })
await YVHsubmitWorkOrder({ id })
ElMessage.success('提交成功')
getList()
} catch {}
}
const handleApprove = async (id: number) => {
try {
await ElMessageBox.confirm('确定要审核通过该工单吗?', '提示', { type: 'warning' })
await YVHapproveWorkOrder({ id })
ElMessage.success('审核通过')
getList()
} catch {}
}
const handleReject = async (id: number) => {
try {
await ElMessageBox.confirm('确定要拒绝该工单吗?', '提示', { type: 'warning' })
await YVHrejectWorkOrder({ id })
ElMessage.success('已拒绝')
getList()
} catch {}
}
const handleClose = async (id: number) => {
try {
await ElMessageBox.confirm('确定要关闭该工单吗?', '提示', { type: 'warning' })
await YVHcloseWorkOrder({ id })
ElMessage.success('已关闭')
getList()
} catch {}
}
const handleProductionIn = async (row: any) => {
try {
currentInboundRow.value = row
selectedWarehouseId.value = undefined
// 加载仓库下拉
const list = await WarehouseApi.getWarehouseSimpleList()
warehouseOptions.value = Array.isArray(list) ? list : list?.data || []
inboundDialogVisible.value = true
} catch (error) {
console.error('加载仓库列表失败:', error)
// 即使加载失败,也允许用户手动输入
inboundDialogVisible.value = true
}
}
const confirmInbound = async () => {
if (!currentInboundRow.value) return
// 优先选择下拉的仓库ID否则使用手动输入
const chosenId = selectedWarehouseId.value
const warehouseId = Number(chosenId)
if (!Number.isInteger(warehouseId) || warehouseId <= 0) {
ElMessage.error('请选择或输入合法的仓库编号')
return
}
const barCode =
currentInboundRow.value.productCode ||
currentInboundRow.value.productBarCode ||
currentInboundRow.value.barCode
if (!barCode) {
ElMessage.error('缺少产品条码,无法入库')
return
}
const count = currentInboundRow.value.producedQuantity || currentInboundRow.value.planQuantity
inboundSubmitting.value = true
try {
await request.post({
url: '/mes/stock/inbound',
data: {
barCode,
warehouseId,
count,
workOrderId: currentInboundRow.value.id
}
})
ElMessage.success('入库成功')
inboundDialogVisible.value = false
} catch (error) {
console.error('入库失败:', error)
ElMessage.error('入库失败')
} finally {
inboundSubmitting.value = false
}
}
const handleShowOperations = async (id: number, status: number) => {
console.log('显示工序执行弹窗工单ID:', id, '工单状态:', status)
if (operationExecutionRef.value) {
operationExecutionRef.value.open(id, status)
} else {
console.error('operationExecutionRef 未定义')
ElMessage.error('无法打开工序执行弹窗')
}
}
const handleSelectionChange = (selection: any[]) => {
selectionList.value = selection
}
/** 行点击操作 */
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
// 检查是否点击了按钮、链接或其他交互元素
const target = event.target as HTMLElement
if (
target.tagName === 'BUTTON' ||
target.tagName === 'A' ||
target.tagName === 'I' ||
target.tagName === 'svg' ||
target.closest('button') ||
target.closest('a') ||
target.closest('.el-button') ||
target.closest('.el-checkbox')
) {
return
}
// 草稿状态打开编辑页面,其他状态打开工序执行页面
if (row.status === 0) {
openForm('update', row.id)
} else {
handleShowOperations(row.id, row.status)
}
}
const formatDateTime = (time: string) => {
if (!time) return ''
return time.split('T')[0]
}
const formatFullDateTime = (time: string) => {
if (!time) return ''
return time.replace('T', ' ')
}
getList()
</script>

View File

@@ -0,0 +1,185 @@
<template>
<el-dialog
:title="formType === 'create' ? '新增工位绑定' : '编辑工位绑定'"
v-model="visible"
width="500px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" v-loading="formLoading">
<el-form-item label="工位编号" prop="code">
<el-input v-model="form.code" placeholder="请输入工位编号" />
</el-form-item>
<el-form-item label="工位名称" prop="name">
<el-input v-model="form.name" placeholder="请输入工位名称" />
</el-form-item>
<el-form-item label="绑定工序" prop="processId">
<el-select
v-model="form.processId"
placeholder="请选择绑定工序"
filterable
clearable
class="!w-full"
>
<el-option
v-for="item in processList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="报工人" prop="reporterId">
<el-select
v-model="form.reporterId"
placeholder="请选择报工人"
filterable
clearable
class="!w-full"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import {
YVHcreateWorkstationBinding,
YVHupdateWorkstationBinding,
YVHgetWorkstationBinding
} from '@/api/mes/production/workstation'
import { YVHgetProcessOperationList } from '@/api/mes/production/process-operation'
import * as UserApi from '@/api/system/user'
// 生成工位编号
const generateWorkstationCode = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const random = Math.floor(Math.random() * 1000)
.toString()
.padStart(3, '0')
return `WS${year}${month}${day}${random}`
}
const emits = defineEmits(['success'])
const visible = ref(false)
const loading = ref(false)
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const form = reactive<any>({})
const formRef = ref()
const processList = ref<any[]>([])
const userList = ref<any[]>([])
const rules = {
code: [{ required: true, message: '请输入工位编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入工位名称', trigger: 'blur' }],
processId: [{ required: true, message: '请选择绑定工序', trigger: 'change' }],
reporterId: [{ required: true, message: '请选择报工人', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// 获取工序列表
const getProcessList = async () => {
try {
processList.value = await YVHgetProcessOperationList()
} catch (error) {
console.error('获取工序列表失败:', error)
}
}
// 获取用户列表
const getUserList = async () => {
try {
const res = await UserApi.getSimpleUserList()
userList.value = res
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
const open = async (type: 'create' | 'update', id?: number) => {
formType.value = type
visible.value = true
formLoading.value = true
try {
// 获取下拉列表数据
await Promise.all([getProcessList(), getUserList()])
// 重置表单
Object.assign(form, {
id: undefined,
code: type === 'create' ? generateWorkstationCode() : '',
name: '',
processId: undefined,
reporterId: undefined,
status: 1,
remark: ''
})
if (type === 'update' && id) {
const res = await YVHgetWorkstationBinding(id)
Object.assign(form, res)
}
} catch (error) {
console.error('初始化表单失败:', error)
ElMessage.error('加载数据失败,请重试')
visible.value = false
} finally {
formLoading.value = false
}
}
const submitForm = () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (formType.value === 'create') {
await YVHcreateWorkstationBinding(form)
ElMessage.success('新增成功')
} else {
await YVHupdateWorkstationBinding(form)
ElMessage.success('修改成功')
}
visible.value = false
emits('success')
} finally {
loading.value = false
}
})
}
const handleClose = () => {
formRef.value?.resetFields()
}
// 对外暴露 open 方法
defineExpose({ open })
</script>

View File

@@ -0,0 +1,210 @@
<template>
<doc-alert title="【MES】工位绑定" url="https://doc.iocoder.cn/mes/workstation-binding/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="工位编号" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入工位编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工位名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入工位名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:workstation-binding:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['mes:workstation-binding:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column type="selection" width="50" />
<el-table-column width="80" label="ID" align="center" prop="id" />
<el-table-column min-width="120" label="工位编号" align="center" prop="code" />
<el-table-column min-width="120" label="工位名称" align="center" prop="name" />
<el-table-column min-width="150" label="绑定工序" align="center" prop="processName" />
<el-table-column min-width="120" label="报工人" align="center" prop="reporterName" />
<el-table-column min-width="100" label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column min-width="150" label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:workstation-binding:update']"
>编辑</el-button
>
<el-button
link
type="danger"
@click="handleDelete([scope.row.id])"
v-hasPermi="['mes:workstation-binding:delete']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<WorkstationBindingForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
YVHgetWorkstationBindingPage,
YVHdeleteWorkstationBinding
} from '@/api/mes/production/workstation'
import { Icon } from '@/components/Icon'
import WorkstationBindingForm from './WorkstationBindingForm.vue'
const loading = ref(true)
const list = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
status: undefined
})
const queryFormRef = ref()
const formRef = ref()
const selectionList = ref<any[]>([])
const getList = async () => {
loading.value = true
try {
const res = await YVHgetWorkstationBindingPage(queryParams)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields?.()
queryParams.code = undefined
queryParams.name = undefined
queryParams.status = undefined
handleQuery()
}
const openForm = (type: 'create' | 'update', id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (ids: number[]) => {
if (!ids || ids.length === 0) return
try {
await ElMessageBox.confirm('确定要删除选中的工位绑定吗?', '提示', { type: 'warning' })
for (const id of ids) {
await YVHdeleteWorkstationBinding(id)
}
ElMessage.success('删除成功')
getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {}
}
const handleSelectionChange = (rows: any[]) => {
selectionList.value = rows
}
/** 行点击操作 */
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
// 检查是否点击了按钮、链接或其他交互元素
const target = event.target as HTMLElement
if (
target.tagName === 'BUTTON' ||
target.tagName === 'A' ||
target.tagName === 'I' ||
target.tagName === 'svg' ||
target.closest('button') ||
target.closest('a') ||
target.closest('.el-button') ||
target.closest('.el-checkbox')
) {
return
}
// 直接打开编辑页面
openForm('update', row.id)
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,497 @@
<template>
<el-dialog :title="formTitle" v-model="dialogVisible" width="60%" append-to-body destroy-on-close>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="编号" prop="code">
<el-input v-model="formData.code" placeholder="自动生成" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="formData.batchNo" placeholder="请选择工单批次号" readonly>
<template #append>
<el-button @click="openBatchSelector" type="primary" :icon="Search">选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品" prop="productId">
<el-select
v-model="formData.productId"
placeholder="请选择产品"
clearable
@change="handleProductChange"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工序路线" prop="processRouteId">
<el-select
v-model="formData.processRouteId"
placeholder="请选择工序路线"
clearable
@change="handleProcessRouteChange"
>
<el-option
v-for="item in processRouteList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工序" prop="processId">
<el-select
v-model="formData.processId"
placeholder="请选择工序"
clearable
@change="handleProcessChange"
>
<el-option
v-for="item in processList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="加工前重量" prop="beforeWeight">
<el-input-number
v-model="formData.beforeWeight"
:precision="2"
:step="0.1"
@change="handleWeightChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="加工后重量" prop="afterWeight">
<el-input-number
v-model="formData.afterWeight"
:precision="2"
:step="0.1"
@change="handleWeightChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="颜色" prop="color">
<el-input v-model="formData.color" placeholder="请输入颜色" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="损耗率" prop="lossRate">
<el-input v-model="formData.lossRate" disabled>
<template #append>%</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="粗细度" prop="thickness">
<el-input v-model="formData.thickness" placeholder="请输入粗细度" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数量" prop="quantity">
<el-input-number v-model="formData.quantity" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否合格" prop="qualified">
<el-switch v-model="formData.qualified" />
<el-tooltip
effect="dark"
content="系统根据季节和损耗率自动判断是否合格"
placement="top"
>
<el-icon class="el-icon--right"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="配料比例" prop="ratio">
<el-input v-model="formData.ratio" placeholder="请输入配料比例" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="formData.startTime"
type="datetime"
placeholder="选择开始时间"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="formData.endTime" type="datetime" placeholder="选择结束时间" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="data1">
<el-input v-model="formData.data1" placeholder="请输入备注" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="说明" prop="data2">
<el-input v-model="formData.data2" placeholder="请输入说明" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="扩展" prop="data3">
<el-input v-model="formData.data3" placeholder="请输入扩展" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</div>
</template>
</el-dialog>
<!-- 批次选择弹窗 -->
<el-dialog
title="选择工单"
v-model="batchSelectorVisible"
width="70%"
append-to-body
destroy-on-close
>
<el-table
v-loading="workOrdersLoading"
:data="workOrdersList"
border
style="width: 100%"
@row-click="handleWorkOrderSelect"
highlight-current-row
>
<el-table-column prop="code" label="工单编号" width="180" />
<el-table-column prop="name" label="工单名称" width="220" />
<el-table-column prop="productName" label="产品" width="150" />
<el-table-column prop="routeName" label="工艺路线" width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag v-if="scope.row.status === 3" type="warning">生产中</el-tag>
<el-tag v-else-if="scope.row.status === 4" type="success">已完成</el-tag>
</template>
</el-table-column>
<el-table-column prop="planQuantity" label="计划数量" width="100" />
<el-table-column prop="producedQuantity" label="已生产" width="100" />
<el-table-column prop="qualifiedQuantity" label="合格数量" width="100" />
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="batchSelectorVisible = false">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
createProcessQuality,
updateProcessQuality,
getProcessQuality,
getProcessRouteList
} from '@/api/mes/quality/process-quality'
import { YVHgetProcessOperationPage } from '@/api/mes/production/process-operation'
import { ProductApi } from '@/api/erp/product/product'
import { formatDate } from '@/utils/formatTime'
import { QuestionFilled } from '@element-plus/icons-vue'
import { Search } from '@element-plus/icons-vue'
// 引入工单API接口
import { YVHgetWorkOrderPage, YVHgetWorkOrder } from '@/api/mes/production/workorder'
interface Product {
id: number
name: string
}
interface Process {
id: number
name: string
}
interface ProcessRoute {
id: number
name: string
code: string
}
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const formTitle = ref('')
const formType = ref<'create' | 'update'>('create')
const loading = ref(false)
const formRef = ref()
const productList = ref<Product[]>([])
const processList = ref<Process[]>([])
const processRouteList = ref<ProcessRoute[]>([])
const batchSelectorVisible = ref(false)
const workOrdersList = ref([])
const workOrdersLoading = ref(false)
const formData = reactive({
id: undefined as number | undefined,
code: '',
batchNo: '',
productId: undefined as number | undefined,
productName: '',
processRouteId: undefined as number | undefined,
processRouteName: '',
processId: undefined as number | undefined,
processName: '',
beforeWeight: undefined as number | undefined,
afterWeight: undefined as number | undefined,
color: '',
lossRate: undefined as string | undefined,
thickness: '',
quantity: undefined as number | undefined,
qualified: false,
ratio: '',
startTime: undefined as Date | undefined,
endTime: undefined as Date | undefined,
data1: '',
data2: '',
data3: ''
})
const formRules = {
// code: [{ required: true, message: '请输入编号', trigger: 'blur' }],
batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
processId: [{ required: true, message: '请选择工序', trigger: 'change' }]
}
// 打开表单
const open = async (type: string, id?: number) => {
dialogVisible.value = true
formType.value = type as 'create' | 'update'
formTitle.value = type === 'create' ? '新增工序质检' : '编辑工序质检'
resetForm()
// 加载产品列表
const productRes = await ProductApi.getProductSimpleList()
productList.value = productRes
// 加载工序路线列表
const routeRes = await getProcessRouteList()
processRouteList.value = routeRes
// 加载工序列表
const processRes = await YVHgetProcessOperationPage({ pageNo: 1, pageSize: 100 })
processList.value = processRes.list
// 修改时加载详情
if (type === 'update' && id) {
loading.value = true
try {
const res = await getProcessQuality(id)
// Convert date strings/timestamps to Date objects before assigning
if (res.startTime) {
res.startTime = new Date(res.startTime)
}
if (res.endTime) {
res.endTime = new Date(res.endTime)
}
Object.assign(formData, res)
} finally {
loading.value = false
}
}
}
// 提交表单
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (valid) {
console.log(formRef)
try {
loading.value = true
// 创建提交数据的副本并处理日期 - 使用时间戳而不是格式化字符串
const submitData = {
...formData,
startTime: formData.startTime ? (formData.startTime instanceof Date ? formData.startTime.getTime() : formData.startTime) : undefined,
endTime: formData.endTime ? (formData.endTime instanceof Date ? formData.endTime.getTime() : formData.endTime) : undefined
}
if (formType.value === 'create') {
await createProcessQuality(submitData)
ElMessage.success('新增成功')
} else {
await updateProcessQuality(submitData)
ElMessage.success('修改成功')
}
dialogVisible.value = false
emit('success')
} finally {
loading.value = false
}
}
})
}
// 重置表单
const resetForm = () => {
formData.id = undefined
formData.code = ''
formData.batchNo = ''
formData.productId = undefined
formData.productName = ''
formData.processRouteId = undefined
formData.processRouteName = ''
formData.processId = undefined
formData.processName = ''
formData.beforeWeight = undefined
formData.afterWeight = undefined
formData.color = ''
formData.lossRate = undefined
formData.thickness = ''
formData.quantity = undefined
formData.qualified = false
formData.ratio = ''
// 设置默认开始时间为当前时间
const now = new Date()
formData.startTime = now
// 设置默认结束时间为开始时间后一天
const tomorrow = new Date(now)
tomorrow.setDate(tomorrow.getDate() + 1)
formData.endTime = tomorrow
formData.data1 = ''
formData.data2 = ''
formData.data3 = ''
}
// 产品选择变更
const handleProductChange = (val: number) => {
const product = productList.value.find((item) => item.id === val)
if (product) {
formData.productName = product.name
}
}
// 工序路线选择变更
const handleProcessRouteChange = (val: number) => {
const route = processRouteList.value.find((item) => item.id === val)
if (route) {
formData.processRouteName = route.name
}
}
// 工序选择变更
const handleProcessChange = (val: number) => {
const process = processList.value.find((item) => item.id === val)
if (process) {
formData.processName = process.name
}
}
// 重量变更时计算损耗率
const handleWeightChange = () => {
if (formData.beforeWeight && formData.afterWeight && formData.beforeWeight > 0) {
formData.lossRate = (
((formData.beforeWeight - formData.afterWeight) / formData.beforeWeight) *
100
).toFixed(2)
}
}
// 打开批次选择器
const openBatchSelector = async () => {
batchSelectorVisible.value = true
workOrdersLoading.value = true
try {
// 查询状态为3(生产中)和4(已完成)的工单
const res = await YVHgetWorkOrderPage({
pageNo: 1,
pageSize: 100,
status: [3, 4]
})
workOrdersList.value = res.list || []
} catch (error) {
console.error('Failed to fetch work orders:', error)
ElMessage.error('获取工单列表失败')
} finally {
workOrdersLoading.value = false
}
}
// 选择工单
const handleWorkOrderSelect = async (row: any) => {
try {
// 获取工单详细信息
const workOrder = await YVHgetWorkOrder(row.id)
// 填充批次号
formData.batchNo = workOrder.code
// 填充产品信息
formData.productId = workOrder.productId
formData.productName = workOrder.productName
// 填充工艺路线信息
formData.processRouteId = workOrder.routeId
formData.processRouteName = workOrder.routeName
// 如果有工序信息,更新工序选择下拉框
if (workOrder.operations && workOrder.operations.length > 0) {
// 更新工序列表,只显示已完成的工序
processList.value = workOrder.operations
.filter(op => op.status === 2) // 已完成的工序
.map(op => ({
id: op.operationId,
name: op.operationName
}))
// 如果只有一个工序,自动选择
if (processList.value.length === 1) {
formData.processId = processList.value[0].id
formData.processName = processList.value[0].name
}
}
// 关闭批次选择器
batchSelectorVisible.value = false
// 显示成功消息
ElMessage.success('工单信息已加载')
} catch (error) {
console.error('获取工单详情失败:', error)
ElMessage.error('获取工单详情失败')
}
}
defineExpose({ open })
</script>
<style scoped>
.dialog-footer {
text-align: right;
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="批次号" prop="batchNo">
<el-input
v-model="queryParams.batchNo"
placeholder="请输入批次号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="工序名称" prop="processName">
<el-input
v-model="queryParams.processName"
placeholder="请输入工序名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:process-quality:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="编号" prop="code" min-width="180" align="center" />
<el-table-column label="批次号" prop="batchNo" min-width="150" align="center" />
<el-table-column label="产品名称" prop="productName" min-width="120" align="center" />
<el-table-column label="工序路线" prop="processRouteName" min-width="120" align="center" />
<el-table-column label="工序名称" prop="processName" min-width="120" align="center" />
<el-table-column label="加工前重量" prop="beforeWeight" min-width="100" align="center" />
<el-table-column label="加工后重量" prop="afterWeight" min-width="100" align="center" />
<el-table-column label="颜色" prop="color" min-width="80" align="center" />
<el-table-column label="损耗率" prop="lossRate" min-width="80" align="center">
<template #default="scope">
{{ scope.row.lossRate }}%
</template>
</el-table-column>
<el-table-column label="粗细度" prop="thickness" min-width="80" align="center" />
<el-table-column label="数量" prop="quantity" min-width="80" align="center" />
<el-table-column label="是否合格" prop="qualified" min-width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.qualified ? 'success' : 'danger'">
{{ scope.row.qualified ? '合格' : '不合格' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="配料比例" prop="ratio" min-width="100" align="center" />
<el-table-column label="开始时间" prop="startTime" min-width="160" align="center">
<template #default="scope">
{{ formatDate(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column label="结束时间" prop="endTime" min-width="160" align="center">
<template #default="scope">
{{ formatDate(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="备注" prop="data1" min-width="100" align="center" />
<el-table-column label="说明" prop="data2" min-width="100" align="center" />
<el-table-column label="扩展" prop="data3" min-width="100" align="center" />
<el-table-column label="操作" width="160" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row)"
v-hasPermi="['mes:process-quality:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row)"
v-hasPermi="['mes:process-quality:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-if="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗 -->
<ProcessQualityForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus'
import { formatDate } from '@/utils/formatTime'
import { getProcessQualityPage, deleteProcessQuality } from '@/api/mes/quality/process-quality'
import ProcessQualityForm from './ProcessQualityForm.vue'
import { Icon } from '@/components/Icon'
import { ContentWrap } from '@/components/ContentWrap'
/** 工序质检列表 */
defineOptions({ name: 'MesProcessQuality' })
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref<FormInstance>()
const formRef = ref()
// 查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
batchNo: '',
productName: '',
processName: ''
})
// 获取列表数据
const getList = async () => {
loading.value = true
try {
const res = await getProcessQualityPage(queryParams)
list.value = res.list
total.value = res.total
} catch (error) {
console.error('获取列表失败', error)
} finally {
loading.value = false
}
}
// 查询操作
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
// 重置查询
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
// 打开表单
const openForm = (type: string, row?: any) => {
formRef.value?.open(type, row?.id)
}
// 删除操作
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm('是否确认删除该工序质检记录?', '警告', {
type: 'warning'
})
await deleteProcessQuality(row.id)
ElMessage.success('删除成功')
await getList()
} catch (error) {
console.error('删除失败', error)
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,898 @@
<template>
<ContentWrap>
<!-- 搜索表单 -->
<el-form
ref="searchFormRef"
:model="searchForm"
:inline="true"
label-width="68px"
class="-mb-15px"
>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="searchForm.productName"
placeholder="请输入产品名称"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="批次编号" prop="workOrderCode">
<el-input
v-model="searchForm.workOrderCode"
placeholder="请输入批次编号"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="质检状态" prop="status">
<el-select
v-model="searchForm.status"
placeholder="请选择质检状态"
clearable
class="!w-240px"
>
<el-option label="待检" :value="0" />
<el-option label="检验中" :value="1" />
<el-option label="已完成" :value="2" />
<el-option label="已入库" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="是否合格" prop="qualified">
<el-select
v-model="searchForm.qualified"
placeholder="请选择是否合格"
clearable
class="!w-240px"
>
<el-option label="合格" :value="1" />
<el-option label="不合格" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="质检时间" prop="inspectionTime">
<el-date-picker
v-model="searchForm.inspectionTime"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
class="!w-420px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-[5px]" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-[5px]" /> 重置
</el-button>
<el-button
type="primary"
plain
@click="openDialog('create')"
v-hasPermi="['mes:finished-product-quality:create']"
>
<Icon icon="ep:plus" class="mr-[5px]" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
v-hasPermi="['mes:finished-product-quality:query']"
>
<Icon icon="ep:download" class="mr-[5px]" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 数据表格 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="质检编号" align="center" prop="code" />
<el-table-column label="批次编号" align="center" prop="workOrderCode" />
<!-- 合并产品名称与产品编号为一列 -->
<el-table-column label="产品信息" align="center">
<template #default="scope">
<div>{{ scope.row.productName }}</div>
<div style="color: #909399; font-size: 12px; margin-top: 2px">{{
scope.row.productCode
}}</div>
</template>
</el-table-column>
<!-- 合并质检状态与是否合格为一列 -->
<el-table-column label="质检状态/合格" align="center">
<template #default="scope">
<el-tag
:type="scope.row.status === 0 ? 'info' : scope.row.status === 1 ? 'warning' : 'success'"
>
{{
scope.row.status === 0
? '待检'
: scope.row.status === 1
? '检验中'
: scope.row.status === 2
? '已完成'
: '已入库'
}}
</el-tag>
<div v-if="scope.row.status === 2" style="margin-top: 6px">
<el-tag :type="scope.row.qualified ? 'success' : 'danger'">
{{ scope.row.qualified ? '合格' : '不合格' }}
</el-tag>
</div>
</template>
</el-table-column>
<!-- 合并质检数量合格数量不合格数量为一列 -->
<el-table-column label="重量(kg)" align="center">
<template #default="scope">
<div
>合格重量{{
scope.row.status === 2 || scope.row.status === 3 ? (scope.row.weight ?? '-') : '-'
}}</div
>
</template>
</el-table-column>
<el-table-column label="质检员" align="center" prop="inspectorName" />
<el-table-column label="质检时间" align="center" prop="inspectionTime" width="180" sortable/>
<el-table-column label="操作" align="center" width="260">
<template #default="scope">
<el-button
v-if="scope.row.status !== 0"
type="primary"
link @click.stop="openDetail(scope.row)"
>
查看 </el-button>
<el-button
v-if="scope.row.status !== 2 && scope.row.status !== 3"
type="success"
link
@click.stop="openDialog('edit', scope.row)"
v-hasPermi="['mes:finished-product-quality:update']"
>
编辑
</el-button>
<el-button
v-if="scope.row.status === 0 || scope.row.status === 1"
type="warning"
link
@click.stop="handleApprove(scope.row)"
v-hasPermi="['mes:finished-product-quality:update']"
>
审核
</el-button>
<el-button
v-if="scope.row.status !== 2 && scope.row.status !== 3"
type="danger"
link
@click.stop="handleDelete(scope.row)"
v-hasPermi="['mes:finished-product-quality:delete']"
>
删除
</el-button>
<el-button
v-if="scope.row.status === 2"
type="success"
link
@click.stop="handleOpenInbound(scope.row)"
v-hasPermi="['mes:finished-product-quality:update']"
>
入库
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 引入弹窗组件 -->
<QualityDialog
v-model:visible="dialog.visible"
:type="dialog.type"
:title="dialog.title"
:data="currentRow"
@submit="handleDialogSubmit"
/>
<!-- 查看抽屉左侧工单生产信息右侧质检信息 -->
<el-drawer v-model="detail.visible" size="80%" :with-header="true" title="查看详情">
<el-row :gutter="16">
<!-- 左侧批次生产信息 -->
<el-col :span="12">
<el-card shadow="never" header="批次生产信息" v-loading="detail.loadingWorkOrder">
<el-empty v-if="!detail.workOrder && !detail.loadingWorkOrder" description="无工单数据" />
<template v-else>
<el-descriptions :column="1" border>
<el-descriptions-item label="工单编号">{{
detail.workOrder?.code || '-'
}}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{
detail.workOrder?.productName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="产品编号">{{
detail.workOrder?.productCode || '-'
}}</el-descriptions-item>
<el-descriptions-item label="工序路线">{{
detail.workOrder?.routeName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="实际生产数量">{{
detail.workOrder?.producedQuantity ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="优先级">{{
detail.workOrder?.priority ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="合格数量">{{
detail.workOrder?.qualifiedQuantity ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="不合格数量">{{
detail.workOrder?.unqualifiedQuantity ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="合格率">{{
formatPercent(
detail.workOrder?.qualifiedQuantity,
detail.workOrder?.producedQuantity
)
}}</el-descriptions-item>
<el-descriptions-item label="状态">{{
workOrderStatusText(detail.workOrder?.status)
}}</el-descriptions-item>
<el-descriptions-item label="实际开始时间">{{
formatDateTime(detail.workOrder?.actualStartTime)
}}</el-descriptions-item>
<el-descriptions-item label="实际结束时间">{{
formatDateTime(detail.workOrder?.actualEndTime)
}}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{
formatDateTime(detail.workOrder?.createTime)
}}</el-descriptions-item>
<el-descriptions-item label="备注">{{
detail.workOrder?.remark || '-'
}}</el-descriptions-item>
</el-descriptions>
<el-divider>工序信息</el-divider>
<el-table
:data="detail.workOrder?.operations || []"
border
size="small"
max-height="280"
>
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column prop="operationCode" label="工序编码" min-width="120" />
<el-table-column prop="operationName" label="工序名称" min-width="140" />
<el-table-column prop="sequence" label="顺序" width="80" align="center" />
<el-table-column prop="completedQuantity" label="完成数" width="100" align="center" />
<el-table-column prop="qualifiedQuantity" label="合格数" width="100" align="center" />
<el-table-column
prop="unqualifiedQuantity"
label="不合格数"
width="110"
align="center"
/>
<el-table-column label="完成时间" min-width="160">
<template #default="{ row }">{{ formatDateTime(row.actualEndTime) }}</template>
</el-table-column>
</el-table>
</template>
</el-card>
</el-col>
<!-- 右侧质检信息 -->
<el-col :span="12">
<el-card shadow="never" header="质检信息">
<el-descriptions :column="2" border>
<el-descriptions-item label="质检编号">{{
detail.row?.code || '-'
}}</el-descriptions-item>
<el-descriptions-item label="批次编号">{{
detail.row?.workOrderCode || '-'
}}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{
detail.row?.productName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="产品编号">{{
detail.row?.productCode || '-'
}}</el-descriptions-item>
<el-descriptions-item label="质检状态">{{
detail.row?.status === 0
? '待检'
: detail.row?.status === 1
? '检验中'
: detail.row?.status === 2
? '已完成'
: '已入库'
}}</el-descriptions-item>
<el-descriptions-item label="是否合格">{{
detail.row?.qualified ? '合格' : '不合格'
}}</el-descriptions-item>
<el-descriptions-item label="质检员">{{
detail.row?.inspectorName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="质检时间">{{
detail.row?.inspectionTime || '-'
}}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{
detail.row?.remark || '-'
}}</el-descriptions-item>
<el-descriptions-item label="不合格原因" :span="2" v-if="detail.row?.unqualifiedReason">{{
detail.row?.unqualifiedReason || '-'
}}</el-descriptions-item>
</el-descriptions>
<template v-if="isTomato(detail.row?.workOrderCode)">
<el-divider>番茄酱番茄附加检验</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="工艺">{{
detail.row?.tomatoProcess ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="浓度(%)">{{
detail.row?.tomatoConcentration ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="数量">{{
detail.row?.tomatoQuantity ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="重量(kg)">{{
detail.row?.weight ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="可溶性固形物(%)">{{
detail.row?.tomatoSolubleSolidsPercent ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="粘稠度(cm/30s)">{{
detail.row?.tomatoViscosityCmPer ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="番茄红素(mg/100g)">{{
detail.row?.tomatoLycopeneMgPer ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="总酸(%)">{{
detail.row?.tomatoTotalAcidPercent ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="PH 值">{{
detail.row?.tomatoPhValue ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="色差 a/b">{{
detail.row?.tomatoColorDifference ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="霉菌(%)">{{
detail.row?.tomatoMouldPositivePercent ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="评定">{{
detail.row?.tomatoAssessConformance === 1
? '品质符合'
: detail.row?.tomatoAssessConformance === 0
? '品质不符合'
: '-'
}}</el-descriptions-item>
<el-descriptions-item label="评定说明" :span="2">{{
detail.row?.tomatoAssessRemark || '-'
}}</el-descriptions-item>
<el-descriptions-item label="感官" :span="2">{{
detail.row?.tomatoSensory || '-'
}}</el-descriptions-item>
<el-descriptions-item label="番茄备注" :span="2">{{
detail.row?.remark || '-'
}}</el-descriptions-item>
<el-descriptions-item label="附件" :span="2">
<template v-if="detail.row?.attachmentUrls">
<el-link type="primary" :href="detail.row.attachmentUrls" target="_blank">查看附件</el-link>
</template>
<template v-else>-</template>
</el-descriptions-item>
</el-descriptions>
</template>
<template v-else-if="isStevia(detail.row?.workOrderCode)">
<el-divider>甜菊糖甜叶菊附加检验</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="标签" :span="2">{{
detail.row?.steviaLabel || '-'
}}</el-descriptions-item>
<!-- <el-descriptions-item label="色泽">{{-->
<!-- detail.row?.steviaColor || '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="色泽合格">{{-->
<!-- detail.row?.steviaColorOk ? '✓' : '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="状态">{{-->
<!-- detail.row?.steviaState || '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="状态合格">{{-->
<!-- detail.row?.steviaStateOk ? '✓' : '-'-->
<!-- }}</el-descriptions-item>-->
<el-descriptions-item label="甜菊糖苷含量(%)">{{
detail.row?.steviaGlycosidesPercent ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="灰分(%)">{{
detail.row?.steviaAshPercent ?? '-'
}}</el-descriptions-item>
<!-- <el-descriptions-item label="干燥减重(%)">{{-->
<!-- detail.row?.steviaLossOnDryingPercent ?? '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="甲醇(mg/kg)">{{-->
<!-- detail.row?.steviaMethanolMgPerKg ?? '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="乙醇(mg/kg)">{{-->
<!-- detail.row?.steviaEthanolMgPerKg ?? '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="铅(mg/kg)">{{-->
<!-- detail.row?.steviaLeadMgPerKg ?? '-'-->
<!-- }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="总砷(mg/kg)">{{-->
<!-- detail.row?.steviaTotalArsenicMgPerKg ?? '-'-->
<!-- }}</el-descriptions-item>-->
<el-descriptions-item label="PH 值">{{
detail.row?.steviaPhValue ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="RD">{{
detail.row?.steviaRd ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="RA">{{
detail.row?.steviaRa ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="STV">{{
detail.row?.steviaStv ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="RF">{{
detail.row?.steviaRf ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="RC">{{
detail.row?.steviaRc ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="DA">{{
detail.row?.steviaDa ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="RV">{{
detail.row?.steviaRv ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="RB">{{
detail.row?.steviaRb ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="SB">{{
detail.row?.steviaSb ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="水分(%)">{{
detail.row?.steviaMoisture ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="比吸光">{{
detail.row?.steviaSpecificAbsorbance ?? '-'
}}</el-descriptions-item>
<el-descriptions-item label="透光率(%)">{{
detail.row?.steviaTransmittance ?? '-'
}}</el-descriptions-item>
<!-- <el-descriptions-item label="产量(kg)">{{-->
<!-- detail.row?.steviaYield ?? '-'-->
<!-- }}</el-descriptions-item>-->
<el-descriptions-item label="甜菊糖备注" :span="2">{{
detail.row?.steviaRemark || '-'
}}</el-descriptions-item>
<el-descriptions-item label="附件" :span="2">
<template v-if="detail.row?.attachmentUrls">
<el-link type="primary" :href="detail.row.attachmentUrls" target="_blank">查看附件</el-link>
</template>
<template v-else>-</template>
</el-descriptions-item>
</el-descriptions>
</template>
</el-card>
</el-col>
</el-row>
</el-drawer>
<!-- 成品质检入库弹窗 -->
<el-dialog v-model="inboundDialogVisible" title="生产入库" width="520px" append-to-body>
<el-form label-width="120px">
<el-form-item label="选择仓库">
<el-select
v-model="selectedWarehouseId"
placeholder="请选择或输入仓库编号"
filterable
allow-create
default-first-option
clearable
class="!w-320px"
>
<el-option
v-for="w in warehouseOptions"
:key="w.id"
:label="w.name + '' + w.id + ''"
:value="w.id"
/>
</el-select>
</el-form-item>
<el-form-item label="产品条码">
<span>{{
(currentInboundRow &&
(currentInboundRow.productCode ||
currentInboundRow.productBarCode ||
currentInboundRow.barCode)) ||
'-'
}}</span>
</el-form-item>
<el-form-item label="入库重量(kg)">
<span>{{ (currentInboundRow && currentInboundRow.weight) || 0 }}</span>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="inboundDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="inboundSubmitting" @click="confirmInbound"
>确定</el-button
>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import QualityDialog from './components/QualityDialog.vue'
import {
getFinishedProductQualityPage,
deleteFinishedProductQuality,
approveFinishedProductQuality
} from '@/api/mes/quality/productquality/index'
import { YVHgetWorkOrder, YVHgetWorkOrderSimpleList } from '@/api/mes/production/workorder'
import { WarehouseApi } from '@/api/erp/stock/warehouse'
import request from '@/config/axios'
// 查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
// 搜索表单
const searchForm = reactive({
productName: '',
workOrderCode: '',
status: undefined,
qualified: undefined,
inspectionTime: [] as string[]
})
// 弹窗控制
const dialog = reactive({
visible: false,
title: '',
type: ''
})
// 当前选中的行数据
const currentRow = ref<any>({})
// 列表数据
const list = ref<any[]>([])
const loading = ref(false)
const total = ref(0)
// 详情抽屉状态
const detail = reactive<any>({
visible: false,
row: null,
loadingWorkOrder: false,
workOrder: null
})
const isTomato = (code?: string) => {
const c = (code || '').toUpperCase()
return c.startsWith('X8')
}
const isStevia = (code?: string) => {
const c = (code || '').toUpperCase()
return c.startsWith('X') && !c.startsWith('X8')
}
// 工具:时间与百分比格式化
const pad2 = (n: number) => String(n).padStart(2, '0')
const formatDateTime = (v: any) => {
if (v == null || v === '') return '-'
try {
let d: Date
if (typeof v === 'number') {
// 兼容秒/毫秒
d = new Date(v > 1e12 ? v : v * 1000)
} else if (typeof v === 'string') {
d = new Date(v)
} else if (v instanceof Date) {
d = v
} else {
return String(v)
}
const Y = d.getFullYear()
const M = pad2(d.getMonth() + 1)
const D = pad2(d.getDate())
const h = pad2(d.getHours())
const m = pad2(d.getMinutes())
const s = pad2(d.getSeconds())
return `${Y}-${M}-${D} ${h}:${m}:${s}`
} catch {
return String(v)
}
}
const formatPercent = (num?: number, denom?: number) => {
if (!denom || denom <= 0 || num == null) return '-'
return `${((num / denom) * 100).toFixed(2)}%`
}
const workOrderStatusText = (s?: number) => {
switch (s) {
case 7:
return '已入库'
case 4:
return '已完成'
case 3:
return '生产中'
case 2:
return '已审核'
case 1:
return '待审核'
case 0:
return '草稿'
default:
return '已入库'
}
}
// 入库弹窗状态
const inboundDialogVisible = ref(false)
const inboundSubmitting = ref(false)
const warehouseOptions = ref<Array<{ id: number; name: string }>>([])
const selectedWarehouseId = ref<string | number | undefined>(undefined)
const currentInboundRow = ref<any>(null)
// 不再使用工单ID入库改为传质检单ID
// 打开入库
const handleOpenInbound = async (row: any) => {
currentInboundRow.value = row
selectedWarehouseId.value = undefined
try {
// 加载仓库选项
const list = await WarehouseApi.getWarehouseSimpleList()
warehouseOptions.value = Array.isArray(list) ? list : list?.data || []
if (warehouseOptions.value.length > 0) {
selectedWarehouseId.value = warehouseOptions.value[0].id
}
} catch (error) {
console.error('加载仓库列表失败:', error)
}
inboundDialogVisible.value = true
}
// 确认入库
const confirmInbound = async () => {
if (!currentInboundRow.value) return
const chosenId = selectedWarehouseId.value
const warehouseId = Number(chosenId)
if (!Number.isInteger(warehouseId) || warehouseId <= 0) {
ElMessage.error('请选择或输入合法的仓库编号')
return
}
const barCode =
currentInboundRow.value.productCode ||
currentInboundRow.value.productBarCode ||
currentInboundRow.value.barCode
if (!barCode) {
ElMessage.error('缺少产品条码,无法入库')
return
}
const count = Number(currentInboundRow.value.weight) || 0
if (!count || count <= 0) {
ElMessage.error('合格重量为 0无法入库')
return
}
// 确保有工单ID
let workOrderId = currentInboundRow.value?.workOrderId
if (!workOrderId && currentInboundRow.value?.workOrderCode) {
try {
const list: any = await YVHgetWorkOrderSimpleList({
code: currentInboundRow.value.workOrderCode
} as any)
const target = Array.isArray(list)
? list.find((it: any) => it.code === currentInboundRow.value.workOrderCode)
: null
workOrderId = target?.id
} catch (e) {}
}
inboundSubmitting.value = true
try {
await request.post({
url: '/mes/stock/inbound',
data: {
barCode,
warehouseId,
count,
finishedProductQualityId: currentInboundRow.value.id
}
})
ElMessage.success('入库成功')
inboundDialogVisible.value = false
} catch (error) {
console.error('入库失败:', error)
ElMessage.error('入库失败')
} finally {
inboundSubmitting.value = false
}
}
// 获取列表数据
const getList = async () => {
loading.value = true
try {
// 构建查询参数
const params: any = {
...queryParams,
productName: searchForm.productName || undefined,
workOrderCode: searchForm.workOrderCode || undefined,
status: searchForm.status,
qualified:
searchForm.qualified === true
? 1
: searchForm.qualified === false
? 0
: searchForm.qualified
}
// 处理日期范围
if (searchForm.inspectionTime && searchForm.inspectionTime.length === 2) {
params.inspectionTime = [
searchForm.inspectionTime[0] + ' 00:00:00',
searchForm.inspectionTime[1] + ' 23:59:59'
]
}
const res = await getFinishedProductQualityPage(params)
if (res.list && Array.isArray(res.list)) {
list.value = res.list
total.value = res.total || 0
} else if (res.code === 0 && res.data) {
// 处理标准响应格式
list.value = res.data.list || []
total.value = res.data.total || 0
} else {
ElMessage.error('获取列表失败')
list.value = []
total.value = 0
}
} catch (error) {
console.error('获取成品质检列表失败', error)
list.value = []
total.value = 0
} finally {
loading.value = false
}
}
// 查询操作
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
// 重置查询
const resetQuery = () => {
searchForm.productName = ''
searchForm.workOrderCode = ''
searchForm.status = undefined
searchForm.qualified = undefined
searchForm.inspectionTime = []
handleQuery()
}
// 打开弹窗
const openDialog = (type: string, row?: any) => {
dialog.type = type
dialog.title =
{
create: '新增报工与质检',
edit: '编辑报工与质检',
view: '查看报工与质检'
}[type] || ''
dialog.visible = true
currentRow.value = row || {}
}
// 打开详情抽屉
const openDetail = async (row: any) => {
detail.visible = true
detail.row = row
// 加载工单基础信息
detail.loadingWorkOrder = true
try {
let workOrderId = row?.workOrderId
if (!workOrderId && row?.workOrderCode) {
// 通过 code 查找 id兜底方案
const list = await YVHgetWorkOrderSimpleList({ code: row.workOrderCode } as any)
const target = Array.isArray(list)
? list.find((it: any) => it.code === row.workOrderCode)
: null
workOrderId = target?.id
}
if (workOrderId) {
const res = await YVHgetWorkOrder(workOrderId)
detail.workOrder = res
} else {
detail.workOrder = null
}
} catch (e) {
console.error('加载工单详情失败', e)
detail.workOrder = null
} finally {
detail.loadingWorkOrder = false
}
}
// 处理弹窗提交
const handleDialogSubmit = () => {
// 直接刷新列表,不显示提示信息
getList()
}
// 审核记录
const handleApprove = (row: any) => {
ElMessageBox.confirm('确认要审核该成品质检记录吗?审核后状态将变更为已完成。', '审核确认', {
type: 'warning'
})
.then(async () => {
try {
await approveFinishedProductQuality(row.id)
ElMessage.success('审核成功')
getList()
} catch (error: any) {
console.error('审核失败:', error)
ElMessage.error(error.message || '审核失败')
}
})
.catch(() => {})
}
// 删除记录
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认要删除该成品质检记录吗?', '警告', {
type: 'warning'
})
.then(async () => {
try {
const res = await deleteFinishedProductQuality(row.id)
if (res) {
ElMessage.success('删除成功')
getList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error: any) {
ElMessage.error(error.message || '删除失败')
}
})
.catch(() => {})
}
// 处理行点击事件
const handleRowClick = (row: any) => {
// 状态为待检(0)或检验中(1)时,打开编辑页面
if (row.status === 0 || row.status === 1) {
openDialog('edit', row)
}
// 状态为已完成(2)或已入库(3)时,打开详情页面
else if (row.status === 2 || row.status === 3) {
openDetail(row)
}
}
// 导出数据
const handleExport = () => {
ElMessage.success('导出成功')
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,103 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="等级名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入质检等级名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getQualityGrade, createQualityGrade, updateQualityGrade, type QualityGradeVO } from '@/api/mes/quality/quality-grade'
/** 质检等级 表单 */
defineOptions({ name: 'QualityGradeForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
name: '',
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '质检等级名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'create' ? '添加质检等级' : '修改质检等级'
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await getQualityGrade(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as QualityGradeVO
if (formType.value === 'create') {
await createQualityGrade(data)
message.success(t('common.createSuccess'))
} else {
await updateQualityGrade(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
remark: ''
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,198 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="等级编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入质检等级编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="等级名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入质检等级名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:quality-grade:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:quality-grade:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@row-click="handleRowClick"
style="cursor: pointer"
>
<el-table-column label="编号" align="center" prop="code" min-width="120" />
<el-table-column label="名称" align="center" prop="name" min-width="120" />
<el-table-column label="备注" align="center" prop="remark" min-width="200" show-overflow-tooltip />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:quality-grade:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:quality-grade:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<QualityGradeForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { getQualityGradePage, deleteQualityGrade, exportQualityGrade } from '@/api/mes/quality/quality-grade'
import QualityGradeForm from './QualityGradeForm.vue'
/** 质检等级 列表 */
defineOptions({ name: 'MesQualityGrade' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: '',
name: ''
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await getQualityGradePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await deleteQualityGrade(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await exportQualityGrade(queryParams)
download.excel(data, '质检等级.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 行点击事件 */
const handleRowClick = (row: any) => {
openForm('update', row.id)
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,585 @@
<template>
<ContentWrap>
<!-- 搜索表单 -->
<el-form
ref="searchFormRef"
:model="searchForm"
:inline="true"
label-width="80px"
class="-mb-15px"
>
<el-form-item label="产品类型" prop="productType">
<el-select
v-model="searchForm.productType"
placeholder="请选择产品类型"
clearable
class="!w-240px"
>
<el-option label="甜菊糖" value="stevia" />
<el-option label="番茄酱" value="tomato" />
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="timeRange">
<el-date-picker
v-model="searchForm.timeRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
class="!w-420px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery" type="primary">
<Icon icon="ep:search" class="mr-[5px]" /> 查询
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-[5px]" /> 重置
</el-button>
<el-button @click="handleExport" type="success" plain>
<Icon icon="ep:download" class="mr-[5px]" /> 导出图表
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 趋势图表 -->
<ContentWrap>
<div class="trend-chart-container">
<div class="chart-header">
<h3>质检指标趋势图</h3>
<div class="chart-controls">
<el-button-group>
<el-button
:type="chartType === 'line' ? 'primary' : ''"
@click="chartType = 'line'"
>
折线图
</el-button>
<el-button
:type="chartType === 'area' ? 'primary' : ''"
@click="chartType = 'area'"
>
面积图
</el-button>
</el-button-group>
</div>
</div>
<!-- 图表容器 -->
<div
ref="chartRef"
class="chart-container"
v-loading="chartLoading"
></div>
<!-- 指标选择器 -->
<div class="indicator-selector">
<div class="selector-title">显示指标</div>
<el-checkbox-group v-model="selectedIndicators" @change="updateChart">
<el-checkbox
v-for="indicator in availableIndicators"
:key="indicator.key"
:label="indicator.key"
:style="{ color: indicator.color }"
>
{{ indicator.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</ContentWrap>
<!-- 数据统计表格 -->
<ContentWrap>
<div class="statistics-container">
<h4>数据统计</h4>
<el-table :data="statisticsData" border stripe>
<el-table-column prop="indicator" label="指标" width="120" />
<el-table-column prop="average" label="平均值" width="100" />
<el-table-column prop="max" label="最大值" width="100" />
<el-table-column prop="min" label="最小值" width="100" />
<el-table-column prop="latest" label="最新值" width="100" />
<el-table-column prop="trend" label="趋势" width="80">
<template #default="{ row }">
<el-tag
:type="row.trend === 'up' ? 'danger' : row.trend === 'down' ? 'success' : 'info'"
>
{{ row.trend === 'up' ? '上升' : row.trend === 'down' ? '下降' : '平稳' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag
:type="row.status === 'normal' ? 'success' : row.status === 'warning' ? 'warning' : 'danger'"
>
{{ row.status === 'normal' ? '正常' : row.status === 'warning' ? '预警' : '异常' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import { getFinishedProductQualityPage } from '@/api/mes/quality/productquality/index'
defineOptions({ name: 'QualityTrend' })
// 搜索表单
const searchForm = reactive({
productType: 'stevia', // 默认甜菊糖
timeRange: [] as string[]
})
// 图表相关
const chartRef = ref<HTMLDivElement>()
const chartInstance = ref<echarts.ECharts>()
const chartLoading = ref(false)
const chartType = ref<'line' | 'area'>('line')
// 可用指标配置
const availableIndicators = ref([
{ key: 'ph', name: 'PH值', color: '#5470c6', unit: '' },
{ key: 'rd', name: 'RD', color: '#91cc75', unit: '%' },
{ key: 'ra', name: 'RA', color: '#fac858', unit: '%' },
{ key: 'stv', name: 'STV', color: '#ee6666', unit: '%' },
{ key: 'rf', name: 'RF', color: '#73c0de', unit: '%' },
{ key: 'rc', name: 'RC', color: '#3ba272', unit: '%' },
{ key: 'da', name: 'DA', color: '#fc8452', unit: '%' },
{ key: 'rv', name: 'RV', color: '#9a60b4', unit: '%' },
{ key: 'rb', name: 'RB', color: '#ea7ccc', unit: '%' },
{ key: 'sb', name: 'SB', color: '#5470c6', unit: '%' },
{ key: 'moisture', name: '水分', color: '#91cc75', unit: '%' }
])
// 选中的指标
const selectedIndicators = ref(['ph', 'rd', 'ra', 'stv', 'moisture'])
// 图表数据
const chartData = ref<any>({
dates: [],
series: {}
})
// 统计数据
const statisticsData = ref<any[]>([])
// 初始化图表
const initChart = () => {
if (!chartRef.value) return
chartInstance.value = echarts.init(chartRef.value)
// 设置图表配置
updateChart()
// 监听窗口大小变化
window.addEventListener('resize', () => {
chartInstance.value?.resize()
})
}
// 更新图表
const updateChart = () => {
if (!chartInstance.value) return
const option = {
title: {
text: '质检指标趋势分析',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params: any) => {
let result = `<div style="margin-bottom: 5px">${params[0].axisValue}</div>`
params.forEach((item: any) => {
const indicator = availableIndicators.value.find(ind => ind.key === item.seriesName)
result += `
<div style="display: flex; align-items: center; margin-bottom: 3px">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${item.color}; border-radius: 50%; margin-right: 8px"></span>
<span style="margin-right: 10px">${indicator?.name || item.seriesName}:</span>
<span style="font-weight: bold">${item.value}${indicator?.unit || ''}</span>
</div>
`
})
return result
}
},
legend: {
data: selectedIndicators.value.map(key => {
const indicator = availableIndicators.value.find(ind => ind.key === key)
return indicator?.name || key
}),
top: 30,
type: 'scroll'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {
title: '保存为图片'
},
dataZoom: {
title: {
zoom: '区域缩放',
back: '区域缩放还原'
}
},
restore: {
title: '还原'
}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: chartData.value.dates,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}'
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 100,
height: 30
}
],
series: selectedIndicators.value.map(key => {
const indicator = availableIndicators.value.find(ind => ind.key === key)
return {
name: key,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2
},
areaStyle: chartType.value === 'area' ? {
opacity: 0.3
} : undefined,
data: chartData.value.series[key] || [],
itemStyle: {
color: indicator?.color || '#5470c6'
}
}
})
}
chartInstance.value.setOption(option, true)
}
// 获取质检数据
const getQualityData = async () => {
chartLoading.value = true
try {
const params: any = {
pageNo: 1,
pageSize: 100, // 获取更多数据用于趋势分析
status: 3 // 只获取已入库的质检数据
}
// 添加时间范围过滤
if (searchForm.timeRange && searchForm.timeRange.length === 2) {
params.inspectionTime = [
searchForm.timeRange[0] + ' 00:00:00',
searchForm.timeRange[1] + ' 23:59:59'
]
}
const res = await getFinishedProductQualityPage(params)
const list = res.list || res.data?.list || []
// 根据产品类型过滤数据
const filteredList = list.filter((item: any) => {
if (searchForm.productType === 'stevia') {
return item.workOrderCode && item.workOrderCode.toUpperCase().startsWith('X') &&
!item.workOrderCode.toUpperCase().startsWith('X8')
} else if (searchForm.productType === 'tomato') {
return item.workOrderCode && item.workOrderCode.toUpperCase().startsWith('X8')
}
return true
})
// 处理数据
processChartData(filteredList)
calculateStatistics(filteredList)
} catch (error) {
console.error('获取质检数据失败:', error)
ElMessage.error('获取质检数据失败')
} finally {
chartLoading.value = false
}
}
// 处理图表数据
const processChartData = (list: any[]) => {
// 按时间排序
const sortedList = list
.filter(item => item.inspectionTime)
.sort((a, b) => new Date(a.inspectionTime).getTime() - new Date(b.inspectionTime).getTime())
const dates: string[] = []
const series: any = {}
// 初始化系列数据
availableIndicators.value.forEach(indicator => {
series[indicator.key] = []
})
sortedList.forEach(item => {
const date = item.inspectionTime.split(' ')[0] // 只取日期部分
dates.push(date)
// 根据产品类型提取对应的指标数据
if (searchForm.productType === 'stevia') {
series.ph.push(item.steviaPhValue || null)
series.rd.push(item.steviaRd || null)
series.ra.push(item.steviaRa || null)
series.stv.push(item.steviaStv || null)
series.rf.push(item.steviaRf || null)
series.rc.push(item.steviaRc || null)
series.da.push(item.steviaDa || null)
series.rv.push(item.steviaRv || null)
series.rb.push(item.steviaRb || null)
series.sb.push(item.steviaSb || null)
series.moisture.push(item.steviaMoisture || null)
} else if (searchForm.productType === 'tomato') {
series.ph.push(item.tomatoPhValue || null)
// 番茄产品的其他指标可能不同,这里用默认值
series.rd.push(null)
series.ra.push(null)
series.stv.push(null)
series.rf.push(null)
series.rc.push(null)
series.da.push(null)
series.rv.push(null)
series.rb.push(null)
series.sb.push(null)
series.moisture.push(null)
}
})
chartData.value = { dates, series }
// 更新图表
nextTick(() => {
updateChart()
})
}
// 计算统计数据
const calculateStatistics = (_list: any[]) => {
const stats: any[] = []
selectedIndicators.value.forEach(key => {
const indicator = availableIndicators.value.find(ind => ind.key === key)
if (!indicator) return
const values = chartData.value.series[key].filter((v: any) => v !== null && v !== undefined)
if (values.length === 0) {
stats.push({
indicator: indicator.name,
average: '-',
max: '-',
min: '-',
latest: '-',
trend: 'stable',
status: 'normal'
})
return
}
const average = (values.reduce((sum: number, val: number) => sum + val, 0) / values.length).toFixed(2)
const max = Math.max(...values).toFixed(2)
const min = Math.min(...values).toFixed(2)
const latest = values[values.length - 1]?.toFixed(2) || '-'
// 计算趋势(简单的前后对比)
let trend = 'stable'
if (values.length >= 2) {
const recent = values.slice(-3).reduce((sum: number, val: number) => sum + val, 0) / Math.min(3, values.length)
const earlier = values.slice(0, Math.max(1, values.length - 3)).reduce((sum: number, val: number) => sum + val, 0) / Math.max(1, values.length - 3)
if (recent > earlier * 1.05) trend = 'up'
else if (recent < earlier * 0.95) trend = 'down'
}
// 状态判断(这里可以根据实际业务规则调整)
let status = 'normal'
const latestValue = parseFloat(latest)
if (key === 'ph') {
if (latestValue < 6.0 || latestValue > 8.0) status = 'warning'
if (latestValue < 5.5 || latestValue > 8.5) status = 'danger'
} else if (key === 'moisture') {
if (latestValue > 10) status = 'warning'
if (latestValue > 15) status = 'danger'
}
stats.push({
indicator: indicator.name,
average,
max,
min,
latest,
trend,
status
})
})
statisticsData.value = stats
}
// 查询操作
const handleQuery = () => {
getQualityData()
}
// 重置查询
const resetQuery = () => {
searchForm.productType = 'stevia'
searchForm.timeRange = []
getQualityData()
}
// 导出图表
const handleExport = () => {
if (chartInstance.value) {
const url = chartInstance.value.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff'
})
const link = document.createElement('a')
link.download = `质检趋势图_${new Date().toISOString().split('T')[0]}.png`
link.href = url
link.click()
ElMessage.success('图表导出成功')
}
}
// 监听图表类型变化
watch(chartType, () => {
updateChart()
})
// 监听产品类型变化
watch(() => searchForm.productType, () => {
// 根据产品类型调整可用指标
if (searchForm.productType === 'tomato') {
selectedIndicators.value = ['ph'] // 番茄产品主要关注PH值
} else {
selectedIndicators.value = ['ph', 'rd', 'ra', 'stv', 'moisture'] // 甜菊糖产品的主要指标
}
getQualityData()
})
onMounted(() => {
nextTick(() => {
initChart()
getQualityData()
})
})
</script>
<style scoped>
.trend-chart-container {
padding: 20px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-header h3 {
margin: 0;
color: #303133;
}
.chart-container {
width: 100%;
height: 500px;
margin-bottom: 20px;
}
.indicator-selector {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 6px;
}
.selector-title {
font-weight: bold;
color: #606266;
margin-right: 15px;
}
.statistics-container {
padding: 20px;
}
.statistics-container h4 {
margin: 0 0 15px 0;
color: #303133;
}
:deep(.el-checkbox) {
margin-right: 20px;
margin-bottom: 10px;
}
:deep(.el-checkbox__label) {
font-weight: 500;
}
/* 保持 checkbox 选中后文字颜色不变 */
:deep(.el-checkbox.is-checked .el-checkbox__label) {
color: inherit !important;
}
</style>