833 lines
27 KiB
Vue
833 lines
27 KiB
Vue
<template>
|
|
<div class="production-page app-container" v-loading="loading">
|
|
<el-card shadow="never" class="page-head">
|
|
<div class="page-head__row">
|
|
<div class="page-title">
|
|
<div class="page-title__main">生产任务</div>
|
|
<div class="page-title__sub">任务清单 / 明细 / 回执</div>
|
|
</div>
|
|
<div class="page-actions">
|
|
<el-button type="primary" @click="openAdd">新增任务</el-button>
|
|
<el-button @click="loadTasks">刷新</el-button>
|
|
</div>
|
|
</div>
|
|
<div class="page-head__row page-head__row--date">
|
|
<div class="date-bar">
|
|
<el-radio-group v-model="datePreset" size="small" @change="applyDatePreset">
|
|
<el-radio-button label="today">今天</el-radio-button>
|
|
<el-radio-button label="yesterday">昨天</el-radio-button>
|
|
<el-radio-button label="last7">近7天</el-radio-button>
|
|
<el-radio-button label="thisMonth">本月</el-radio-button>
|
|
<el-radio-button label="custom">自定义</el-radio-button>
|
|
</el-radio-group>
|
|
<el-popover
|
|
v-if="datePreset === 'custom'"
|
|
v-model:visible="customPopoverOpen"
|
|
placement="bottom-start"
|
|
trigger="click"
|
|
width="360"
|
|
>
|
|
<template #reference>
|
|
<el-button size="small" plain class="custom-range-btn">{{ customRangeLabel }}</el-button>
|
|
</template>
|
|
<el-date-picker
|
|
v-model="customRange"
|
|
type="daterange"
|
|
value-format="YYYY-MM-DD"
|
|
format="YYYY-MM-DD"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
unlink-panels
|
|
teleported
|
|
style="width: 320px;"
|
|
@change="onCustomRangePicked"
|
|
/>
|
|
</el-popover>
|
|
</div>
|
|
</div>
|
|
<div class="page-head__row page-head__row--filters">
|
|
<el-form :inline="true" class="filter-form">
|
|
<el-form-item label="状态">
|
|
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 140px;">
|
|
<el-option label="进行中" value="1" />
|
|
<el-option label="已完成" value="2" />
|
|
<el-option label="已暂停" value="3" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="关键词">
|
|
<el-input v-model="filters.keyword" placeholder="任务名称/编号" clearable style="width: 240px;" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<div class="page-stats">
|
|
<el-tag type="info" effect="light">全部 {{ taskList.length }}</el-tag>
|
|
<el-tag type="success" effect="light" style="margin-left: 8px;">进行中 {{ runningCount }}</el-tag>
|
|
<el-tag type="warning" effect="light" style="margin-left: 8px;">暂停 {{ pausedCount }}</el-tag>
|
|
<el-tag type="danger" effect="light" style="margin-left: 8px;">完成 {{ finishedCount }}</el-tag>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="6">
|
|
<el-card shadow="never" class="left-card">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>任务清单</span>
|
|
<div>
|
|
<el-button type="primary" link @click="openAdd">新增</el-button>
|
|
<el-button type="primary" link @click="loadTasks">刷新</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div class="task-list">
|
|
<el-empty v-if="!filteredTaskList.length" description="暂无任务" />
|
|
<el-scrollbar v-else height="640px">
|
|
<div
|
|
v-for="t in filteredTaskList"
|
|
:key="t.taskId"
|
|
class="task-item"
|
|
:class="{ active: t.taskId === selectedTaskId }"
|
|
@click="selectTask(t.taskId)"
|
|
>
|
|
<div class="task-item__top">
|
|
<div class="task-item__title">{{ t.taskName || t.taskCode || ('任务' + t.taskId) }}</div>
|
|
<el-tag size="small" :type="statusTagType(t.status)" effect="light">{{ statusLabel(t.status) }}</el-tag>
|
|
</div>
|
|
<div class="task-item__sub">
|
|
<span>计划 {{ formatQty(t.planQty) }}</span>
|
|
<span style="margin-left: 10px;">已完 {{ formatQty(t.finishedQty) }}</span>
|
|
<span style="margin-left: 10px;">未完 {{ formatQty(t.unfinishedQty) }}</span>
|
|
</div>
|
|
<div class="task-item__meta">
|
|
<span v-if="t.planStartTime || t.planEndTime">计划 {{ formatTimeRange(t.planStartTime, t.planEndTime) }}</span>
|
|
<span v-if="t.createBy" style="margin-left: 10px;">创建 {{ t.createBy }}</span>
|
|
<span v-if="t.updateTime" style="margin-left: 10px;">更新 {{ formatTime(t.updateTime) }}</span>
|
|
</div>
|
|
<el-progress :percentage="taskProgressPercent(t)" :stroke-width="8" :show-text="false" />
|
|
</div>
|
|
</el-scrollbar>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
|
|
<el-col :span="18">
|
|
<el-card shadow="never" class="mb12">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>任务明细</span>
|
|
<div v-if="selectedTask" class="card-header__right">
|
|
<div class="card-meta">
|
|
<span class="meta-main">{{ selectedTask.taskName || selectedTask.taskCode || ('任务' + selectedTask.taskId) }}</span>
|
|
<el-tag size="small" :type="statusTagType(selectedTask.status)" effect="light" style="margin-left: 8px;">{{ statusLabel(selectedTask.status) }}</el-tag>
|
|
</div>
|
|
<el-button
|
|
v-if="String(selectedTask.status) === '1'"
|
|
size="small"
|
|
type="primary"
|
|
:loading="completeLoading"
|
|
@click="completeSelectedTask"
|
|
>
|
|
完成任务
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<el-empty v-if="!selectedTaskId" description="请选择任务" />
|
|
<el-table v-else :data="productRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
|
|
<el-table-column label="产品" prop="productName" min-width="180" show-overflow-tooltip />
|
|
<el-table-column label="规格" prop="spec" width="140" show-overflow-tooltip />
|
|
<el-table-column label="型号" prop="model" width="140" show-overflow-tooltip />
|
|
<el-table-column label="计划数量" prop="planQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="已完成" prop="finishedQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.finishedQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="不良" prop="badQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.badQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="单位" prop="unit" width="90" />
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>任务回执</span>
|
|
<div v-if="selectedTask" class="card-meta">
|
|
<span class="meta-sub">主材 / 辅材 / 产品概况</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<el-empty v-if="!selectedTaskId" description="请选择任务" />
|
|
<el-tabs v-else type="border-card">
|
|
<el-tab-pane label="主材">
|
|
<el-table :data="mainMaterialRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
|
|
<el-table-column label="名称" prop="materialName" min-width="180" show-overflow-tooltip />
|
|
<el-table-column label="计划" prop="planQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="已用" prop="usedQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.usedQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="单位" prop="unit" width="90" />
|
|
</el-table>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="辅材">
|
|
<el-table :data="auxMaterialRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
|
|
<el-table-column label="名称" prop="materialName" min-width="180" show-overflow-tooltip />
|
|
<el-table-column label="计划" prop="planQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="已用" prop="usedQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.usedQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="单位" prop="unit" width="90" />
|
|
</el-table>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="产品概况">
|
|
<el-table :data="productRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
|
|
<el-table-column label="产品" prop="productName" min-width="180" show-overflow-tooltip />
|
|
<el-table-column label="规格/型号" min-width="200">
|
|
<template #default="scope">
|
|
{{ (scope.row.spec || '-') + ' / ' + (scope.row.model || '-') }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="计划" prop="planQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="已完成" prop="finishedQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.finishedQty) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="不良" prop="badQty" width="120" align="right">
|
|
<template #default="scope">{{ formatQty(scope.row.badQty) }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-dialog v-model="addOpen" title="新增生产任务" width="1100px" top="5vh" append-to-body>
|
|
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
|
|
<el-row :gutter="12">
|
|
<el-col :span="8">
|
|
<el-form-item label="任务编号" prop="taskCode">
|
|
<el-input v-model="addForm.taskCode" placeholder="可不填" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<el-form-item label="任务名称" prop="taskName">
|
|
<el-input v-model="addForm.taskName" placeholder="请输入任务名称" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<el-form-item label="状态" prop="status">
|
|
<el-select v-model="addForm.status" placeholder="请选择">
|
|
<el-option label="进行中" value="1" />
|
|
<el-option label="已完成" value="2" />
|
|
<el-option label="已暂停" value="3" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<el-form-item label="计划开始" prop="planStartTime">
|
|
<el-date-picker v-model="addForm.planStartTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%;" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<el-form-item label="计划结束" prop="planEndTime">
|
|
<el-date-picker v-model="addForm.planEndTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%;" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="24">
|
|
<el-form-item label="备注" prop="remark">
|
|
<el-input v-model="addForm.remark" type="textarea" :rows="2" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
</el-form>
|
|
|
|
<el-divider content-position="left">生产产品明细</el-divider>
|
|
<div style="margin-bottom: 8px;">
|
|
<el-button type="primary" @click="addProductLine">新增产品行</el-button>
|
|
</div>
|
|
<el-table :data="addProducts" border size="small" :header-cell-style="{ background: '#f5f7fa' }">
|
|
<el-table-column label="产品" min-width="220">
|
|
<template #default="scope">
|
|
<el-select v-model="scope.row.productId" filterable clearable placeholder="请选择" style="width: 100%;" @change="onProductPicked(scope.row)">
|
|
<el-option v-for="p in productOptions" :key="p.productId" :label="p.productName" :value="p.productId" />
|
|
</el-select>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="计划数量" width="140" align="right">
|
|
<template #default="scope">
|
|
<el-input-number v-model="scope.row.planQty" :min="0" :precision="4" controls-position="right" style="width: 120px;" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="单位" width="140">
|
|
<template #default="scope">
|
|
<el-input v-model="scope.row.unit" placeholder="单位" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="备注" min-width="200">
|
|
<template #default="scope">
|
|
<el-input v-model="scope.row.remark" placeholder="备注" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="90" align="center">
|
|
<template #default="scope">
|
|
<el-button type="danger" link @click="removeProductLine(scope.$index)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<template #footer>
|
|
<el-button @click="addOpen = false">取消</el-button>
|
|
<el-button type="primary" :loading="addSaving" @click="submitAdd">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup name="Production">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { addProductionTask, completeProductionTask, getProductionTask, listProductionTask } from '@/api/mes/productionTask'
|
|
import { listProductBase } from '@/api/mat/product'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
const loading = ref(false)
|
|
const taskList = ref([])
|
|
const selectedTaskId = ref(null)
|
|
const productRows = ref([])
|
|
const materialRows = ref([])
|
|
const datePreset = ref('today')
|
|
const customRange = ref([])
|
|
const customPopoverOpen = ref(false)
|
|
const filters = ref({
|
|
status: '1',
|
|
keyword: '',
|
|
beginTime: '',
|
|
endTime: ''
|
|
})
|
|
|
|
const addOpen = ref(false)
|
|
const addSaving = ref(false)
|
|
const completeLoading = ref(false)
|
|
const addFormRef = ref()
|
|
const addForm = ref({
|
|
taskCode: '',
|
|
taskName: '',
|
|
status: '1',
|
|
planStartTime: '',
|
|
planEndTime: '',
|
|
remark: ''
|
|
})
|
|
const addRules = {
|
|
taskName: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }]
|
|
}
|
|
|
|
const productOptions = ref([])
|
|
const addProducts = ref([])
|
|
|
|
function toNumber(v) {
|
|
const n = Number(v)
|
|
return Number.isFinite(n) ? n : 0
|
|
}
|
|
|
|
function formatQty(v) {
|
|
return toNumber(v).toFixed(2)
|
|
}
|
|
|
|
function normalizeRole(v) {
|
|
return String(v || '').toLowerCase()
|
|
}
|
|
|
|
function formatTime(v) {
|
|
if (!v) return '-'
|
|
if (v instanceof Date) {
|
|
return formatDateTime(v).slice(0, 16)
|
|
}
|
|
const s = String(v)
|
|
if (s.length >= 16) return s.slice(0, 16)
|
|
return s
|
|
}
|
|
|
|
function formatTimeRange(start, end) {
|
|
const a = start ? formatTime(start) : '-'
|
|
const b = end ? formatTime(end) : '-'
|
|
return `${a} ~ ${b}`
|
|
}
|
|
|
|
function pad2(n) {
|
|
return String(n).padStart(2, '0')
|
|
}
|
|
|
|
function formatDateTime(dt) {
|
|
const d = dt instanceof Date ? dt : new Date(dt)
|
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
|
}
|
|
|
|
function startOfDay(d) {
|
|
const x = new Date(d)
|
|
x.setHours(0, 0, 0, 0)
|
|
return x
|
|
}
|
|
|
|
function endOfDay(d) {
|
|
const x = new Date(d)
|
|
x.setHours(23, 59, 59, 999)
|
|
return x
|
|
}
|
|
|
|
function addDays(d, n) {
|
|
const x = new Date(d)
|
|
x.setDate(x.getDate() + n)
|
|
return x
|
|
}
|
|
|
|
function startOfMonth(d) {
|
|
const x = new Date(d)
|
|
x.setDate(1)
|
|
x.setHours(0, 0, 0, 0)
|
|
return x
|
|
}
|
|
|
|
function applyDatePreset() {
|
|
const now = new Date()
|
|
let begin = null
|
|
let end = null
|
|
const p = String(datePreset.value || '')
|
|
if (p === 'today') {
|
|
begin = startOfDay(now)
|
|
end = endOfDay(now)
|
|
} else if (p === 'yesterday') {
|
|
const y = addDays(now, -1)
|
|
begin = startOfDay(y)
|
|
end = endOfDay(y)
|
|
} else if (p === 'last7') {
|
|
begin = startOfDay(addDays(now, -6))
|
|
end = endOfDay(now)
|
|
} else if (p === 'thisMonth') {
|
|
begin = startOfMonth(now)
|
|
end = endOfDay(now)
|
|
} else if (p === 'custom') {
|
|
if (!customRange.value || customRange.value.length !== 2) {
|
|
const b = startOfDay(now)
|
|
const e = endOfDay(now)
|
|
customRange.value = [formatDateTime(b).slice(0, 10), formatDateTime(e).slice(0, 10)]
|
|
filters.value.beginTime = formatDateTime(b)
|
|
filters.value.endTime = formatDateTime(e)
|
|
loadTasks()
|
|
}
|
|
customPopoverOpen.value = true
|
|
return
|
|
}
|
|
|
|
if (begin && end) {
|
|
filters.value.beginTime = formatDateTime(begin)
|
|
filters.value.endTime = formatDateTime(end)
|
|
loadTasks()
|
|
}
|
|
}
|
|
|
|
function applyCustomRange(v) {
|
|
if (!v || v.length !== 2) return
|
|
datePreset.value = 'custom'
|
|
const begin = startOfDay(new Date(v[0]))
|
|
const end = endOfDay(new Date(v[1]))
|
|
filters.value.beginTime = formatDateTime(begin)
|
|
filters.value.endTime = formatDateTime(end)
|
|
loadTasks()
|
|
}
|
|
|
|
const customRangeLabel = computed(() => {
|
|
const v = customRange.value
|
|
if (Array.isArray(v) && v.length === 2 && v[0] && v[1]) {
|
|
return `${v[0]} ~ ${v[1]}`
|
|
}
|
|
return '选择日期范围'
|
|
})
|
|
|
|
function onCustomRangePicked(v) {
|
|
applyCustomRange(v)
|
|
customPopoverOpen.value = false
|
|
}
|
|
|
|
function statusLabel(v) {
|
|
const s = String(v || '')
|
|
if (s === '1') return '进行中'
|
|
if (s === '2') return '已完成'
|
|
if (s === '3') return '已暂停'
|
|
return s || '-'
|
|
}
|
|
|
|
function statusTagType(v) {
|
|
const s = String(v || '')
|
|
if (s === '1') return 'success'
|
|
if (s === '2') return 'danger'
|
|
if (s === '3') return 'warning'
|
|
return 'info'
|
|
}
|
|
|
|
function taskProgressPercent(t) {
|
|
const plan = toNumber(t && t.planQty)
|
|
const finished = toNumber(t && t.finishedQty)
|
|
if (plan <= 0) return 0
|
|
const p = Math.round((finished / plan) * 100)
|
|
return Math.max(0, Math.min(100, p))
|
|
}
|
|
|
|
function selectTask(taskId) {
|
|
selectedTaskId.value = taskId
|
|
loading.value = true
|
|
return getProductionTask(taskId)
|
|
.then((res) => {
|
|
const data = res && res.data ? res.data : {}
|
|
productRows.value = Array.isArray(data.products) ? data.products : []
|
|
materialRows.value = Array.isArray(data.materials) ? data.materials : []
|
|
})
|
|
.finally(() => {
|
|
loading.value = false
|
|
})
|
|
}
|
|
|
|
const mainMaterialRows = computed(() => (materialRows.value || []).filter((r) => normalizeRole(r && r.materialRole) === 'main'))
|
|
const auxMaterialRows = computed(() => (materialRows.value || []).filter((r) => normalizeRole(r && r.materialRole) === 'aux'))
|
|
|
|
const selectedTask = computed(() => (taskList.value || []).find((t) => String(t.taskId) === String(selectedTaskId.value)) || null)
|
|
const runningCount = computed(() => (taskList.value || []).filter((t) => String(t && t.status) === '1').length)
|
|
const finishedCount = computed(() => (taskList.value || []).filter((t) => String(t && t.status) === '2').length)
|
|
const pausedCount = computed(() => (taskList.value || []).filter((t) => String(t && t.status) === '3').length)
|
|
|
|
const filteredTaskList = computed(() => {
|
|
const list = Array.isArray(taskList.value) ? taskList.value : []
|
|
const st = String(filters.value && filters.value.status ? filters.value.status : '')
|
|
const kw = String(filters.value && filters.value.keyword ? filters.value.keyword : '').trim().toLowerCase()
|
|
return list.filter((t) => {
|
|
if (st && String(t && t.status) !== st) return false
|
|
if (kw) {
|
|
const a = String(t && t.taskName ? t.taskName : '').toLowerCase()
|
|
const b = String(t && t.taskCode ? t.taskCode : '').toLowerCase()
|
|
if (!a.includes(kw) && !b.includes(kw)) return false
|
|
}
|
|
return true
|
|
})
|
|
})
|
|
|
|
function loadTasks() {
|
|
loading.value = true
|
|
const q = {
|
|
pageNum: 1,
|
|
pageSize: 9999,
|
|
status: filters.value && filters.value.status ? filters.value.status : undefined,
|
|
beginTime: filters.value && filters.value.beginTime ? filters.value.beginTime : undefined,
|
|
endTime: filters.value && filters.value.endTime ? filters.value.endTime : undefined
|
|
}
|
|
return listProductionTask(q)
|
|
.then((res) => {
|
|
const rows = (res && res.rows) ? res.rows : []
|
|
taskList.value = rows
|
|
if (!selectedTaskId.value && taskList.value.length) {
|
|
const first = filteredTaskList.value && filteredTaskList.value.length ? filteredTaskList.value[0] : taskList.value[0]
|
|
return selectTask(first.taskId)
|
|
}
|
|
if (selectedTaskId.value) {
|
|
const hit = (taskList.value || []).some((t) => t.taskId === selectedTaskId.value)
|
|
if (hit) return selectTask(selectedTaskId.value)
|
|
}
|
|
})
|
|
.finally(() => {
|
|
loading.value = false
|
|
})
|
|
}
|
|
|
|
function openAdd() {
|
|
addOpen.value = true
|
|
addForm.value = {
|
|
taskCode: '',
|
|
taskName: '',
|
|
status: '1',
|
|
planStartTime: '',
|
|
planEndTime: '',
|
|
remark: ''
|
|
}
|
|
addProducts.value = []
|
|
addProductLine()
|
|
ensureProductOptions()
|
|
}
|
|
|
|
function ensureProductOptions() {
|
|
if (productOptions.value && productOptions.value.length) return
|
|
listProductBase({ pageNum: 1, pageSize: 1000 }).then((res) => {
|
|
productOptions.value = (res && res.rows) ? res.rows : []
|
|
})
|
|
}
|
|
|
|
function addProductLine() {
|
|
addProducts.value.push({ productId: null, planQty: 0, unit: '', remark: '' })
|
|
}
|
|
|
|
function removeProductLine(idx) {
|
|
addProducts.value.splice(idx, 1)
|
|
}
|
|
|
|
function onProductPicked(row) {
|
|
if (!row || !row.productId) return
|
|
const hit = (productOptions.value || []).find((p) => String(p.productId) === String(row.productId))
|
|
if (hit && !row.unit) {
|
|
row.unit = ''
|
|
}
|
|
}
|
|
|
|
function mergeProductLines(lines) {
|
|
const list = Array.isArray(lines) ? lines : []
|
|
const map = new Map()
|
|
list.forEach((p) => {
|
|
if (!p || p.productId == null) return
|
|
const key = String(p.productId)
|
|
const planQty = toNumber(p.planQty)
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
productId: p.productId,
|
|
planQty,
|
|
unit: p.unit || '',
|
|
remark: p.remark || ''
|
|
})
|
|
return
|
|
}
|
|
const hit = map.get(key)
|
|
hit.planQty = toNumber(hit.planQty) + planQty
|
|
if (!hit.unit && p.unit) hit.unit = p.unit
|
|
if (!hit.remark && p.remark) hit.remark = p.remark
|
|
})
|
|
return Array.from(map.values())
|
|
}
|
|
|
|
function submitAdd() {
|
|
if (!addFormRef.value) return
|
|
addFormRef.value.validate((valid) => {
|
|
if (!valid) return
|
|
const task = Object.assign({}, addForm.value)
|
|
const products = mergeProductLines(addProducts.value).map((p) => ({
|
|
productId: p.productId,
|
|
planQty: p.planQty || 0,
|
|
finishedQty: 0,
|
|
badQty: 0,
|
|
unit: p.unit || '',
|
|
remark: p.remark || ''
|
|
}))
|
|
|
|
addSaving.value = true
|
|
addProductionTask({ task, products })
|
|
.then((res) => {
|
|
const taskId = res && res.data ? res.data : null
|
|
ElMessage.success('已新增')
|
|
addOpen.value = false
|
|
return loadTasks().then(() => {
|
|
if (taskId != null) {
|
|
selectedTaskId.value = taskId
|
|
return selectTask(taskId)
|
|
}
|
|
})
|
|
})
|
|
.finally(() => {
|
|
addSaving.value = false
|
|
})
|
|
})
|
|
}
|
|
|
|
function completeSelectedTask() {
|
|
const task = selectedTask.value
|
|
const taskId = selectedTaskId.value
|
|
if (!task || taskId == null) return
|
|
completeLoading.value = true
|
|
return ElMessageBox.confirm('完成后将根据产品配方自动生成主材/辅材回执,是否继续?', '确认完成', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
})
|
|
.then(() => completeProductionTask(taskId))
|
|
.then(() => {
|
|
ElMessage.success('已完成')
|
|
return loadTasks().then(() => selectTask(taskId))
|
|
})
|
|
.catch((e) => {
|
|
const s = String(e || '')
|
|
if (s && s !== 'cancel' && s !== 'close') {
|
|
ElMessage.error('操作失败')
|
|
}
|
|
})
|
|
.finally(() => {
|
|
completeLoading.value = false
|
|
})
|
|
}
|
|
|
|
onMounted(() => {
|
|
applyDatePreset()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.production-page {
|
|
padding: 0;
|
|
}
|
|
|
|
.production-page :deep(.el-card) {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.production-page :deep(.el-card__header) {
|
|
background: #fafafa;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.card-header__right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.card-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
min-width: 0;
|
|
}
|
|
|
|
.meta-main {
|
|
max-width: 520px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: #303133;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.meta-sub {
|
|
color: #909399;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.page-head {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.page-head__row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.page-head__row--filters {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.page-head__row--date {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.date-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.custom-range-btn {
|
|
min-width: 180px;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.page-title__main {
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
color: #303133;
|
|
}
|
|
|
|
.page-title__sub {
|
|
margin-top: 2px;
|
|
font-size: 12px;
|
|
color: #909399;
|
|
}
|
|
|
|
.page-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.filter-form :deep(.el-form-item) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.page-stats {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.task-item {
|
|
padding: 10px 10px;
|
|
border: 1px solid var(--el-border-color-light);
|
|
border-radius: 6px;
|
|
margin-bottom: 10px;
|
|
cursor: pointer;
|
|
background: #fff;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.task-item:hover {
|
|
border-color: rgba(64, 158, 255, 0.55);
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.task-item.active {
|
|
border-color: #409eff;
|
|
background: rgba(64, 158, 255, 0.08);
|
|
}
|
|
|
|
.task-item__top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.task-item__title {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #303133;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.task-item__sub {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
color: #909399;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.task-item__meta {
|
|
margin-top: -2px;
|
|
margin-bottom: 8px;
|
|
font-size: 12px;
|
|
color: #c0c4cc;
|
|
}
|
|
|
|
.mb12 {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
</style>
|