first commit
This commit is contained in:
908
src/views/mes/collect/fullscreen.vue
Normal file
908
src/views/mes/collect/fullscreen.vue
Normal 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">m³/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">m³/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>
|
||||
1189
src/views/mes/collect/index.vue
Normal file
1189
src/views/mes/collect/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1073
src/views/mes/collect/tomato/index.vue
Normal file
1073
src/views/mes/collect/tomato/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
148
src/views/mes/energy/EnergyConsumptionAudit.vue
Normal file
148
src/views/mes/energy/EnergyConsumptionAudit.vue
Normal 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>
|
||||
97
src/views/mes/energy/EnergyConsumptionDetail.vue
Normal file
97
src/views/mes/energy/EnergyConsumptionDetail.vue
Normal 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>
|
||||
293
src/views/mes/energy/EnergyConsumptionForm.vue
Normal file
293
src/views/mes/energy/EnergyConsumptionForm.vue
Normal 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>
|
||||
247
src/views/mes/energy/index.vue
Normal file
247
src/views/mes/energy/index.vue
Normal 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>
|
||||
803
src/views/mes/home/index.vue
Normal file
803
src/views/mes/home/index.vue
Normal 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>
|
||||
1060
src/views/mes/home/index_1.vue
Normal file
1060
src/views/mes/home/index_1.vue
Normal file
File diff suppressed because it is too large
Load Diff
693
src/views/mes/home/index_2.vue
Normal file
693
src/views/mes/home/index_2.vue
Normal 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>
|
||||
246
src/views/mes/humidity/index.vue
Normal file
246
src/views/mes/humidity/index.vue
Normal 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>
|
||||
376
src/views/mes/product/bom/ProductBomForm.vue
Normal file
376
src/views/mes/product/bom/ProductBomForm.vue
Normal 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>
|
||||
197
src/views/mes/product/bom/index.vue
Normal file
197
src/views/mes/product/bom/index.vue
Normal 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>
|
||||
487
src/views/mes/production/BOM/index.vue
Normal file
487
src/views/mes/production/BOM/index.vue
Normal 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
@@ -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>
|
||||
357
src/views/mes/production/material-requisition/index.vue
Normal file
357
src/views/mes/production/material-requisition/index.vue
Normal 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>
|
||||
417
src/views/mes/production/plan/detail/index.vue
Normal file
417
src/views/mes/production/plan/detail/index.vue
Normal 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>
|
||||
150
src/views/mes/production/plan/index.vue
Normal file
150
src/views/mes/production/plan/index.vue
Normal 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>
|
||||
140
src/views/mes/production/plan/list/GenerateMonthlyPlanDialog.vue
Normal file
140
src/views/mes/production/plan/list/GenerateMonthlyPlanDialog.vue
Normal 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>
|
||||
173
src/views/mes/production/plan/list/GenerateOrderDialog.vue
Normal file
173
src/views/mes/production/plan/list/GenerateOrderDialog.vue
Normal 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>
|
||||
356
src/views/mes/production/plan/list/PlanAnalysisDialog.vue
Normal file
356
src/views/mes/production/plan/list/PlanAnalysisDialog.vue
Normal 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>
|
||||
407
src/views/mes/production/plan/list/ProductionPlanForm.vue
Normal file
407
src/views/mes/production/plan/list/ProductionPlanForm.vue
Normal 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>
|
||||
531
src/views/mes/production/plan/list/index.vue
Normal file
531
src/views/mes/production/plan/list/index.vue
Normal 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>
|
||||
839
src/views/mes/production/process-record/index.vue
Normal file
839
src/views/mes/production/process-record/index.vue
Normal 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>
|
||||
201
src/views/mes/production/process-record/readme.txt
Normal file
201
src/views/mes/production/process-record/readme.txt
Normal 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. 编辑权限注意事项:
|
||||
- 关键字段建议设置为不可编辑
|
||||
- 必填字段要有明确的提示
|
||||
- 考虑字段间的依赖关系
|
||||
431
src/views/mes/production/process-record/数据驱动表单说明.md
Normal file
431
src/views/mes/production/process-record/数据驱动表单说明.md
Normal 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 个以内
|
||||
- 必填字段不要太多,影响操作效率
|
||||
135
src/views/mes/production/process/ProcessOperationForm.vue
Normal file
135
src/views/mes/production/process/ProcessOperationForm.vue
Normal 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>
|
||||
211
src/views/mes/production/process/index.vue
Normal file
211
src/views/mes/production/process/index.vue
Normal 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>
|
||||
302
src/views/mes/production/route/ProcessRouteForm.vue
Normal file
302
src/views/mes/production/route/ProcessRouteForm.vue
Normal 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>
|
||||
206
src/views/mes/production/route/index.vue
Normal file
206
src/views/mes/production/route/index.vue
Normal 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>
|
||||
409
src/views/mes/production/workorder/WorkOrderForm.vue
Normal file
409
src/views/mes/production/workorder/WorkOrderForm.vue
Normal 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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
700
src/views/mes/production/workorder/index.vue
Normal file
700
src/views/mes/production/workorder/index.vue
Normal 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']"-->
|
||||
<!-- >-->
|
||||
<!--<!– <Icon icon="ep:close" />–>-->
|
||||
<!-- 取消-->
|
||||
<!-- </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']"-->
|
||||
<!-- >-->
|
||||
<!--<!– <Icon icon="ep:circle-check" />–>-->
|
||||
<!-- 审核-->
|
||||
<!-- </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']"-->
|
||||
<!-- >-->
|
||||
<!--<!– <Icon icon="ep:circle-close" />–>-->
|
||||
<!-- 拒绝-->
|
||||
<!-- </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>
|
||||
185
src/views/mes/production/workstation/WorkstationBindingForm.vue
Normal file
185
src/views/mes/production/workstation/WorkstationBindingForm.vue
Normal 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>
|
||||
210
src/views/mes/production/workstation/index.vue
Normal file
210
src/views/mes/production/workstation/index.vue
Normal 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>
|
||||
497
src/views/mes/quality/processquality/ProcessQualityForm.vue
Normal file
497
src/views/mes/quality/processquality/ProcessQualityForm.vue
Normal 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>
|
||||
211
src/views/mes/quality/processquality/index.vue
Normal file
211
src/views/mes/quality/processquality/index.vue
Normal 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>
|
||||
|
||||
|
||||
1246
src/views/mes/quality/productquality/components/QualityDialog.vue
Normal file
1246
src/views/mes/quality/productquality/components/QualityDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
898
src/views/mes/quality/productquality/index.vue
Normal file
898
src/views/mes/quality/productquality/index.vue
Normal 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>
|
||||
103
src/views/mes/quality/qualitygrade/QualityGradeForm.vue
Normal file
103
src/views/mes/quality/qualitygrade/QualityGradeForm.vue
Normal 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>
|
||||
198
src/views/mes/quality/qualitygrade/index.vue
Normal file
198
src/views/mes/quality/qualitygrade/index.vue
Normal 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>
|
||||
585
src/views/mes/quality/qualitytrend/index.vue
Normal file
585
src/views/mes/quality/qualitytrend/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user