完成排产(测试过了)
This commit is contained in:
344
klp-ui/src/api/aps/aps.js
Normal file
344
klp-ui/src/api/aps/aps.js
Normal file
@@ -0,0 +1,344 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// CRM 订单转换为库存订单
|
||||
export function convertFromCrm(data) {
|
||||
return request({
|
||||
url: '/aps/order/convert-from-crm',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 查询 CRM 订单明细
|
||||
export function listCrmOrderItems(query) {
|
||||
return request({
|
||||
url: '/crm/orderItem/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 产品直排转换为库存订单(单产品)
|
||||
export function convertFromProduct(data) {
|
||||
return request({
|
||||
url: '/aps/order/convert-from-product',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 产品直排转换为库存订单(多产品)
|
||||
export function convertFromProducts(data) {
|
||||
return request({
|
||||
url: '/aps/order/convert-from-products',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 创建排产计划
|
||||
export function createPlan(data) {
|
||||
return request({
|
||||
url: '/aps/plan/create',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 自动排程
|
||||
export function autoSchedule(data) {
|
||||
return request({
|
||||
url: '/aps/plan/auto-schedule',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 发布计划
|
||||
export function publishPlan(planId) {
|
||||
return request({
|
||||
url: `/aps/plan/publish/${planId}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 甘特查询
|
||||
export function fetchGantt(params) {
|
||||
return request({
|
||||
url: '/aps/gantt',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 工厂日历聚合(后端计算)
|
||||
export function fetchFactoryCalendar(params) {
|
||||
return request({
|
||||
url: '/aps/factory-calendar',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 单工序重排
|
||||
export function rescheduleOperation(data) {
|
||||
return request({
|
||||
url: '/aps/operation/reschedule',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 创建锁定
|
||||
export function createLock(data) {
|
||||
return request({
|
||||
url: '/aps/lock',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 解除锁定
|
||||
export function releaseLock(lockId) {
|
||||
return request({
|
||||
url: `/aps/lock/release/${lockId}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一排产表(按模板/机组导出与展示)
|
||||
* 说明:
|
||||
* - templateKey: cold_rolling / acid_pickling / slitting / galvanizing
|
||||
* - 后端建议返回:{ header: {...}, rows: [...], summary: {...} }
|
||||
*/
|
||||
export function fetchScheduleSheet(params) {
|
||||
return request({
|
||||
url: '/aps/schedule-sheet',
|
||||
method: 'get',
|
||||
params,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
export function exportScheduleSheet(params) {
|
||||
return request({
|
||||
url: '/aps/schedule-sheet/export',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob',
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
export function saveScheduleSheetSupplement(data) {
|
||||
return request({
|
||||
url: '/aps/schedule-sheet/supplement/save',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 基础数据管理 ==========
|
||||
|
||||
// 工厂日历
|
||||
export function listCalendar(query) {
|
||||
return request({
|
||||
url: '/aps/calendar/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getCalendar(calendarId) {
|
||||
return request({
|
||||
url: '/aps/calendar/' + calendarId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addCalendar(data) {
|
||||
return request({
|
||||
url: '/aps/calendar',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCalendar(data) {
|
||||
return request({
|
||||
url: '/aps/calendar',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function delCalendar(calendarIds) {
|
||||
return request({
|
||||
url: '/aps/calendar/' + calendarIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function exportCalendar(query) {
|
||||
return request({
|
||||
url: '/aps/calendar/export',
|
||||
method: 'post',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 班次模板
|
||||
export function listShiftTemplate(query) {
|
||||
return request({
|
||||
url: '/aps/shift-template/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getShiftTemplate(shiftId) {
|
||||
return request({
|
||||
url: '/aps/shift-template/' + shiftId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addShiftTemplate(data) {
|
||||
return request({
|
||||
url: '/aps/shift-template',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateShiftTemplate(data) {
|
||||
return request({
|
||||
url: '/aps/shift-template',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function delShiftTemplate(shiftIds) {
|
||||
return request({
|
||||
url: '/aps/shift-template/' + shiftIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function exportShiftTemplate(query) {
|
||||
return request({
|
||||
url: '/aps/shift-template/export',
|
||||
method: 'post',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 日历班次配置
|
||||
export function listCalendarShift(query) {
|
||||
return request({
|
||||
url: '/aps/calendar-shift/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getCalendarShift(configId) {
|
||||
return request({
|
||||
url: '/aps/calendar-shift/' + configId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addCalendarShift(data) {
|
||||
return request({
|
||||
url: '/aps/calendar-shift',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCalendarShift(data) {
|
||||
return request({
|
||||
url: '/aps/calendar-shift',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function delCalendarShift(configIds) {
|
||||
return request({
|
||||
url: '/aps/calendar-shift/' + configIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function exportCalendarShift(query) {
|
||||
return request({
|
||||
url: '/aps/calendar-shift/export',
|
||||
method: 'post',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function generateCalendarShiftWorkdayYear(year, overwriteExisting = false) {
|
||||
return request({
|
||||
url: `/aps/calendar-shift/generate-workday-year/${year}`,
|
||||
method: 'post',
|
||||
params: { overwriteExisting },
|
||||
timeout: 600000
|
||||
})
|
||||
}
|
||||
|
||||
// 产线能力
|
||||
export function listLineCapability(query) {
|
||||
return request({
|
||||
url: '/aps/line-capability/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getLineCapability(capabilityId) {
|
||||
return request({
|
||||
url: '/aps/line-capability/' + capabilityId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addLineCapability(data) {
|
||||
return request({
|
||||
url: '/aps/line-capability',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLineCapability(data) {
|
||||
return request({
|
||||
url: '/aps/line-capability',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function delLineCapability(capabilityIds) {
|
||||
return request({
|
||||
url: '/aps/line-capability/' + capabilityIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function exportLineCapability(query) {
|
||||
return request({
|
||||
url: '/aps/line-capability/export',
|
||||
method: 'post',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,14 @@ export function listMaterialCoil(query) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getMaterialCoilLocationGrid(query) {
|
||||
return request({
|
||||
url: '/wms/materialCoil/locationGrid',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function exportMaterialCoil(query) {
|
||||
return request({
|
||||
url: '/wms/materialCoil/export',
|
||||
|
||||
@@ -100,6 +100,31 @@ export const constantRoutes = [
|
||||
meta: { title: '个人中心', icon: 'user' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/aps',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'lineCapability',
|
||||
component: () => import('@/views/aps/lineCapability/index'),
|
||||
name: 'ApsLineCapability',
|
||||
meta: { title: '产线能力' }
|
||||
},
|
||||
{
|
||||
path: 'processManage',
|
||||
component: () => import('@/views/aps/processManage/index'),
|
||||
name: 'ApsProcessManage',
|
||||
meta: { title: '工序管理' }
|
||||
},
|
||||
{
|
||||
path: 'factory-calendar',
|
||||
component: () => import('@/views/aps/factoryCalendar'),
|
||||
name: 'ApsFactoryCalendarPage',
|
||||
meta: { title: '工厂总日历' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
512
klp-ui/src/views/aps/calendar/index.vue
Normal file
512
klp-ui/src/views/aps/calendar/index.vue
Normal file
@@ -0,0 +1,512 @@
|
||||
<template>
|
||||
<div :class="embedded ? 'factory-calendar-board embedded' : 'factory-calendar-board app-container'">
|
||||
<div class="calendar-shell">
|
||||
<div class="calendar-head">
|
||||
<div>
|
||||
<div class="calendar-title">工厂日历排产看板</div>
|
||||
<div class="calendar-sub">同一日期聚合展示全部产线×班次,悬浮查看完整任务信息,支持快速编辑日历</div>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button size="mini" @click="shiftRangeDays(-pager.pageSize)">上一段</el-button>
|
||||
<el-button size="mini" @click="shiftRangeDays(pager.pageSize)">下一段</el-button>
|
||||
<el-button size="mini" @click="setHistoryRange">近30天</el-button>
|
||||
<el-button size="mini" @click="setHybridRange">前后30天</el-button>
|
||||
<el-button size="mini" type="primary" plain @click="goToday">回到今天</el-button>
|
||||
<el-date-picker
|
||||
v-model="jumpDate"
|
||||
size="mini"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="跳转日期"
|
||||
style="width: 140px"
|
||||
@change="jumpToDate"
|
||||
/>
|
||||
<el-select v-model="pager.pageSize" size="mini" style="width: 110px" @change="pager.pageNum = 1">
|
||||
<el-option :value="7" label="每页7天" />
|
||||
<el-option :value="14" label="每页14天" />
|
||||
<el-option :value="28" label="每页28天" />
|
||||
</el-select>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="handleQuery">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form v-if="!embedded" :inline="true" size="small" class="calendar-filter">
|
||||
<el-form-item label="日期范围">
|
||||
<el-date-picker
|
||||
v-model="query.range"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产线">
|
||||
<el-select v-model="query.lineId" clearable filterable placeholder="全部产线" style="width: 220px">
|
||||
<el-option
|
||||
v-for="line in lineOptions"
|
||||
:key="line.lineId"
|
||||
:label="line.lineName || line.lineCode || ('产线' + line.lineId)"
|
||||
:value="line.lineId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单/产品">
|
||||
<el-input v-model="query.keyword" clearable placeholder="订单号、产品名" style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="el-icon-refresh-left" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="legend-wrap">
|
||||
<span class="legend-title">日历图例:</span>
|
||||
<span class="legend-item"><i class="legend-dot day-work" />工作日</span>
|
||||
<span class="legend-item"><i class="legend-dot day-rest" />休息日</span>
|
||||
</div>
|
||||
|
||||
<div class="calendar-body" :style="boardStyle" v-loading="loading">
|
||||
<el-empty v-if="!lineBoard.length || !pagedDateList.length || !shifts.length" description="暂无产线或班次基础数据" />
|
||||
|
||||
<div v-else class="date-grid" :style="dateGridStyle">
|
||||
<div class="date-col" v-for="date in displayDateList" :key="date">
|
||||
<div class="date-head">{{ date }}</div>
|
||||
<div class="slot-list">
|
||||
<el-tooltip
|
||||
v-for="slot in getDateSlots(date)"
|
||||
:key="date + '-' + slot.lineId + '-' + slot.shiftId"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<div slot="content" class="slot-tooltip">
|
||||
<div>产线:{{ slot.lineName }}</div>
|
||||
<div>班次:{{ slot.shiftName }}({{ slot.startTime }}-{{ slot.endTime }}<span v-if="slot.crossDay === 1">+1</span>)</div>
|
||||
<div>状态:{{ workdayLabel(slot.lineId, date, slot.shiftId) }}</div>
|
||||
<div v-if="slot.tasks.length">任务:{{ slot.tasks.map(i => taskTooltipText(i)).join(';') }}</div>
|
||||
<div v-else>任务:无</div>
|
||||
</div>
|
||||
<div class="slot-row" :class="workdayClass(slot.lineId, date, slot.shiftId)">
|
||||
<div class="slot-left">
|
||||
<span class="slot-line">{{ shortText(slot.lineName, 6) }}</span>
|
||||
<span class="slot-shift">{{ shortText(slot.shiftName, 4) }}</span>
|
||||
</div>
|
||||
<div class="slot-mid">
|
||||
<span v-if="slot.tasks.length" class="task-chip" :style="{ backgroundColor: getTaskColor(slot.tasks[0]), borderColor: getTaskColor(slot.tasks[0]) }">
|
||||
{{ shortText(taskDisplayName(slot.tasks[0]), 14) }}
|
||||
</span>
|
||||
<span v-else class="no-task">无任务</span>
|
||||
<span v-if="slot.tasks.length > 1" class="task-count">+{{ slot.tasks.length - 1 }}</span>
|
||||
</div>
|
||||
<el-button type="text" size="mini" class="slot-edit" @click.stop="openShiftEditor(slot, date)">编辑</el-button>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pager-wrap" v-if="dateList.length > pager.pageSize">
|
||||
<el-pagination
|
||||
small
|
||||
background
|
||||
layout="prev, pager, next, total"
|
||||
:current-page.sync="pager.pageNum"
|
||||
:page-size="pager.pageSize"
|
||||
:total="dateList.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog title="编辑工厂日历班次" :visible.sync="shiftEditor.visible" width="500px" append-to-body>
|
||||
<el-form ref="shiftEditorFormRef" :model="shiftEditor.form" :rules="shiftEditorRules" label-width="100px" size="small">
|
||||
<el-form-item label="日期"><el-input :value="shiftEditor.meta.date" readonly /></el-form-item>
|
||||
<el-form-item label="产线"><el-input :value="shiftEditor.meta.lineName" readonly /></el-form-item>
|
||||
<el-form-item label="班次"><el-input :value="shiftEditor.meta.shiftName" readonly /></el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="shiftEditor.form.status">
|
||||
<el-radio :label="1">工作日</el-radio>
|
||||
<el-radio :label="0">停用/休息</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划工时" prop="plannedHours">
|
||||
<el-input-number v-model="shiftEditor.form.plannedHours" :min="0" :max="24" :step="0.5" :precision="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="shiftEditor.form.remark" type="textarea" :rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button size="small" @click="shiftEditor.visible = false">取 消</el-button>
|
||||
<el-button size="small" type="primary" :loading="shiftEditor.saving" @click="submitShiftEditor">保 存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fetchFactoryCalendar, listShiftTemplate, addCalendarShift, updateCalendarShift } from '@/api/aps/aps'
|
||||
import { listProductionLine } from '@/api/wms/productionLine'
|
||||
|
||||
const TASK_COLORS = ['#5B8FF9', '#61DDAA', '#65789B', '#F6BD16', '#7262FD', '#78D3F8', '#9661BC', '#F6903D', '#008685', '#F08BB4']
|
||||
|
||||
export default {
|
||||
name: 'ApsFactoryCalendarBoard',
|
||||
props: {
|
||||
embedded: { type: Boolean, default: false },
|
||||
height: { type: Number, default: 420 },
|
||||
externalQuery: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
query: { range: [], lineId: undefined, keyword: '', orderId: undefined },
|
||||
jumpDate: '',
|
||||
pager: { pageNum: 1, pageSize: 7 },
|
||||
lineOptions: [],
|
||||
shifts: [],
|
||||
calendarPayload: {
|
||||
overview: {},
|
||||
dateList: [],
|
||||
lineNameMap: {},
|
||||
shiftNameMap: {},
|
||||
lineDayMap: {},
|
||||
lineShiftDayMap: {}
|
||||
},
|
||||
shiftEditor: {
|
||||
visible: false,
|
||||
saving: false,
|
||||
meta: { configId: null, date: '', lineId: null, lineName: '', shiftId: null, shiftName: '' },
|
||||
form: { status: 1, plannedHours: 8, remark: '' }
|
||||
},
|
||||
shiftEditorRules: {
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
plannedHours: [{ required: true, message: '请填写计划工时', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
boardStyle() { return { minHeight: `${this.height}px` } },
|
||||
dateGridStyle() {
|
||||
const count = this.displayDateList.length || 1
|
||||
return { gridTemplateColumns: `repeat(${count}, minmax(260px, 1fr))` }
|
||||
},
|
||||
lineBoard() {
|
||||
const map = this.calendarPayload.lineNameMap || {}
|
||||
let rows = Object.keys(map).map(id => ({ lineId: String(id), lineName: map[id] || ('产线' + id) }))
|
||||
if (this.query.lineId) rows = rows.filter(i => String(i.lineId) === String(this.query.lineId))
|
||||
return rows
|
||||
},
|
||||
dateList() {
|
||||
if (!this.query.range || this.query.range.length !== 2) return []
|
||||
const start = new Date(`${this.query.range[0]} 00:00:00`)
|
||||
const end = new Date(`${this.query.range[1]} 00:00:00`)
|
||||
const arr = []
|
||||
const cursor = new Date(start.getTime())
|
||||
while (cursor.getTime() <= end.getTime()) {
|
||||
arr.push(this.formatDate(cursor))
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
return arr
|
||||
},
|
||||
pagedDateList() {
|
||||
const start = (this.pager.pageNum - 1) * this.pager.pageSize
|
||||
return this.dateList.slice(start, start + this.pager.pageSize)
|
||||
},
|
||||
displayDateList() {
|
||||
return this.pagedDateList || []
|
||||
},
|
||||
taskLegend() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dateList() {
|
||||
const maxPage = Math.max(1, Math.ceil(this.dateList.length / this.pager.pageSize))
|
||||
if (this.pager.pageNum > maxPage) this.pager.pageNum = maxPage
|
||||
},
|
||||
externalQuery: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (!value || Object.keys(value).length === 0) return
|
||||
if (value.range && value.range.length === 2) this.query.range = [value.range[0], value.range[1]]
|
||||
this.query.lineId = value.lineId || undefined
|
||||
this.query.orderId = value.orderId || undefined
|
||||
this.query.keyword = value.keyword || ''
|
||||
if (this.shifts.length) this.handleQuery()
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.query.range.length) this.initDefaultRange()
|
||||
this.loadLines()
|
||||
this.loadShifts().then(() => this.handleQuery())
|
||||
},
|
||||
methods: {
|
||||
initDefaultRange() {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 30)
|
||||
this.query.range = [this.formatDate(start), this.formatDate(end)]
|
||||
},
|
||||
parseDate(dateText) {
|
||||
return new Date(`${dateText} 00:00:00`)
|
||||
},
|
||||
shiftRangeDays(delta) {
|
||||
if (!this.query.range || this.query.range.length !== 2) {
|
||||
this.initDefaultRange()
|
||||
}
|
||||
const start = this.parseDate(this.query.range[0])
|
||||
const end = this.parseDate(this.query.range[1])
|
||||
start.setDate(start.getDate() + delta)
|
||||
end.setDate(end.getDate() + delta)
|
||||
this.query.range = [this.formatDate(start), this.formatDate(end)]
|
||||
this.pager.pageNum = 1
|
||||
this.handleQuery()
|
||||
},
|
||||
setHistoryRange() {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30)
|
||||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
this.query.range = [this.formatDate(start), this.formatDate(end)]
|
||||
this.pager.pageNum = 1
|
||||
this.handleQuery()
|
||||
},
|
||||
setHybridRange() {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30)
|
||||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 30)
|
||||
this.query.range = [this.formatDate(start), this.formatDate(end)]
|
||||
this.pager.pageNum = 1
|
||||
this.handleQuery()
|
||||
},
|
||||
goToday() {
|
||||
const now = new Date()
|
||||
const days = this.pager.pageSize
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - Math.floor(days / 2))
|
||||
const end = new Date(start.getTime())
|
||||
end.setDate(end.getDate() + days - 1)
|
||||
this.query.range = [this.formatDate(start), this.formatDate(end)]
|
||||
this.pager.pageNum = 1
|
||||
this.handleQuery()
|
||||
},
|
||||
jumpToDate(value) {
|
||||
if (!value) return
|
||||
const d = this.parseDate(value)
|
||||
const start = new Date(d.getFullYear(), d.getMonth(), d.getDate() - Math.floor(this.pager.pageSize / 2))
|
||||
const end = new Date(start.getTime())
|
||||
end.setDate(end.getDate() + this.pager.pageSize - 1)
|
||||
this.query.range = [this.formatDate(start), this.formatDate(end)]
|
||||
this.pager.pageNum = 1
|
||||
this.handleQuery()
|
||||
},
|
||||
formatDate(d) {
|
||||
const y = d.getFullYear()
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
},
|
||||
shortText(text, maxLen) {
|
||||
const s = String(text || '')
|
||||
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s
|
||||
},
|
||||
shortTime(dt) {
|
||||
if (!dt) return '--:--'
|
||||
const d = new Date(dt)
|
||||
if (Number.isNaN(d.getTime())) return '--:--'
|
||||
return `${`${d.getHours()}`.padStart(2, '0')}:${`${d.getMinutes()}`.padStart(2, '0')}`
|
||||
},
|
||||
taskDisplayName(task) {
|
||||
return task && task.label ? task.label : '任务'
|
||||
},
|
||||
taskTimeLabel(task) {
|
||||
return task && task.label ? task.label : '--'
|
||||
},
|
||||
taskTooltipText(task) {
|
||||
return task && task.title ? task.title : (task && task.label ? task.label : '--')
|
||||
},
|
||||
async loadLines() {
|
||||
const res = await listProductionLine({ pageNum: 1, pageSize: 1000 })
|
||||
this.lineOptions = res.rows || []
|
||||
},
|
||||
async loadShifts() {
|
||||
const res = await listShiftTemplate({ pageNum: 1, pageSize: 1000 })
|
||||
this.shifts = (res.rows || []).sort((a, b) => String(a.startTime).localeCompare(String(b.startTime)))
|
||||
},
|
||||
async handleQuery() {
|
||||
if (!this.query.range || this.query.range.length !== 2) {
|
||||
this.$message.warning('请先选择日期范围')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
queryStart: `${this.query.range[0]} 00:00:00`,
|
||||
queryEnd: `${this.query.range[1]} 23:59:59`,
|
||||
lineId: this.query.lineId || undefined,
|
||||
orderId: this.query.orderId || undefined
|
||||
}
|
||||
const aggRes = await fetchFactoryCalendar(params)
|
||||
this.calendarPayload = aggRes.data || {
|
||||
overview: {}, dateList: [], lineNameMap: {}, shiftNameMap: {}, lineDayMap: {}, lineShiftDayMap: {}
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
resetQuery() {
|
||||
this.query.lineId = undefined
|
||||
this.query.orderId = undefined
|
||||
this.query.keyword = ''
|
||||
this.pager.pageNum = 1
|
||||
this.initDefaultRange()
|
||||
this.handleQuery()
|
||||
},
|
||||
getDateSlots(date) {
|
||||
const slots = []
|
||||
this.lineBoard.forEach(line => {
|
||||
this.shifts.forEach(shift => {
|
||||
const day = this.getLineShiftDay(line.lineId, shift.shiftId, date)
|
||||
slots.push({
|
||||
...line,
|
||||
shiftId: shift.shiftId,
|
||||
shiftName: shift.shiftName,
|
||||
startTime: shift.startTime,
|
||||
endTime: shift.endTime,
|
||||
crossDay: shift.crossDay,
|
||||
tasks: day.tasks || [],
|
||||
taskCount: day.taskCount || 0,
|
||||
dayStatus: day.dayStatus
|
||||
})
|
||||
})
|
||||
})
|
||||
return slots
|
||||
},
|
||||
workdayLabel(lineId, date, shiftId) {
|
||||
const day = this.getLineShiftDay(lineId, shiftId, date)
|
||||
if (day && day.dayStatus !== undefined && day.dayStatus !== null) {
|
||||
return Number(day.dayStatus) === 1 ? '工作日' : '休息'
|
||||
}
|
||||
return '默认'
|
||||
},
|
||||
workdayClass(lineId, date, shiftId) {
|
||||
const day = this.getLineShiftDay(lineId, shiftId, date)
|
||||
if (day && day.dayStatus !== undefined && day.dayStatus !== null) {
|
||||
return Number(day.dayStatus) === 1 ? 'workday' : 'restday'
|
||||
}
|
||||
return 'neutral'
|
||||
},
|
||||
openShiftEditor(slot, date) {
|
||||
const day = this.getLineShiftDay(slot.lineId, slot.shiftId, date)
|
||||
this.shiftEditor.meta = {
|
||||
configId: day && day.configId ? day.configId : null,
|
||||
date,
|
||||
lineId: slot.lineId,
|
||||
lineName: slot.lineName,
|
||||
shiftId: slot.shiftId,
|
||||
shiftName: slot.shiftName
|
||||
}
|
||||
this.shiftEditor.form = {
|
||||
status: day && day.dayStatus !== undefined && day.dayStatus !== null ? Number(day.dayStatus) : 1,
|
||||
plannedHours: day && day.plannedHours !== undefined && day.plannedHours !== null ? Number(day.plannedHours) : 8,
|
||||
remark: day && day.remark ? day.remark : ''
|
||||
}
|
||||
this.shiftEditor.visible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.shiftEditorFormRef && this.$refs.shiftEditorFormRef.clearValidate()
|
||||
})
|
||||
},
|
||||
submitShiftEditor() {
|
||||
this.$refs.shiftEditorFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
const payload = {
|
||||
configId: this.shiftEditor.meta.configId || undefined,
|
||||
calendarDate: this.shiftEditor.meta.date,
|
||||
lineId: this.shiftEditor.meta.lineId,
|
||||
shiftId: this.shiftEditor.meta.shiftId,
|
||||
status: this.shiftEditor.form.status,
|
||||
plannedHours: this.shiftEditor.form.plannedHours,
|
||||
remark: this.shiftEditor.form.remark
|
||||
}
|
||||
const req = payload.configId ? updateCalendarShift(payload) : addCalendarShift(payload)
|
||||
this.shiftEditor.saving = true
|
||||
req.then(() => {
|
||||
this.$message.success(payload.configId ? '日历班次已更新' : '日历班次已新增')
|
||||
this.shiftEditor.visible = false
|
||||
this.handleQuery()
|
||||
}).finally(() => {
|
||||
this.shiftEditor.saving = false
|
||||
})
|
||||
})
|
||||
},
|
||||
getLineShiftDay(lineId, shiftId, date) {
|
||||
const map = this.calendarPayload.lineShiftDayMap || {}
|
||||
return map[`${String(lineId)}|${String(shiftId)}|${date}`] || { taskCount: 0, dayStatus: null, tasks: [] }
|
||||
},
|
||||
resolveShiftRange(dateText, shift) {
|
||||
if (!shift || !shift.startTime || !shift.endTime || !dateText) return null
|
||||
const baseDate = new Date(`${dateText} 00:00:00`)
|
||||
const [sh, sm, ss] = shift.startTime.split(':').map(v => Number(v || 0))
|
||||
const [eh, em, es] = shift.endTime.split(':').map(v => Number(v || 0))
|
||||
const start = new Date(baseDate.getTime())
|
||||
start.setHours(sh, sm, ss, 0)
|
||||
const end = new Date(baseDate.getTime())
|
||||
end.setHours(eh, em, es, 0)
|
||||
if (shift.crossDay === 1 || end.getTime() < start.getTime()) end.setDate(end.getDate() + 1)
|
||||
return { start: start.getTime(), end: end.getTime() }
|
||||
},
|
||||
taskKey(task) {
|
||||
return `${task.orderId || task.orderCode || ''}_${task.processId || task.processName || ''}`
|
||||
},
|
||||
getTaskColor(task) {
|
||||
const key = this.taskKey(task)
|
||||
let hash = 0
|
||||
for (let i = 0; i < key.length; i++) hash = ((hash << 5) - hash) + key.charCodeAt(i)
|
||||
const idx = Math.abs(hash) % TASK_COLORS.length
|
||||
return TASK_COLORS[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.factory-calendar-board {
|
||||
.calendar-shell { border: 1px solid #eef2f7; border-radius: 8px; background: #fff; padding: 10px; }
|
||||
.calendar-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.calendar-title { font-size: 16px; font-weight: 600; }
|
||||
.calendar-sub { margin-top: 4px; color: #909399; font-size: 12px; }
|
||||
.head-actions { display: flex; gap: 8px; align-items: center; }
|
||||
.calendar-filter { margin-top: 8px; margin-bottom: 6px; }
|
||||
.legend-wrap { margin-bottom: 8px; font-size: 12px; color: #606266; }
|
||||
.legend-title { color: #909399; margin-right: 6px; }
|
||||
.legend-item { margin-right: 10px; }
|
||||
.legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; }
|
||||
.legend-dot.day-work { background: #67c23a; }
|
||||
.legend-dot.day-rest { background: #f56c6c; }
|
||||
.date-grid { display: grid; gap: 10px; overflow-x: auto; }
|
||||
.date-col { border: 1px solid #f1f4f8; border-radius: 6px; padding: 8px; background: #fff; }
|
||||
.date-head { font-size: 13px; font-weight: 600; margin-bottom: 8px; color: #409eff; }
|
||||
.slot-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.slot-row { border: 1px solid #f2f5f9; border-radius: 4px; background: #fcfdff; padding: 4px 6px; display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
||||
.slot-row.workday { border-left: 3px solid #67c23a; background: #edf8ee; }
|
||||
.slot-row.restday { border-left: 3px solid #f56c6c; background: #fff2e6; }
|
||||
.slot-row.neutral { border-left: 3px solid #dcdfe6; }
|
||||
.slot-left { min-width: 82px; font-size: 12px; color: #606266; display: flex; flex-direction: column; }
|
||||
.slot-line { font-weight: 600; color: #303133; }
|
||||
.slot-shift { color: #909399; }
|
||||
.slot-mid { flex: 1; min-width: 0; display: flex; align-items: center; gap: 4px; }
|
||||
.task-chip { color: #fff; border: 1px solid transparent; border-radius: 10px; padding: 0 8px; font-size: 12px; line-height: 20px; max-width: 130px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.task-count { color: #909399; font-size: 12px; }
|
||||
.no-task { color: #c0c4cc; font-size: 12px; }
|
||||
.slot-edit { padding: 0; font-size: 12px; }
|
||||
.pager-wrap { margin-top: 10px; display: flex; justify-content: flex-end; }
|
||||
}
|
||||
|
||||
.slot-tooltip {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.embedded { padding: 0; }
|
||||
</style>
|
||||
343
klp-ui/src/views/aps/calendarShift/index.vue
Normal file
343
klp-ui/src/views/aps/calendarShift/index.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
|
||||
<el-form-item label="日期" prop="calendarDate">
|
||||
<el-date-picker
|
||||
v-model="queryParams.calendarDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产线" prop="lineId">
|
||||
<el-select v-model="queryParams.lineId" placeholder="请选择产线" clearable filterable style="width: 180px">
|
||||
<el-option
|
||||
v-for="line in lineOptions"
|
||||
:key="line.lineId"
|
||||
:label="line.lineName"
|
||||
:value="line.lineId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="班次" prop="shiftId">
|
||||
<el-select v-model="queryParams.shiftId" placeholder="请选择班次" clearable filterable style="width: 180px">
|
||||
<el-option
|
||||
v-for="shift in shiftOptions"
|
||||
:key="shift.shiftId"
|
||||
:label="shift.shiftName"
|
||||
:value="shift.shiftId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
|
||||
<el-option label="停用" :value="0" />
|
||||
<el-option label="启用" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-button type="info" plain icon="el-icon-date" size="mini" @click="handleGenerateYearWorkday">一键生成全年工作日</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<KLPTable v-loading="loading" :data="calendarShiftList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="日期" align="center" prop="calendarDate" width="120" />
|
||||
<el-table-column label="产线" align="center" prop="lineName" width="150" />
|
||||
<el-table-column label="班次" align="center" prop="shiftName" width="150" />
|
||||
<el-table-column label="计划工时" align="center" prop="plannedHours" width="100" />
|
||||
<el-table-column label="状态" align="center" prop="statusName" width="80" />
|
||||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 添加或修改日历班次配置对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="日期" prop="calendarDate">
|
||||
<el-date-picker
|
||||
v-model="form.calendarDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产线" prop="lineId">
|
||||
<el-select v-model="form.lineId" placeholder="请选择产线" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="line in lineOptions"
|
||||
:key="line.lineId"
|
||||
:label="line.lineName"
|
||||
:value="line.lineId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="班次" prop="shiftId">
|
||||
<el-select v-model="form.shiftId" placeholder="请选择班次" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="shift in shiftOptions"
|
||||
:key="shift.shiftId"
|
||||
:label="shift.shiftName"
|
||||
:value="shift.shiftId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划工时" prop="plannedHours">
|
||||
<el-input-number v-model="form.plannedHours" :min="0" :precision="2" :step="0.5" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="0">停用</el-radio>
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listCalendarShift, getCalendarShift, delCalendarShift, addCalendarShift, updateCalendarShift, exportCalendarShift, listShiftTemplate, generateCalendarShiftWorkdayYear } from "@/api/aps/aps";
|
||||
import { listProductionLine } from "@/api/wms/productionLine";
|
||||
|
||||
export default {
|
||||
name: "ApsCalendarShift",
|
||||
data() {
|
||||
return {
|
||||
buttonLoading: false,
|
||||
loading: true,
|
||||
ids: [],
|
||||
single: true,
|
||||
multiple: true,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
calendarShiftList: [],
|
||||
lineOptions: [],
|
||||
shiftOptions: [],
|
||||
title: "",
|
||||
open: false,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 50,
|
||||
calendarDate: undefined,
|
||||
lineId: undefined,
|
||||
shiftId: undefined,
|
||||
status: undefined
|
||||
},
|
||||
form: {},
|
||||
generateYear: new Date().getFullYear(),
|
||||
rules: {
|
||||
calendarDate: [{ required: true, message: "日期不能为空", trigger: "change" }],
|
||||
lineId: [{ required: true, message: "产线不能为空", trigger: "change" }],
|
||||
shiftId: [{ required: true, message: "班次不能为空", trigger: "change" }]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.loadLines();
|
||||
this.loadShifts();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listCalendarShift(this.queryParams).then(response => {
|
||||
this.calendarShiftList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
loadLines() {
|
||||
listProductionLine({ pageNum: 1, pageSize: 1000 }).then(response => {
|
||||
this.lineOptions = response.rows || [];
|
||||
});
|
||||
},
|
||||
loadShifts() {
|
||||
listShiftTemplate({ pageNum: 1, pageSize: 1000 }).then(response => {
|
||||
this.shiftOptions = response.rows || [];
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
reset() {
|
||||
this.form = {
|
||||
configId: undefined,
|
||||
calendarDate: undefined,
|
||||
lineId: undefined,
|
||||
shiftId: undefined,
|
||||
plannedHours: 0,
|
||||
status: 1,
|
||||
remark: undefined
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.configId);
|
||||
this.single = selection.length !== 1;
|
||||
this.multiple = !selection.length;
|
||||
},
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加日历班次配置";
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const configId = row.configId || this.ids[0];
|
||||
getCalendarShift(configId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改日历班次配置";
|
||||
});
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.configId != null) {
|
||||
updateCalendarShift(this.form).then(() => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addCalendarShift(this.form).then(() => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
const configIds = row.configId || this.ids;
|
||||
this.$modal.confirm('是否确认删除选中的日历班次配置数据?').then(() => {
|
||||
this.loading = true;
|
||||
return delCalendarShift(configIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleExport() {
|
||||
this.download('aps/calendar-shift/export', {
|
||||
...this.queryParams
|
||||
}, `calendar_shift_${new Date().getTime()}.xlsx`);
|
||||
},
|
||||
handleGenerateYearWorkday() {
|
||||
this.$prompt('请输入要生成的年份(例如 2026)', '一键生成全年工作日', {
|
||||
confirmButtonText: '下一步',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: String(this.generateYear),
|
||||
inputPattern: /^(20\d{2}|2100)$/,
|
||||
inputErrorMessage: '请输入 2000-2100 的年份'
|
||||
}).then(({ value }) => {
|
||||
const year = Number(value)
|
||||
this.$confirm('是否覆盖已有配置?\n- 选“是”:将已有记录强制改为工作日\n- 选“否”:仅补齐缺失记录', '覆盖选项', {
|
||||
confirmButtonText: '是(覆盖)',
|
||||
cancelButtonText: '否(仅补齐)',
|
||||
distinguishCancelAndClose: true,
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.loading = true
|
||||
return generateCalendarShiftWorkdayYear(year, true).then(res => {
|
||||
this.generateYear = year
|
||||
this.$modal.msgSuccess(`生成完成(覆盖模式),处理记录 ${res.data || 0} 条`)
|
||||
this.getList()
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}).catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.loading = true
|
||||
return generateCalendarShiftWorkdayYear(year, false).then(res => {
|
||||
this.generateYear = year
|
||||
this.$modal.msgSuccess(`生成完成(仅补齐),处理记录 ${res.data || 0} 条`)
|
||||
this.getList()
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
::v-deep .el-table,
|
||||
::v-deep .el-table--border,
|
||||
::v-deep .el-table--group {
|
||||
border-color: #eef2f7;
|
||||
}
|
||||
|
||||
::v-deep .el-table td,
|
||||
::v-deep .el-table th.is-leaf {
|
||||
border-bottom: 1px solid #f3f5f8;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
583
klp-ui/src/views/aps/factoryCalendar.vue
Normal file
583
klp-ui/src/views/aps/factoryCalendar.vue
Normal file
@@ -0,0 +1,583 @@
|
||||
<template>
|
||||
<div class="factory-calendar-page app-container" v-loading="loading">
|
||||
<div class="toolbar-row">
|
||||
<el-form :inline="true" size="small" class="filter-row">
|
||||
<el-form-item label="年份">
|
||||
<el-date-picker
|
||||
v-model="query.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="选择年份"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleRefresh">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="month-strip">
|
||||
<el-button
|
||||
v-for="m in monthOptions"
|
||||
:key="`m-${m}`"
|
||||
size="mini"
|
||||
:type="query.monthNum === m ? 'primary' : 'default'"
|
||||
plain
|
||||
class="month-btn"
|
||||
@click="selectMonth(m)"
|
||||
>
|
||||
{{ m }}月
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="k"><i class="el-icon-s-operation" />任务总数</div>
|
||||
<div class="v">{{ overview.taskCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="k"><i class="el-icon-data-line" />活跃产线</div>
|
||||
<div class="v">{{ overview.activeLineCount }} / {{ overview.totalLineCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="k"><i class="el-icon-time" />排产总工时</div>
|
||||
<div class="v">{{ overview.totalHours }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="k"><i class="el-icon-date" />峰值日</div>
|
||||
<div class="v">{{ overview.peakDayText }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="k"><i class="el-icon-s-flag" />最忙产线</div>
|
||||
<div class="v">{{ overview.busiestLineText }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="k"><i class="el-icon-collection-tag" />最忙产线-班组</div>
|
||||
<div class="v">{{ overview.busiestLineShiftText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-block">
|
||||
<div class="block-head">
|
||||
<div class="block-title">产线日历</div>
|
||||
<div class="legend">
|
||||
<span class="legend-item"><i class="dot work" />工作日</span>
|
||||
<span class="legend-item"><i class="dot rest" />休息日</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeLineTab" type="card" class="sub-tabs" v-if="lineRows.length">
|
||||
<el-tab-pane
|
||||
v-for="line in lineRows"
|
||||
:key="`line-tab-${line.lineId}`"
|
||||
:name="String(line.lineId)"
|
||||
:label="line.lineName"
|
||||
>
|
||||
<div class="calendar-full">
|
||||
<div class="week-head">
|
||||
<span v-for="w in weekLabels" :key="`line-week-${line.lineId}-${w}`">{{ w }}</span>
|
||||
</div>
|
||||
<div class="month-grid">
|
||||
<div
|
||||
v-for="cell in monthCells"
|
||||
:key="`line-${line.lineId}-${cell.key}`"
|
||||
class="day-cell"
|
||||
:class="[cell.isCurrentMonth ? 'cur' : 'other', lineDayClass(line.lineId, cell.date), dayExtraClass(cell)]"
|
||||
>
|
||||
<div class="day-no">{{ cell.day }}</div>
|
||||
<template v-if="cell.isCurrentMonth">
|
||||
<div class="task-count">{{ getLineDay(line.lineId, cell.date).taskCount }} 单</div>
|
||||
<div
|
||||
v-for="(t, idx) in tasksByLineDate(line.lineId, cell.date).slice(0, 3)"
|
||||
:key="`line-${line.lineId}-${cell.date}-${idx}`"
|
||||
class="task-item"
|
||||
:title="taskLabel(t)"
|
||||
>
|
||||
{{ shortTaskWithShift(t) }}
|
||||
</div>
|
||||
<div class="more" v-if="getLineDay(line.lineId, cell.date).taskCount > 3">+{{ getLineDay(line.lineId, cell.date).taskCount - 3 }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-empty v-else description="暂无产线数据" />
|
||||
</div>
|
||||
|
||||
<div class="calendar-block">
|
||||
<div class="block-head">
|
||||
<div class="block-title">产线-班组日历</div>
|
||||
<div class="legend">
|
||||
<span class="legend-item"><i class="dot work" />工作日</span>
|
||||
<span class="legend-item"><i class="dot rest" />休息日</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeLineShiftTab" type="card" class="sub-tabs" v-if="lineShiftRows.length">
|
||||
<el-tab-pane
|
||||
v-for="item in lineShiftRows"
|
||||
:key="`ls-tab-${item.lineId}-${item.shiftId}`"
|
||||
:name="`${item.lineId}-${item.shiftId}`"
|
||||
:label="`${item.lineName} · ${item.shiftName}`"
|
||||
>
|
||||
<div class="calendar-full">
|
||||
<div class="week-head">
|
||||
<span v-for="w in weekLabels" :key="`ls-week-${item.lineId}-${item.shiftId}-${w}`">{{ w }}</span>
|
||||
</div>
|
||||
<div class="month-grid">
|
||||
<div
|
||||
v-for="cell in monthCells"
|
||||
:key="`ls-${item.lineId}-${item.shiftId}-${cell.key}`"
|
||||
class="day-cell"
|
||||
:class="[cell.isCurrentMonth ? 'cur' : 'other', lineShiftDayClass(item.lineId, item.shiftId, cell.date), dayExtraClass(cell)]"
|
||||
>
|
||||
<div class="day-no">{{ cell.day }}</div>
|
||||
<template v-if="cell.isCurrentMonth">
|
||||
<div class="task-count">{{ getLineShiftDay(item.lineId, item.shiftId, cell.date).taskCount }} 单</div>
|
||||
<div
|
||||
v-for="(t, idx) in tasksByLineShiftDate(item.lineId, item.shiftId, cell.date).slice(0, 3)"
|
||||
:key="`ls-${item.lineId}-${item.shiftId}-${cell.date}-${idx}`"
|
||||
class="task-item"
|
||||
:title="taskLabel(t)"
|
||||
>
|
||||
{{ shortTask(t) }}
|
||||
</div>
|
||||
<div class="more" v-if="getLineShiftDay(item.lineId, item.shiftId, cell.date).taskCount > 3">+{{ getLineShiftDay(item.lineId, item.shiftId, cell.date).taskCount - 3 }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-empty v-else description="暂无产线-班组数据" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fetchFactoryCalendar } from '@/api/aps/aps'
|
||||
|
||||
export default {
|
||||
name: 'ApsFactoryCalendarPage',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
query: { year: String(new Date().getFullYear()), monthNum: new Date().getMonth() + 1 },
|
||||
monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
weekLabels: ['日', '一', '二', '三', '四', '五', '六'],
|
||||
activeLineTab: '',
|
||||
activeLineShiftTab: '',
|
||||
payload: {
|
||||
overview: {},
|
||||
dateList: [],
|
||||
lineNameMap: {},
|
||||
shiftNameMap: {},
|
||||
lineDayMap: {},
|
||||
lineShiftDayMap: {}
|
||||
},
|
||||
legendVisible: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
monthText() {
|
||||
return `${this.query.year}-${`${this.query.monthNum}`.padStart(2, '0')}`
|
||||
},
|
||||
lineRows() {
|
||||
return Object.keys(this.payload.lineNameMap || {}).map(id => ({ lineId: String(id), lineName: this.payload.lineNameMap[id] }))
|
||||
},
|
||||
lineShiftRows() {
|
||||
const rows = []
|
||||
Object.keys(this.payload.lineNameMap || {}).forEach(lineId => {
|
||||
Object.keys(this.payload.shiftNameMap || {}).forEach(shiftId => {
|
||||
rows.push({
|
||||
lineId: String(lineId),
|
||||
lineName: this.payload.lineNameMap[lineId],
|
||||
shiftId: String(shiftId),
|
||||
shiftName: this.payload.shiftNameMap[shiftId]
|
||||
})
|
||||
})
|
||||
})
|
||||
return rows
|
||||
},
|
||||
monthRange() {
|
||||
const y = Number((this.query.year || '').toString())
|
||||
const m = Number(this.query.monthNum || 0)
|
||||
if (!y || !m) return { start: '', end: '' }
|
||||
const start = new Date(y, m - 1, 1)
|
||||
const end = new Date(y, m, 0)
|
||||
return { start: this.formatDate(start), end: this.formatDate(end) }
|
||||
},
|
||||
monthCells() {
|
||||
const { start, end } = this.monthRange
|
||||
if (!start || !end) return []
|
||||
const startDate = new Date(`${start} 00:00:00`)
|
||||
const endDate = new Date(`${end} 00:00:00`)
|
||||
const firstWeekDay = startDate.getDay()
|
||||
const cells = []
|
||||
const prefixStart = new Date(startDate.getTime())
|
||||
prefixStart.setDate(prefixStart.getDate() - firstWeekDay)
|
||||
const totalDays = Math.ceil((endDate.getTime() - prefixStart.getTime()) / 86400000) + 1
|
||||
const totalCells = Math.ceil(totalDays / 7) * 7
|
||||
for (let i = 0; i < totalCells; i++) {
|
||||
const d = new Date(prefixStart.getTime())
|
||||
d.setDate(prefixStart.getDate() + i)
|
||||
const date = this.formatDate(d)
|
||||
cells.push({ key: `${date}-${i}`, date, day: d.getDate(), isCurrentMonth: date >= start && date <= end })
|
||||
}
|
||||
return cells
|
||||
},
|
||||
overview() {
|
||||
return this.payload.overview || {
|
||||
taskCount: 0,
|
||||
activeLineCount: 0,
|
||||
totalLineCount: 0,
|
||||
totalHours: '0.0',
|
||||
peakDayText: '-',
|
||||
busiestLineText: '-',
|
||||
busiestLineShiftText: '-'
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleRefresh()
|
||||
},
|
||||
methods: {
|
||||
selectMonth(m) {
|
||||
this.query.monthNum = m
|
||||
this.handleRefresh()
|
||||
},
|
||||
formatDate(d) {
|
||||
const y = d.getFullYear()
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
},
|
||||
async handleRefresh() {
|
||||
const { start, end } = this.monthRange
|
||||
if (!start || !end) {
|
||||
this.$message.warning('请先选择月份')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await fetchFactoryCalendar({ queryStart: `${start} 00:00:00`, queryEnd: `${end} 23:59:59` })
|
||||
this.payload = res.data || {
|
||||
overview: {}, dateList: [], lineNameMap: {}, shiftNameMap: {}, lineDayMap: {}, lineShiftDayMap: {}
|
||||
}
|
||||
if (this.lineRows.length && !this.lineRows.find(i => String(i.lineId) === this.activeLineTab)) {
|
||||
this.activeLineTab = String(this.lineRows[0].lineId)
|
||||
}
|
||||
if (this.lineShiftRows.length && !this.lineShiftRows.find(i => `${i.lineId}-${i.shiftId}` === this.activeLineShiftTab)) {
|
||||
this.activeLineShiftTab = `${this.lineShiftRows[0].lineId}-${this.lineShiftRows[0].shiftId}`
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getLineDay(lineId, date) {
|
||||
const key = `${String(lineId)}|${date}`
|
||||
const m = this.payload.lineDayMap || {}
|
||||
const found = m[key]
|
||||
return found || { taskCount: 0, shiftCount: 0, tasks: [] }
|
||||
},
|
||||
getLineShiftDay(lineId, shiftId, date) {
|
||||
const key = `${String(lineId)}|${String(shiftId)}|${date}`
|
||||
const m = this.payload.lineShiftDayMap || {}
|
||||
const found = m[key]
|
||||
return found || { taskCount: 0, lineCount: 0, tasks: [] }
|
||||
},
|
||||
tasksByLineDate(lineId, date) {
|
||||
return this.getLineDay(lineId, date).tasks || []
|
||||
},
|
||||
tasksByLineShiftDate(lineId, shiftId, date) {
|
||||
return this.getLineShiftDay(lineId, shiftId, date).tasks || []
|
||||
},
|
||||
taskLabel(task) {
|
||||
return task && task.title ? task.title : '-'
|
||||
},
|
||||
shortTask(task) {
|
||||
return task && task.label ? task.label : '-'
|
||||
},
|
||||
shortTaskWithShift(task) {
|
||||
return this.shortTask(task)
|
||||
},
|
||||
lineDayClass(lineId, date) {
|
||||
const day = this.getLineDay(lineId, date)
|
||||
if (day.dayStatus === 1) return 'workday'
|
||||
if (day.dayStatus === 0) return 'restday'
|
||||
return day.taskCount > 0 ? 'workday' : 'neutral'
|
||||
},
|
||||
lineShiftDayClass(lineId, shiftId, date) {
|
||||
const day = this.getLineShiftDay(lineId, shiftId, date)
|
||||
if (day.dayStatus === 1) return 'workday'
|
||||
if (day.dayStatus === 0) return 'restday'
|
||||
return day.taskCount > 0 ? 'workday' : 'neutral'
|
||||
},
|
||||
dayExtraClass(cell) {
|
||||
const classes = []
|
||||
const d = new Date(`${cell.date} 00:00:00`)
|
||||
const week = d.getDay()
|
||||
if (week === 0 || week === 6) classes.push('is-weekend')
|
||||
const today = this.formatDate(new Date())
|
||||
if (cell.date === today) classes.push('is-today')
|
||||
return classes.join(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.factory-calendar-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
background: #fff;
|
||||
border: 1px solid #e9edf3;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
::v-deep .filter-row .el-date-editor.el-input,
|
||||
::v-deep .filter-row .el-date-editor.el-input__inner {
|
||||
width: 120px !important;
|
||||
}
|
||||
|
||||
::v-deep .filter-row .el-input__inner {
|
||||
border-radius: 8px;
|
||||
border-color: #d7deea;
|
||||
}
|
||||
|
||||
::v-deep .filter-row .el-input__inner:focus {
|
||||
border-color: #7aa9ff;
|
||||
}
|
||||
|
||||
.month-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-btn {
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e9edf3;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.stat-card .k {
|
||||
font-size: 12px;
|
||||
color: #8b95a7;
|
||||
}
|
||||
|
||||
.stat-card .v {
|
||||
margin-top: 6px;
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.calendar-block {
|
||||
margin-bottom: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e9edf3;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.block-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: 14px;
|
||||
color: #1f2d3d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dot.work {
|
||||
background: #edf8ee;
|
||||
}
|
||||
|
||||
.dot.rest {
|
||||
background: #fff2e6;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__header {
|
||||
margin-bottom: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__nav-wrap,
|
||||
::v-deep .sub-tabs .el-tabs__nav-scroll,
|
||||
::v-deep .sub-tabs .el-tabs__nav {
|
||||
border: none !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__item {
|
||||
border-radius: 16px;
|
||||
margin-right: 8px;
|
||||
border: 1px solid #dbe3ef !important;
|
||||
background: #fff;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 12px;
|
||||
color: #4a5668;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__item:hover {
|
||||
color: #2f7df6;
|
||||
border-color: #b8d3ff !important;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__item.is-active {
|
||||
color: #2f7df6;
|
||||
background: #eef5ff;
|
||||
border-color: #bfd8ff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
::v-deep .sub-tabs .el-tabs__nav-wrap::after,
|
||||
::v-deep .sub-tabs .el-tabs__active-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.calendar-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.week-head {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
border: 1px solid #edf1f6;
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.week-head span {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
color: #7d8797;
|
||||
background: #fafcff;
|
||||
border-right: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.week-head span:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.month-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
border-left: 1px solid #edf1f6;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
height: 120px;
|
||||
padding: 8px;
|
||||
border-right: 1px solid #edf1f6;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.day-cell.other {
|
||||
background: #fafbfd;
|
||||
color: #c4cad4;
|
||||
}
|
||||
|
||||
.day-cell.cur.workday {
|
||||
background: #edf8ee;
|
||||
}
|
||||
|
||||
.day-cell.cur.restday {
|
||||
background: #fff2e6;
|
||||
}
|
||||
|
||||
.day-cell.is-today {
|
||||
box-shadow: inset 0 0 0 2px #7aa9ff;
|
||||
}
|
||||
|
||||
.day-cell.is-weekend .day-no {
|
||||
color: #5f84c9;
|
||||
}
|
||||
|
||||
.day-no {
|
||||
font-size: 11px;
|
||||
color: #8f99aa;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.task-count {
|
||||
font-size: 12px;
|
||||
color: #1f2d3d;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
font-size: 11px;
|
||||
color: #4c596d;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 10px;
|
||||
color: #9aa5b5;
|
||||
margin-top: 3px;
|
||||
}</style>
|
||||
1580
klp-ui/src/views/aps/index.vue
Normal file
1580
klp-ui/src/views/aps/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
486
klp-ui/src/views/aps/lineCapability/index.vue
Normal file
486
klp-ui/src/views/aps/lineCapability/index.vue
Normal file
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<div class="app-container line-capability-page" v-loading="loading">
|
||||
<el-row :gutter="12" class="mb12">
|
||||
<el-col :span="6"><div class="kpi-card"><div class="k">能力规则</div><div class="v">{{ total }}</div></div></el-col>
|
||||
<el-col :span="6"><div class="kpi-card"><div class="k">启用规则</div><div class="v">{{ enabledCount }}</div></div></el-col>
|
||||
<el-col :span="6"><div class="kpi-card"><div class="k">产线数量</div><div class="v">{{ lineTotal }}</div></div></el-col>
|
||||
<el-col :span="6"><div class="kpi-card"><div class="k">平均每小时产能</div><div class="v">{{ avgCapacityText }}</div></div></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="mb12">
|
||||
<div slot="header" class="card-title">产线管理</div>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="7">
|
||||
<el-input v-model="lineQuery.keyword" size="small" clearable placeholder="搜索产线名称/编号" @keyup.enter.native="loadLineList">
|
||||
<el-button slot="append" icon="el-icon-search" @click="loadLineList" />
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="17" class="tr">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openLineForm()">新增产线</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadLineList">刷新</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<KLPTable :data="lineList" max-height="260" @row-click="onLineRowClick">
|
||||
<el-table-column label="产线名称" prop="lineName" min-width="140" />
|
||||
<el-table-column label="产线编号" prop="lineCode" min-width="120" />
|
||||
<el-table-column label="日产能" prop="capacity" width="110" />
|
||||
<el-table-column label="单位" prop="unit" width="90" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click.stop="openLineForm(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" @click.stop="removeLine(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<div slot="header" class="card-title">产线能力设置</div>
|
||||
<el-form :inline="true" size="small" class="mb8">
|
||||
<el-form-item label="产线">
|
||||
<el-select v-model="queryParams.lineId" clearable filterable placeholder="全部产线" style="width: 220px" @change="handleQuery">
|
||||
<el-option v-for="line in lineList" :key="line.lineId" :label="line.lineName || line.lineCode || ('产线' + line.lineId)" :value="line.lineId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.isEnabled" clearable placeholder="全部" style="width: 120px" @change="handleQuery">
|
||||
<el-option :value="1" label="启用" />
|
||||
<el-option :value="0" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openCapabilityForm">新增能力</el-button>
|
||||
<el-button size="mini" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<KLPTable :data="lineCapabilityList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="45" />
|
||||
<el-table-column label="产线" prop="lineName" min-width="130" />
|
||||
<el-table-column label="产品" min-width="210" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ formatProduct(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工序" min-width="180" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ formatProcess(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="每小时产能" prop="capacityPerHour" width="120" />
|
||||
<el-table-column label="准备时长(分钟)" prop="setupMinutes" width="130" />
|
||||
<el-table-column label="优先级" prop="priority" width="90" />
|
||||
<el-table-column label="状态" prop="isEnabledName" width="90" />
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="openCapabilityForm(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" @click="removeCapability(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog :title="lineDialog.title" :visible.sync="lineDialog.visible" width="560px" append-to-body>
|
||||
<el-form ref="lineFormRef" :model="lineDialog.form" label-width="110px" :rules="lineRules">
|
||||
<el-form-item label="产线名称" prop="lineName"><el-input v-model="lineDialog.form.lineName" /></el-form-item>
|
||||
<el-form-item label="产线编号" prop="lineCode"><el-input v-model="lineDialog.form.lineCode" /></el-form-item>
|
||||
<el-form-item label="日产能"><el-input-number v-model="lineDialog.form.capacity" :min="0" :step="1" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="单位"><el-input v-model="lineDialog.form.unit" /></el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" :loading="buttonLoading" @click="submitLine">确定</el-button>
|
||||
<el-button @click="lineDialog.visible = false">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="720px" append-to-body>
|
||||
<el-form ref="capabilityFormRef" :model="form" :rules="rules" label-width="130px">
|
||||
<el-form-item label="产线" prop="lineId">
|
||||
<el-select v-model="form.lineId" filterable placeholder="请选择产线" style="width: 100%" @change="onFormLineChange">
|
||||
<el-option v-for="line in lineList" :key="line.lineId" :label="line.lineName || line.lineCode || ('产线' + line.lineId)" :value="line.lineId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品">
|
||||
<el-input :value="form.productDisplay" readonly style="width: calc(100% - 120px); margin-right: 8px" />
|
||||
<el-button @click="openProductSelector">选择</el-button>
|
||||
<el-button type="text" @click="clearProduct">清空</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="工序">
|
||||
<el-input :value="form.processDisplay" readonly style="width: calc(100% - 120px); margin-right: 8px" />
|
||||
<el-button @click="openProcessSelector">选择</el-button>
|
||||
<el-button type="text" @click="clearProcess">清空</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="每小时产能" prop="capacityPerHour"><el-input-number v-model="form.capacityPerHour" :min="0" :precision="2" :step="0.1" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="准备时长(分钟)"><el-input-number v-model="form.setupMinutes" :min="0" :step="1" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="优先级"><el-input-number v-model="form.priority" :min="1" :step="1" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="是否启用"><el-radio-group v-model="form.isEnabled"><el-radio :label="1">启用</el-radio><el-radio :label="0">停用</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" :loading="buttonLoading" @click="submitForm">确定</el-button>
|
||||
<el-button @click="open = false">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="选择产品" :visible.sync="productSelector.visible" width="980px" append-to-body>
|
||||
<el-form :inline="true" size="small">
|
||||
<el-form-item><el-input v-model="productSelector.keyword" placeholder="产品名称/编号" @keyup.enter.native="fetchProductOptions" /></el-form-item>
|
||||
<el-form-item><el-button type="primary" size="mini" @click="fetchProductOptions">搜索</el-button></el-form-item>
|
||||
</el-form>
|
||||
<KLPTable :data="productSelector.list" max-height="360">
|
||||
<el-table-column prop="productName" label="产品名称" min-width="150" />
|
||||
<el-table-column prop="productCode" label="产品编号" min-width="120" />
|
||||
<el-table-column prop="specification" label="规格" min-width="120" />
|
||||
<el-table-column prop="material" label="材质" min-width="100" />
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template slot-scope="scope"><el-button size="mini" type="text" @click="selectProduct(scope.row)">选择</el-button></template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
<pagination v-show="productSelector.total > 0" :total="productSelector.total" :page.sync="productSelector.pageNum" :limit.sync="productSelector.pageSize" @pagination="fetchProductOptions" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="选择工序" :visible.sync="processSelector.visible" width="900px" append-to-body>
|
||||
<el-form :inline="true" size="small">
|
||||
<el-form-item><el-input v-model="processSelector.keyword" placeholder="工序名称/编码" @keyup.enter.native="fetchProcessOptions" /></el-form-item>
|
||||
<el-form-item><el-button type="primary" size="mini" @click="fetchProcessOptions">搜索</el-button></el-form-item>
|
||||
</el-form>
|
||||
<KLPTable :data="processSelector.list" max-height="360">
|
||||
<el-table-column prop="processName" label="工序名称" min-width="150" />
|
||||
<el-table-column prop="processCode" label="工序编码" min-width="120" />
|
||||
<el-table-column prop="standardTime" label="标准工时" width="110" />
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template slot-scope="scope"><el-button size="mini" type="text" @click="selectProcess(scope.row)">选择</el-button></template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
<pagination v-show="processSelector.total > 0" :total="processSelector.total" :page.sync="processSelector.pageNum" :limit.sync="processSelector.pageSize" @pagination="fetchProcessOptions" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listLineCapability, getLineCapability, delLineCapability, addLineCapability, updateLineCapability } from '@/api/aps/aps'
|
||||
import { listProductionLine, addProductionLine, updateProductionLine, delProductionLine } from '@/api/wms/productionLine'
|
||||
import { listProduct } from '@/api/wms/product'
|
||||
import { listProcesse } from '@/api/wms/craft'
|
||||
|
||||
export default {
|
||||
name: 'ApsLineCapability',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
buttonLoading: false,
|
||||
total: 0,
|
||||
lineTotal: 0,
|
||||
ids: [],
|
||||
lineCapabilityList: [],
|
||||
lineList: [],
|
||||
lineQuery: { keyword: '' },
|
||||
queryParams: { pageNum: 1, pageSize: 20, lineId: undefined, isEnabled: undefined },
|
||||
open: false,
|
||||
title: '',
|
||||
form: {},
|
||||
lineDialog: { visible: false, title: '', form: {} },
|
||||
productSelector: { visible: false, keyword: '', pageNum: 1, pageSize: 10, total: 0, list: [] },
|
||||
processSelector: { visible: false, keyword: '', pageNum: 1, pageSize: 10, total: 0, list: [] },
|
||||
rules: {
|
||||
lineId: [{ required: true, message: '请选择产线', trigger: 'change' }],
|
||||
capacityPerHour: [{ required: true, message: '请输入每小时产能', trigger: 'blur' }]
|
||||
},
|
||||
lineRules: {
|
||||
lineName: [{ required: true, message: '请输入产线名称', trigger: 'blur' }],
|
||||
lineCode: [{ required: true, message: '请输入产线编号', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enabledCount() {
|
||||
return (this.lineCapabilityList || []).filter(i => Number(i.isEnabled) === 1).length
|
||||
},
|
||||
avgCapacityText() {
|
||||
if (!this.lineCapabilityList.length) return '-'
|
||||
const sum = this.lineCapabilityList.reduce((s, i) => s + Number(i.capacityPerHour || 0), 0)
|
||||
return (sum / this.lineCapabilityList.length).toFixed(2)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.resetFormData()
|
||||
this.loadLineList().then(() => this.getList())
|
||||
},
|
||||
methods: {
|
||||
formatProduct(row) {
|
||||
if (!row.productName) return '通用'
|
||||
return `${row.productName}${row.productCode ? '(' + row.productCode + ')' : ''}`
|
||||
},
|
||||
formatProcess(row) {
|
||||
if (!row.processName) return '通用'
|
||||
return `${row.processName}${row.processCode ? '(' + row.processCode + ')' : ''}`
|
||||
},
|
||||
resetFormData() {
|
||||
this.form = {
|
||||
capabilityId: undefined,
|
||||
lineId: undefined,
|
||||
productId: undefined,
|
||||
processId: undefined,
|
||||
productDisplay: '',
|
||||
processDisplay: '',
|
||||
capacityPerHour: undefined,
|
||||
setupMinutes: 0,
|
||||
priority: 999,
|
||||
isEnabled: 1,
|
||||
remark: ''
|
||||
}
|
||||
},
|
||||
async loadLineList() {
|
||||
const res = await listProductionLine({ pageNum: 1, pageSize: 500, lineName: this.lineQuery.keyword || undefined })
|
||||
this.lineList = res.rows || []
|
||||
this.lineTotal = res.total || this.lineList.length
|
||||
},
|
||||
onLineRowClick(row) {
|
||||
this.queryParams.lineId = row.lineId
|
||||
this.handleQuery()
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listLineCapability(this.queryParams).then(res => {
|
||||
this.lineCapabilityList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.queryParams = { pageNum: 1, pageSize: 20, lineId: undefined, isEnabled: undefined }
|
||||
this.getList()
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = (selection || []).map(i => i.capabilityId)
|
||||
},
|
||||
openCapabilityForm(row) {
|
||||
this.resetFormData()
|
||||
if (!row) {
|
||||
this.title = '新增产线能力'
|
||||
if (this.queryParams.lineId) this.form.lineId = this.queryParams.lineId
|
||||
this.open = true
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
getLineCapability(row.capabilityId).then(res => {
|
||||
const d = res.data || {}
|
||||
this.form = {
|
||||
...this.form,
|
||||
...d,
|
||||
productDisplay: d.productName ? `${d.productName}${d.productCode ? '(' + d.productCode + ')' : ''}` : '',
|
||||
processDisplay: d.processName ? `${d.processName}${d.processCode ? '(' + d.processCode + ')' : ''}` : ''
|
||||
}
|
||||
this.title = '修改产线能力'
|
||||
this.open = true
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
onFormLineChange(lineId) {
|
||||
const line = (this.lineList || []).find(i => String(i.lineId) === String(lineId))
|
||||
const cap = Number((line && line.capacity) || 0)
|
||||
if (cap > 0 && (!this.form.capacityPerHour || Number(this.form.capacityPerHour) <= 0)) {
|
||||
this.form.capacityPerHour = Number((cap / 24).toFixed(2))
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.capabilityFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.buttonLoading = true
|
||||
const payload = {
|
||||
capabilityId: this.form.capabilityId,
|
||||
lineId: this.form.lineId,
|
||||
productId: this.form.productId,
|
||||
processId: this.form.processId,
|
||||
capacityPerHour: this.form.capacityPerHour,
|
||||
setupMinutes: this.form.setupMinutes,
|
||||
priority: this.form.priority,
|
||||
isEnabled: this.form.isEnabled,
|
||||
remark: this.form.remark
|
||||
}
|
||||
const req = payload.capabilityId ? updateLineCapability(payload) : addLineCapability(payload)
|
||||
req.then(() => {
|
||||
this.$modal.msgSuccess(payload.capabilityId ? '修改成功' : '新增成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
}).finally(() => { this.buttonLoading = false })
|
||||
})
|
||||
},
|
||||
removeCapability(row) {
|
||||
this.$modal.confirm('确认删除该能力规则吗?').then(() => delLineCapability(row.capabilityId)).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
openLineForm(row) {
|
||||
this.lineDialog.visible = true
|
||||
this.lineDialog.title = row ? '修改产线' : '新增产线'
|
||||
this.lineDialog.form = row ? { ...row } : { lineName: '', lineCode: '', capacity: 0, unit: '' }
|
||||
},
|
||||
submitLine() {
|
||||
this.$refs.lineFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
const payload = { ...this.lineDialog.form }
|
||||
this.buttonLoading = true
|
||||
const req = payload.lineId ? updateProductionLine(payload) : addProductionLine(payload)
|
||||
req.then(() => {
|
||||
this.$modal.msgSuccess(payload.lineId ? '产线修改成功' : '产线新增成功')
|
||||
this.lineDialog.visible = false
|
||||
return this.loadLineList()
|
||||
}).then(() => this.getList()).finally(() => { this.buttonLoading = false })
|
||||
})
|
||||
},
|
||||
removeLine(row) {
|
||||
this.$modal.confirm('确认删除该产线吗?').then(() => delProductionLine(row.lineId)).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
return this.loadLineList()
|
||||
}).then(() => this.getList())
|
||||
},
|
||||
openProductSelector() {
|
||||
this.productSelector.visible = true
|
||||
this.productSelector.pageNum = 1
|
||||
this.fetchProductOptions()
|
||||
},
|
||||
fetchProductOptions() {
|
||||
listProduct({
|
||||
pageNum: this.productSelector.pageNum,
|
||||
pageSize: this.productSelector.pageSize,
|
||||
productName: this.productSelector.keyword || undefined,
|
||||
productCode: this.productSelector.keyword || undefined
|
||||
}).then(res => {
|
||||
this.productSelector.list = res.rows || []
|
||||
this.productSelector.total = res.total || 0
|
||||
})
|
||||
},
|
||||
selectProduct(row) {
|
||||
this.form.productId = row.productId
|
||||
this.form.productDisplay = `${row.productName || ''}${row.productCode ? '(' + row.productCode + ')' : ''}`
|
||||
this.productSelector.visible = false
|
||||
},
|
||||
clearProduct() {
|
||||
this.form.productId = undefined
|
||||
this.form.productDisplay = ''
|
||||
},
|
||||
openProcessSelector() {
|
||||
this.processSelector.visible = true
|
||||
this.processSelector.pageNum = 1
|
||||
this.fetchProcessOptions()
|
||||
},
|
||||
fetchProcessOptions() {
|
||||
listProcesse({
|
||||
pageNum: this.processSelector.pageNum,
|
||||
pageSize: this.processSelector.pageSize,
|
||||
processName: this.processSelector.keyword || undefined,
|
||||
processCode: this.processSelector.keyword || undefined
|
||||
}).then(res => {
|
||||
this.processSelector.list = res.rows || []
|
||||
this.processSelector.total = res.total || 0
|
||||
})
|
||||
},
|
||||
selectProcess(row) {
|
||||
this.form.processId = row.processId
|
||||
this.form.processDisplay = `${row.processName || ''}${row.processCode ? '(' + row.processCode + ')' : ''}`
|
||||
this.processSelector.visible = false
|
||||
},
|
||||
clearProcess() {
|
||||
this.form.processId = undefined
|
||||
this.form.processDisplay = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.line-capability-page {
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.line-capability-page > .el-row,
|
||||
.line-capability-page > .el-card {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.mb8 { margin-bottom: 8px; }
|
||||
.tr { text-align: right; }
|
||||
|
||||
.kpi-card {
|
||||
border: 1px solid #e9edf5;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 4px rgba(30, 41, 59, 0.04);
|
||||
}
|
||||
|
||||
.kpi-card .k {
|
||||
font-size: 12px;
|
||||
color: #7c8798;
|
||||
}
|
||||
|
||||
.kpi-card .v {
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
color: #344054;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.card-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 3px;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #4f8ff7;
|
||||
}
|
||||
|
||||
::v-deep .el-card {
|
||||
border: 1px solid #e9edf5;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(30, 41, 59, 0.04);
|
||||
}
|
||||
|
||||
::v-deep .el-card__header {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #eef2f8;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
::v-deep .el-table,
|
||||
::v-deep .el-table::before,
|
||||
::v-deep .el-table__fixed::before,
|
||||
::v-deep .el-table__fixed-right::before {
|
||||
border-color: #edf1f7;
|
||||
}
|
||||
|
||||
::v-deep .el-table th.is-leaf,
|
||||
::v-deep .el-table td {
|
||||
border-bottom: 1px solid #f1f4f9;
|
||||
}
|
||||
|
||||
::v-deep .el-table th {
|
||||
background: #fff;
|
||||
color: #5d6b82;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .el-table tbody tr:hover > td {
|
||||
background: #f9fbff !important;
|
||||
}
|
||||
</style>
|
||||
14
klp-ui/src/views/aps/processManage/index.vue
Normal file
14
klp-ui/src/views/aps/processManage/index.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<aps-plan-gantt />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApsPlanGantt from '@/views/aps/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ApsProcessManagePage',
|
||||
components: {
|
||||
ApsPlanGantt
|
||||
}
|
||||
}
|
||||
</script>
|
||||
756
klp-ui/src/views/aps/sheet.vue
Normal file
756
klp-ui/src/views/aps/sheet.vue
Normal file
@@ -0,0 +1,756 @@
|
||||
<template>
|
||||
<div class="aps-sheet-page excel-theme">
|
||||
<el-card class="sheet-header" shadow="never">
|
||||
<div class="sheet-header-row">
|
||||
<div class="sheet-actions">
|
||||
<el-button size="small" icon="el-icon-back" @click="$router.back()">返回</el-button>
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button type="success" size="small" icon="el-icon-check" :loading="saving" @click="handleSaveSupplement">保存补录</el-button>
|
||||
<el-button size="small" icon="el-icon-download" @click="handleExport">导出 Excel</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form :inline="true" size="small" :model="query" class="sheet-form">
|
||||
<el-form-item label="模板">
|
||||
<el-input v-model="currentTemplate.name" disabled style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="机组/产线">
|
||||
<el-select v-model="query.lineId" clearable filterable placeholder="全部">
|
||||
<el-option
|
||||
v-for="line in lineOptions"
|
||||
:key="line.lineId"
|
||||
:label="line.lineName || ('产线' + line.lineId)"
|
||||
:value="line.lineId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划编号">
|
||||
<el-input v-model="query.planId" placeholder="计划号/计划编码(可选)" clearable style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单编号">
|
||||
<el-input v-model="query.orderId" placeholder="订单号/订单编码(可选)" clearable style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="query.range"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
:default-time="['00:00:00', '23:59:59']"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="sheet-body" shadow="never">
|
||||
<div class="sheet-meta" v-if="sheet.header">
|
||||
<div class="meta-item"><span class="k">计划号:</span><span class="v">{{ sheet.header.planCode || '-' }}</span></div>
|
||||
<div class="meta-item"><span class="k">机组:</span><span class="v">{{ sheet.header.lineName || '-' }}</span></div>
|
||||
<div class="meta-item"><span class="k">日期:</span><span class="v">{{ sheet.header.dateText || '-' }}</span></div>
|
||||
<div class="meta-item"><span class="k">说明:</span><span class="v">点击“选择原料钢卷”弹窗按库区选卷</span></div>
|
||||
<div class="meta-item meta-item-warn"><span class="k">补录提示:</span><span class="v">原料钢卷、原料卷号、钢卷位置、包装要求、切边要求、镀层种类为优先补录项,已前置展示;已有值也可直接修改,修改后自动保存。</span></div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="sheet.rows"
|
||||
border
|
||||
size="mini"
|
||||
:height="tableHeight"
|
||||
class="excel-table"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="col in (currentTemplate.columns || [])"
|
||||
:key="col.prop || col.label"
|
||||
:label="col.label"
|
||||
:prop="col.prop"
|
||||
:width="col.width"
|
||||
:min-width="col.minWidth"
|
||||
:align="col.align || 'center'"
|
||||
:show-overflow-tooltip="col.showOverflowTooltip !== false"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<template v-if="col.prop === 'rawMaterialId'">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
class="coil-picker-btn"
|
||||
@click="openCoilPicker(scope.row)"
|
||||
>
|
||||
{{ scope.row.rawCoilNos || '选择原料钢卷' }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isEditableColumn(col.prop)">
|
||||
<el-input
|
||||
v-model="scope.row[col.prop]"
|
||||
size="mini"
|
||||
clearable
|
||||
class="excel-cell-input"
|
||||
:placeholder="`请输入${col.label}`"
|
||||
@change="onCellEdit(scope.row, col.prop)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ scope.row[col.prop] == null || scope.row[col.prop] === '' ? '-' : scope.row[col.prop] }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="sheet-summary" v-if="summaryText">
|
||||
<span class="k">合计:</span>
|
||||
<span class="v">{{ summaryText }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog title="选择原料钢卷" :visible.sync="coilPicker.visible" width="1280px" append-to-body>
|
||||
<el-form :inline="true" size="small" class="coil-picker-form">
|
||||
<el-form-item label="库区">
|
||||
<el-select v-model="coilPicker.selectedWarehouseId" clearable filterable placeholder="全部库区" style="width: 220px" @change="onCoilFilterChange">
|
||||
<el-option
|
||||
v-for="w in coilPicker.warehouseOptions"
|
||||
:key="w.actualWarehouseId"
|
||||
:label="w.actualWarehouseName"
|
||||
:value="w.actualWarehouseId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="入场钢卷号">
|
||||
<el-input v-model="coilPicker.enterCoilNo" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前钢卷号">
|
||||
<el-input v-model="coilPicker.currentCoilNo" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
|
||||
</el-form-item>
|
||||
<el-form-item label="厂家">
|
||||
<el-input v-model="coilPicker.manufacturer" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="mini" type="primary" @click="searchCoils">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="coil-stack-wrap" v-loading="coilPicker.loading">
|
||||
<el-empty v-if="!coilColumnKeys.length" description="暂无可选原料钢卷" :image-size="60" />
|
||||
<div v-else class="coil-bird-wrap">
|
||||
<div class="coil-legend">
|
||||
<span class="legend-item"><i class="dot layer1" />一层</span>
|
||||
<span class="legend-item"><i class="dot layer2" />二层</span>
|
||||
<span class="legend-item"><i class="dot occupied" />已占用</span>
|
||||
</div>
|
||||
<div class="coil-grid-scroll">
|
||||
<div class="coil-col-ruler">
|
||||
<div class="ruler-empty" />
|
||||
<div v-for="col in coilColumnKeys" :key="`ruler-${col}`" class="ruler-col">{{ col }}</div>
|
||||
</div>
|
||||
<div class="coil-grid-main">
|
||||
<div class="coil-row-ruler">
|
||||
<div v-for="row in coilMaxRow" :key="`row-${row}`" class="ruler-row">{{ row }}</div>
|
||||
</div>
|
||||
<div class="coil-grid-columns">
|
||||
<div v-for="col in coilColumnKeys" :key="`col-${col}`" class="coil-col-pair">
|
||||
<div class="coil-layer">
|
||||
<div
|
||||
v-for="row in coilMaxRow"
|
||||
:key="`l1-${col}-${row}`"
|
||||
class="coil-cell layer1"
|
||||
:class="{ occupied: !!(getCoilCell(col, row, 1) && getCoilCell(col, row, 1).rawMaterialId) }"
|
||||
@click="selectCoilCell(col, row, 1)"
|
||||
>
|
||||
<template v-if="getCoilCell(col, row, 1)">
|
||||
<div class="line1" :title="getCoilCell(col, row, 1).rawLocation">{{ getCoilCell(col, row, 1).rawLocation }}</div>
|
||||
<div class="line2" :title="getCoilCell(col, row, 1).rawMaterialCode">{{ getCoilCell(col, row, 1).rawMaterialCode || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coil-layer layer2-shift">
|
||||
<div
|
||||
v-for="row in (coilMaxRow - 1)"
|
||||
:key="`l2-${col}-${row}`"
|
||||
class="coil-cell layer2"
|
||||
:class="{ occupied: !!(getCoilCell(col, row, 2) && getCoilCell(col, row, 2).rawMaterialId) }"
|
||||
@click="selectCoilCell(col, row, 2)"
|
||||
>
|
||||
<template v-if="getCoilCell(col, row, 2)">
|
||||
<div class="line1" :title="getCoilCell(col, row, 2).rawLocation">{{ getCoilCell(col, row, 2).rawLocation }}</div>
|
||||
<div class="line2" :title="getCoilCell(col, row, 2).rawMaterialCode">{{ getCoilCell(col, row, 2).rawMaterialCode || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coil-picker-foot">共 {{ coilPicker.total || 0 }} 条钢卷(按库位分布展示)</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { APS_SHEET_TEMPLATES, getTemplateByKey } from './sheets/templates'
|
||||
import { listProductionLine } from '@/api/wms/productionLine'
|
||||
import { getMaterialCoilLocationGrid } from '@/api/wms/coil'
|
||||
import { treeActualWarehouseTwoLevel } from '@/api/wms/actualWarehouse'
|
||||
import { fetchScheduleSheet, exportScheduleSheet, saveScheduleSheetSupplement } from '@/api/aps/aps'
|
||||
import { parseTime } from '@/utils/klp'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export default {
|
||||
name: 'ApsScheduleSheet',
|
||||
data() {
|
||||
return {
|
||||
templates: APS_SHEET_TEMPLATES,
|
||||
lineOptions: [],
|
||||
rawMaterialOptions: [],
|
||||
loading: false,
|
||||
saving: false,
|
||||
autoSaveTimer: null,
|
||||
pendingAutoSave: false,
|
||||
dirtyOperationIds: [],
|
||||
baselineByOperationId: {},
|
||||
tableHeight: 520,
|
||||
coilPicker: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
currentRow: null,
|
||||
selectedWarehouseId: undefined,
|
||||
enterCoilNo: '',
|
||||
currentCoilNo: '',
|
||||
manufacturer: '',
|
||||
warehouseOptions: [],
|
||||
groupedCoils: [],
|
||||
coilGrid: {},
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
},
|
||||
query: {
|
||||
templateKey: 'unified',
|
||||
lineId: null,
|
||||
planId: '',
|
||||
orderId: '',
|
||||
range: []
|
||||
},
|
||||
sheet: {
|
||||
header: null,
|
||||
rows: [],
|
||||
summary: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentTemplate() {
|
||||
return getTemplateByKey(this.query.templateKey)
|
||||
},
|
||||
summaryText() {
|
||||
const s = this.sheet && this.sheet.summary
|
||||
if (!s) return ''
|
||||
const parts = []
|
||||
if (s.totalPieceCount != null) parts.push(`件数 ${s.totalPieceCount}`)
|
||||
if (s.totalWeightTon != null) parts.push(`重量 ${s.totalWeightTon}`)
|
||||
return parts.join(',')
|
||||
},
|
||||
coilColumnKeys() {
|
||||
const keys = Object.keys(this.coilPicker.coilGrid || {})
|
||||
return keys.map(v => Number(v)).filter(v => !Number.isNaN(v)).sort((a, b) => a - b)
|
||||
},
|
||||
coilMaxRow() {
|
||||
let max = 0
|
||||
Object.values(this.coilPicker.coilGrid || {}).forEach(col => {
|
||||
const l1 = col.layer1 || []
|
||||
const l2 = col.layer2 || []
|
||||
;[...l1, ...l2].forEach(item => {
|
||||
const row = Number(item._row || 0)
|
||||
if (row > max) max = row
|
||||
})
|
||||
})
|
||||
return max || 1
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.tableHeight = document.documentElement.clientHeight - 280
|
||||
this.initDefaultRange()
|
||||
Promise.all([this.loadLines(), this.loadRawMaterials()]).finally(() => {
|
||||
this.applyRouteQuery()
|
||||
this.handleQuery()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.autoSaveTimer) {
|
||||
clearTimeout(this.autoSaveTimer)
|
||||
this.autoSaveTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initDefaultRange() {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
|
||||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7, 23, 59, 59)
|
||||
this.query.range = [parseTime(start), parseTime(end)]
|
||||
},
|
||||
loadLines() {
|
||||
return listProductionLine({ pageNum: 1, pageSize: 999 }).then(res => {
|
||||
this.lineOptions = (res.rows || res.data || []).map(r => ({
|
||||
lineId: r.lineId,
|
||||
lineName: r.lineName || r.lineCode
|
||||
}))
|
||||
}).catch(() => {})
|
||||
},
|
||||
loadRawMaterials() {
|
||||
return this.loadCoilsByWarehouse()
|
||||
},
|
||||
loadWarehouseOptions() {
|
||||
return treeActualWarehouseTwoLevel().then(res => {
|
||||
const tree = res.data || []
|
||||
const leaf = []
|
||||
tree.forEach(p => {
|
||||
;(p.children || []).forEach(c => {
|
||||
leaf.push({
|
||||
actualWarehouseId: c.actualWarehouseId,
|
||||
actualWarehouseName: `${p.actualWarehouseName || ''}/${c.actualWarehouseName || ''}`
|
||||
})
|
||||
})
|
||||
})
|
||||
this.coilPicker.warehouseOptions = leaf
|
||||
}).catch(() => {
|
||||
this.coilPicker.warehouseOptions = []
|
||||
})
|
||||
},
|
||||
async loadCoilsByWarehouse() {
|
||||
if (!this.coilPicker.selectedWarehouseId) {
|
||||
this.coilPicker.coilGrid = {}
|
||||
this.rawMaterialOptions = []
|
||||
this.coilPicker.total = 0
|
||||
return
|
||||
}
|
||||
this.coilPicker.loading = true
|
||||
try {
|
||||
const gridRes = await getMaterialCoilLocationGrid({
|
||||
itemType: 'raw_material',
|
||||
actualWarehouseId: this.coilPicker.selectedWarehouseId,
|
||||
enterCoilNo: this.coilPicker.enterCoilNo || undefined,
|
||||
currentCoilNo: this.coilPicker.currentCoilNo || undefined,
|
||||
manufacturer: this.coilPicker.manufacturer || undefined
|
||||
})
|
||||
|
||||
const data = gridRes.data || {}
|
||||
const warehouses = data.warehouses || []
|
||||
const coils = (data.coils || []).map(item => ({
|
||||
rawMaterialId: item.coilId,
|
||||
rawMaterialCode: item.currentCoilNo || item.enterCoilNo || item.coilNo || '',
|
||||
enterCoilNo: item.enterCoilNo || '',
|
||||
manufacturer: item.manufacturer || '',
|
||||
rawMaterialName: item.itemName || item.materialName || '',
|
||||
specification: item.specification || '',
|
||||
coilWeight: item.netWeight,
|
||||
packageType: item.packagingRequirement || item.packageType,
|
||||
edgeType: item.trimmingRequirement || item.edgeType,
|
||||
zincLayer: item.coatingType || item.zincLayer,
|
||||
surfaceTreatmentDesc: item.surfaceTreatmentDesc,
|
||||
actualWarehouseId: item.actualWarehouseId,
|
||||
actualWarehouseName: item.actualWarehouseName || '未分配库位',
|
||||
rawLocation: item.actualWarehouseName || '未分配库位'
|
||||
}))
|
||||
this.rawMaterialOptions = coils
|
||||
this.coilPicker.total = coils.length
|
||||
|
||||
const coilByWarehouseId = {}
|
||||
coils.forEach(c => {
|
||||
if (c.actualWarehouseId && !coilByWarehouseId[c.actualWarehouseId]) {
|
||||
coilByWarehouseId[c.actualWarehouseId] = c
|
||||
}
|
||||
})
|
||||
|
||||
const grid = {}
|
||||
warehouses.forEach(w => {
|
||||
const code = String(w.actualWarehouseCode || '')
|
||||
const m = code.match(/^([A-Za-z0-9]{3})([^-]+)-(\d{2})-(\d+)$/)
|
||||
if (!m) return
|
||||
const col = Number(m[2])
|
||||
const row = Number(m[3])
|
||||
const layer = Number(m[4])
|
||||
if (!grid[col]) grid[col] = { layer1: [], layer2: [] }
|
||||
const coil = coilByWarehouseId[w.actualWarehouseId] || null
|
||||
const cell = {
|
||||
_col: col,
|
||||
_row: row,
|
||||
_layer: layer,
|
||||
actualWarehouseId: w.actualWarehouseId,
|
||||
actualWarehouseCode: w.actualWarehouseCode,
|
||||
rawLocation: w.actualWarehouseName || w.actualWarehouseCode,
|
||||
...(coil || {})
|
||||
}
|
||||
if (layer === 1) grid[col].layer1.push(cell)
|
||||
else if (layer === 2) grid[col].layer2.push(cell)
|
||||
})
|
||||
|
||||
Object.keys(grid).forEach(k => {
|
||||
grid[k].layer1.sort((a, b) => Number(a._row) - Number(b._row))
|
||||
grid[k].layer2.sort((a, b) => Number(a._row) - Number(b._row))
|
||||
})
|
||||
this.coilPicker.coilGrid = grid
|
||||
} finally {
|
||||
this.coilPicker.loading = false
|
||||
}
|
||||
},
|
||||
searchCoils() {
|
||||
this.loadCoilsByWarehouse()
|
||||
},
|
||||
onCoilFilterChange() {
|
||||
this.loadCoilsByWarehouse()
|
||||
},
|
||||
hasSelectedWarehouse() {
|
||||
return !!this.coilPicker.selectedWarehouseId
|
||||
},
|
||||
getCoilCell(col, row, layer) {
|
||||
const c = this.coilPicker.coilGrid[col]
|
||||
if (!c) return null
|
||||
const list = layer === 1 ? (c.layer1 || []) : (c.layer2 || [])
|
||||
return list.find(i => Number(i._row) === Number(row)) || null
|
||||
},
|
||||
selectCoilCell(col, row, layer) {
|
||||
const item = this.getCoilCell(col, row, layer)
|
||||
if (!item) return
|
||||
if (!item.rawMaterialId) {
|
||||
this.$message.warning('该库位当前无钢卷,请选择有钢卷的库位')
|
||||
return
|
||||
}
|
||||
this.pickRawMaterial(item)
|
||||
},
|
||||
rawMaterialLabel(item) {
|
||||
return `${item.rawMaterialCode || '-'} / ${item.rawMaterialName || '-'} / ${item.specification || '-'}`
|
||||
},
|
||||
applyRouteQuery() {
|
||||
const q = this.$route && this.$route.query ? this.$route.query : {}
|
||||
if (q.templateKey) this.query.templateKey = String(q.templateKey)
|
||||
if (q.planId) this.query.planId = String(q.planId)
|
||||
if (q.orderId) this.query.orderId = String(q.orderId)
|
||||
if (q.lineId) {
|
||||
const lineIdStr = String(q.lineId)
|
||||
const matched = (this.lineOptions || []).find(l => String(l.lineId) === lineIdStr)
|
||||
this.query.lineId = matched ? matched.lineId : lineIdStr
|
||||
}
|
||||
if (q.queryStart && q.queryEnd) this.query.range = [q.queryStart, q.queryEnd]
|
||||
},
|
||||
isEditableColumn(prop) {
|
||||
return ['rawCoilNos', 'rawNetWeight', 'rawPackaging', 'rawEdgeReq', 'rawCoatingType'].includes(prop)
|
||||
},
|
||||
normalizeSaveRow(r) {
|
||||
return {
|
||||
operationId: r.operationId,
|
||||
rawMaterialId: r.rawMaterialId || null,
|
||||
rawCoilNos: r.rawCoilNos || '',
|
||||
rawNetWeight: r.rawNetWeight === '' || r.rawNetWeight == null ? null : r.rawNetWeight,
|
||||
rawPackaging: r.rawPackaging || '',
|
||||
rawEdgeReq: r.rawEdgeReq || '',
|
||||
rawCoatingType: r.rawCoatingType || '',
|
||||
rawLocation: r.rawLocation || ''
|
||||
}
|
||||
},
|
||||
setBaselineFromRows(rows) {
|
||||
const map = {}
|
||||
;(rows || []).forEach(r => {
|
||||
if (r && r.operationId) {
|
||||
map[r.operationId] = this.normalizeSaveRow(r)
|
||||
}
|
||||
})
|
||||
this.baselineByOperationId = map
|
||||
this.dirtyOperationIds = []
|
||||
},
|
||||
markRowDirty(row) {
|
||||
if (!row || !row.operationId) return
|
||||
const id = row.operationId
|
||||
const curr = this.normalizeSaveRow(row)
|
||||
const baseline = this.baselineByOperationId[id] || {}
|
||||
const changed = JSON.stringify(curr) !== JSON.stringify(baseline)
|
||||
const ids = new Set(this.dirtyOperationIds || [])
|
||||
if (changed) ids.add(id)
|
||||
else ids.delete(id)
|
||||
this.dirtyOperationIds = Array.from(ids)
|
||||
},
|
||||
onCellEdit(row) {
|
||||
this.$forceUpdate()
|
||||
this.markRowDirty(row)
|
||||
this.scheduleAutoSave(row)
|
||||
},
|
||||
scheduleAutoSave(row) {
|
||||
if (!row || !row.operationId) return
|
||||
this.pendingAutoSave = true
|
||||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
||||
this.autoSaveTimer = setTimeout(() => {
|
||||
this.handleSaveSupplement(true)
|
||||
}, 900)
|
||||
},
|
||||
openCoilPicker(row) {
|
||||
this.coilPicker.currentRow = row
|
||||
this.coilPicker.visible = true
|
||||
Promise.all([this.loadWarehouseOptions()]).then(() => {
|
||||
if (!this.coilPicker.selectedWarehouseId && this.coilPicker.warehouseOptions.length) {
|
||||
this.coilPicker.selectedWarehouseId = this.coilPicker.warehouseOptions[0].actualWarehouseId
|
||||
this.$message.info('请先选择库区,再点击查询或直接选择库位钢卷')
|
||||
}
|
||||
this.loadCoilsByWarehouse()
|
||||
})
|
||||
},
|
||||
pickRawMaterial(item) {
|
||||
const row = this.coilPicker.currentRow
|
||||
if (!row) return
|
||||
if (!item || !item.rawMaterialId) {
|
||||
this.$message.warning('请选择有效钢卷')
|
||||
return
|
||||
}
|
||||
this.$set(row, 'rawMaterialId', item.rawMaterialId)
|
||||
this.$set(row, 'rawCoilNos', item.rawMaterialCode || item.rawCoilNos || item.rawMaterialName || '')
|
||||
this.$set(row, 'rawNetWeight', item.coilWeight != null ? item.coilWeight : (row.rawNetWeight || ''))
|
||||
this.$set(row, 'rawPackaging', item.packageType || item.rawPackaging || row.rawPackaging || '')
|
||||
this.$set(row, 'rawEdgeReq', item.edgeType || item.rawEdgeReq || row.rawEdgeReq || '')
|
||||
this.$set(row, 'rawCoatingType', item.zincLayer || item.surfaceTreatmentDesc || row.rawCoatingType || '')
|
||||
this.$set(row, 'rawLocation', item.rawLocation || '')
|
||||
this.coilPicker.visible = false
|
||||
this.$message.success('已选择钢卷并回填')
|
||||
this.$forceUpdate()
|
||||
this.markRowDirty(row)
|
||||
this.scheduleAutoSave(row)
|
||||
},
|
||||
handleRawMaterialChange(row, rawMaterialId) {
|
||||
const item = (this.rawMaterialOptions || []).find(r => r.rawMaterialId === rawMaterialId)
|
||||
if (!item) return
|
||||
row.rawMaterialId = item.rawMaterialId
|
||||
row.rawCoilNos = item.rawMaterialCode || item.rawCoilNos || item.rawMaterialName || ''
|
||||
row.rawNetWeight = item.coilWeight != null ? item.coilWeight : (row.rawNetWeight || '')
|
||||
row.rawPackaging = item.packageType || item.rawPackaging || row.rawPackaging || ''
|
||||
row.rawEdgeReq = item.edgeType || item.rawEdgeReq || row.rawEdgeReq || ''
|
||||
row.rawCoatingType = item.zincLayer || item.surfaceTreatmentDesc || row.rawCoatingType || ''
|
||||
row.rawLocation = item.rawLocation || row.rawLocation || ''
|
||||
this.$forceUpdate()
|
||||
this.markRowDirty(row)
|
||||
this.scheduleAutoSave(row)
|
||||
},
|
||||
buildSavePayload() {
|
||||
const dirtyIds = new Set(this.dirtyOperationIds || [])
|
||||
const rows = (this.sheet.rows || [])
|
||||
.filter(r => r && r.operationId && dirtyIds.has(r.operationId))
|
||||
.map(r => this.normalizeSaveRow(r))
|
||||
return { rows }
|
||||
},
|
||||
async handleSaveSupplement(isAuto = false) {
|
||||
const payload = this.buildSavePayload()
|
||||
if (!payload.rows.length) {
|
||||
if (!isAuto) this.$message.warning('暂无可保存的数据')
|
||||
return
|
||||
}
|
||||
if (this.saving) {
|
||||
this.pendingAutoSave = true
|
||||
return
|
||||
}
|
||||
const savedIds = payload.rows.map(r => r.operationId)
|
||||
this.saving = true
|
||||
try {
|
||||
await saveScheduleSheetSupplement(payload)
|
||||
payload.rows.forEach(r => {
|
||||
this.baselineByOperationId[r.operationId] = { ...r }
|
||||
})
|
||||
const dirtySet = new Set(this.dirtyOperationIds || [])
|
||||
savedIds.forEach(id => dirtySet.delete(id))
|
||||
this.dirtyOperationIds = Array.from(dirtySet)
|
||||
if (isAuto) {
|
||||
this.pendingAutoSave = false
|
||||
} else {
|
||||
this.$message.success('补录保存成功')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
if (this.pendingAutoSave) {
|
||||
this.pendingAutoSave = false
|
||||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
||||
this.autoSaveTimer = setTimeout(() => {
|
||||
this.handleSaveSupplement(true)
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleQuery() {
|
||||
if (!this.query.range || this.query.range.length !== 2) {
|
||||
this.$message.warning('请先选择时间范围')
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
templateKey: this.query.templateKey,
|
||||
lineId: this.query.lineId || undefined,
|
||||
planId: this.query.planId || undefined,
|
||||
orderId: this.query.orderId || undefined,
|
||||
queryStart: this.query.range[0],
|
||||
queryEnd: this.query.range[1]
|
||||
}
|
||||
this.loading = true
|
||||
fetchScheduleSheet(params)
|
||||
.then(res => {
|
||||
const data = res.data || res
|
||||
this.sheet = {
|
||||
header: data.header || null,
|
||||
rows: (data.rows || []).map(r => ({ ...r })),
|
||||
summary: data.summary || null
|
||||
}
|
||||
this.setBaselineFromRows(this.sheet.rows)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleExport() {
|
||||
if (!this.query.range || this.query.range.length !== 2) {
|
||||
this.$message.warning('请先选择时间范围')
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
templateKey: this.query.templateKey,
|
||||
lineId: this.query.lineId || undefined,
|
||||
planId: this.query.planId || undefined,
|
||||
orderId: this.query.orderId || undefined,
|
||||
queryStart: this.query.range[0],
|
||||
queryEnd: this.query.range[1]
|
||||
}
|
||||
this.loading = true
|
||||
exportScheduleSheet(params)
|
||||
.then(blob => {
|
||||
const filename = `aps_${this.query.templateKey}_${new Date().getTime()}.xlsx`
|
||||
saveAs(new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), filename)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.aps-sheet-page {
|
||||
padding: 8px;
|
||||
background: #f3f5f7;
|
||||
}
|
||||
|
||||
::v-deep .sheet-header.el-card,
|
||||
::v-deep .sheet-body.el-card {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sheet-header,
|
||||
.sheet-body {
|
||||
border: 1px solid #d3d7dc;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.sheet-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.sheet-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #4d4f53;
|
||||
}
|
||||
.meta-item .k {
|
||||
color: #8b9098;
|
||||
}
|
||||
.meta-item-warn {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff8e6;
|
||||
border: 1px solid #f7df9a;
|
||||
}
|
||||
.meta-item-warn .k,
|
||||
.meta-item-warn .v {
|
||||
color: #8a6d1d;
|
||||
}
|
||||
.sheet-summary {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.sheet-summary .k {
|
||||
color: #8b9098;
|
||||
}
|
||||
|
||||
::v-deep .excel-table {
|
||||
border: 1px solid #bfc5cc;
|
||||
}
|
||||
::v-deep .excel-table th.el-table__cell {
|
||||
background: #eef1f5;
|
||||
color: #3c4045;
|
||||
font-weight: 600;
|
||||
border-right: 1px solid #bfc5cc;
|
||||
border-bottom: 1px solid #bfc5cc;
|
||||
padding: 4px 0;
|
||||
}
|
||||
::v-deep .excel-table td.el-table__cell {
|
||||
border-right: 1px solid #d0d5db;
|
||||
border-bottom: 1px solid #d0d5db;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
::v-deep .excel-table .el-table__row:hover > td {
|
||||
background-color: #f5f9ff !important;
|
||||
}
|
||||
::v-deep .excel-table .current-row > td {
|
||||
background-color: #eaf2ff !important;
|
||||
}
|
||||
|
||||
::v-deep .excel-cell-input .el-input__inner,
|
||||
::v-deep .excel-cell-select .el-input__inner {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 6px;
|
||||
background: #fff;
|
||||
}
|
||||
::v-deep .excel-cell-input .el-input__inner:focus,
|
||||
::v-deep .excel-cell-select .el-input__inner:focus {
|
||||
border-color: #4a90e2;
|
||||
box-shadow: inset 0 0 0 1px #4a90e2;
|
||||
}
|
||||
|
||||
.coil-bird-wrap { border: 1px solid #ebeef5; border-radius: 6px; padding: 8px; }
|
||||
.coil-legend { display: flex; gap: 14px; font-size: 12px; color: #606266; margin-bottom: 8px; }
|
||||
.legend-item { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
||||
.dot.layer1 { background: #fff3e0; border: 1px solid #f5d7a1; }
|
||||
.dot.layer2 { background: #e8f5e9; border: 1px solid #b7ddb9; }
|
||||
.dot.occupied { background: #f6f8fb; border: 1px solid #d7dde5; }
|
||||
.coil-grid-scroll { overflow: auto; max-height: 460px; }
|
||||
.coil-col-ruler { display: flex; min-width: max-content; }
|
||||
.ruler-empty { width: 28px; flex: none; }
|
||||
.ruler-col { width: 138px; text-align: center; font-size: 12px; color: #606266; }
|
||||
.coil-grid-main { display: flex; min-width: max-content; }
|
||||
.coil-row-ruler { width: 28px; display: flex; flex-direction: column; }
|
||||
.ruler-row { height: 56px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #909399; }
|
||||
.coil-grid-columns { display: flex; gap: 8px; }
|
||||
.coil-col-pair { display: grid; grid-template-columns: 1fr 1fr; width: 180px; border: 1px solid #eef2f7; border-radius: 4px; overflow: visible; }
|
||||
.coil-layer { display: flex; flex-direction: column; }
|
||||
.coil-layer.layer2-shift { margin-top: 28px; }
|
||||
.coil-cell { height: 56px; border-bottom: 1px solid #eef2f7; border-right: 1px solid #eef2f7; padding: 4px; font-size: 11px; color: #909399; display: flex; align-items: center; justify-content: center; text-align: center; cursor: pointer; }
|
||||
.coil-cell.layer1 { background: #fff3e0; color: #e67e22; }
|
||||
.coil-cell.layer2 { background: #e8f5e9; color: #2e8b57; border-right: none; }
|
||||
.coil-cell.occupied { font-weight: 600; }
|
||||
.coil-cell .line1 { width: 100%; font-size: 11px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.coil-cell .line2 { width: 100%; font-size: 11px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.coil-picker-foot { margin-top: 8px; text-align: right; color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
43
klp-ui/src/views/aps/sheets/SheetColumns.vue
Normal file
43
klp-ui/src/views/aps/sheets/SheetColumns.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
/**
|
||||
* 递归渲染 el-table-column(支持多级表头)
|
||||
* 用法:
|
||||
* <sheet-columns :columns="template.columns" />
|
||||
*/
|
||||
export default {
|
||||
name: 'SheetColumns',
|
||||
functional: true,
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
render(h, ctx) {
|
||||
const cols = ctx.props.columns || []
|
||||
const renderCol = (col) => {
|
||||
const props = {
|
||||
label: col.label,
|
||||
prop: col.prop,
|
||||
width: col.width,
|
||||
minWidth: col.minWidth,
|
||||
fixed: col.fixed,
|
||||
align: col.align || 'center',
|
||||
showOverflowTooltip: col.showOverflowTooltip !== false
|
||||
}
|
||||
// 叶子列
|
||||
if (!col.children || col.children.length === 0) {
|
||||
return h('el-table-column', { props })
|
||||
}
|
||||
// 分组列
|
||||
return h(
|
||||
'el-table-column',
|
||||
{ props: { label: col.label, align: col.align || 'center' } },
|
||||
col.children.map(child => renderCol(child))
|
||||
)
|
||||
}
|
||||
return h('div', cols.map(c => renderCol(c)))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
42
klp-ui/src/views/aps/sheets/templates.js
Normal file
42
klp-ui/src/views/aps/sheets/templates.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 统一排产表:模板配置
|
||||
*
|
||||
* 设计目标:
|
||||
* - 用同一套 UI 渲染不同机组的“排产表样式”(多级表头/字段差异)
|
||||
* - 后端返回统一的 rowModel(行数据)后,前端仅做字段映射和展示
|
||||
*
|
||||
* 约定:
|
||||
* - columns 支持多级:{ label, children } 或叶子列 { label, prop, width, minWidth, fixed, align }
|
||||
* - summary:用于底部合计显示(哪些字段求和)
|
||||
*/
|
||||
|
||||
export const APS_SHEET_TEMPLATES = [
|
||||
{
|
||||
key: 'unified',
|
||||
name: '统一排产表',
|
||||
columns: [
|
||||
{ label: '产线', prop: 'lineName', minWidth: 120 },
|
||||
{ label: '计划号', prop: 'planCode', minWidth: 140 },
|
||||
{ label: '订单号', prop: 'orderCode', minWidth: 140 },
|
||||
{ label: '客户', prop: 'customerName', minWidth: 140 },
|
||||
{ label: '业务员', prop: 'salesman', width: 100 },
|
||||
{ label: '产品', prop: 'productName', minWidth: 140 },
|
||||
{ label: '原料钢卷', prop: 'rawMaterialId', minWidth: 220 },
|
||||
{ label: '原料卷号', prop: 'rawCoilNos', minWidth: 220 },
|
||||
{ label: '钢卷位置', prop: 'rawLocation', minWidth: 140 },
|
||||
{ label: '包装要求', prop: 'rawPackaging', minWidth: 140 },
|
||||
{ label: '切边要求', prop: 'rawEdgeReq', minWidth: 120 },
|
||||
{ label: '镀层种类', prop: 'rawCoatingType', width: 110 },
|
||||
{ label: '原料净重', prop: 'rawNetWeight', width: 110, align: 'right' },
|
||||
{ label: '计划数量', prop: 'planQty', width: 100, align: 'right' },
|
||||
{ label: '开始时间', prop: 'startTime', width: 170 },
|
||||
{ label: '结束时间', prop: 'endTime', width: 170 }
|
||||
],
|
||||
summary: { sumFields: ['planQty', 'rawNetWeight'] }
|
||||
}
|
||||
]
|
||||
|
||||
export function getTemplateByKey(key) {
|
||||
return APS_SHEET_TEMPLATES.find(t => t.key === key) || APS_SHEET_TEMPLATES[0]
|
||||
}
|
||||
|
||||
252
klp-ui/src/views/aps/shiftTemplate/index.vue
Normal file
252
klp-ui/src/views/aps/shiftTemplate/index.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
|
||||
<el-form-item label="班次编码" prop="shiftCode">
|
||||
<el-input v-model="queryParams.shiftCode" placeholder="请输入班次编码" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="班次名称" prop="shiftName">
|
||||
<el-input v-model="queryParams.shiftName" placeholder="请输入班次名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<KLPTable v-loading="loading" :data="shiftTemplateList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="班次编码" align="center" prop="shiftCode" width="120" />
|
||||
<el-table-column label="班次名称" align="center" prop="shiftName" width="150" />
|
||||
<el-table-column label="开始时间" align="center" prop="startTime" width="120" />
|
||||
<el-table-column label="结束时间" align="center" prop="endTime" width="120" />
|
||||
<el-table-column label="跨天" align="center" prop="crossDayName" width="80" />
|
||||
<el-table-column label="效率系数" align="center" prop="efficiencyRate" width="100" />
|
||||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 添加或修改班次模板对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="班次编码" prop="shiftCode">
|
||||
<el-input v-model="form.shiftCode" placeholder="请输入班次编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="班次名称" prop="shiftName">
|
||||
<el-input v-model="form.shiftName" placeholder="请输入班次名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-time-picker
|
||||
v-model="form.startTime"
|
||||
value-format="HH:mm:ss"
|
||||
placeholder="选择开始时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-time-picker
|
||||
v-model="form.endTime"
|
||||
value-format="HH:mm:ss"
|
||||
placeholder="选择结束时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跨天标识" prop="crossDay">
|
||||
<el-radio-group v-model="form.crossDay">
|
||||
<el-radio :label="0">不跨天</el-radio>
|
||||
<el-radio :label="1">跨天</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="效率系数" prop="efficiencyRate">
|
||||
<el-input-number v-model="form.efficiencyRate" :min="0" :max="2" :precision="2" :step="0.1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listShiftTemplate, getShiftTemplate, delShiftTemplate, addShiftTemplate, updateShiftTemplate, exportShiftTemplate } from "@/api/aps/aps";
|
||||
|
||||
export default {
|
||||
name: "ApsShiftTemplate",
|
||||
data() {
|
||||
return {
|
||||
buttonLoading: false,
|
||||
loading: true,
|
||||
ids: [],
|
||||
single: true,
|
||||
multiple: true,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
shiftTemplateList: [],
|
||||
title: "",
|
||||
open: false,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
shiftCode: undefined,
|
||||
shiftName: undefined
|
||||
},
|
||||
form: {},
|
||||
rules: {
|
||||
shiftCode: [{ required: true, message: "班次编码不能为空", trigger: "blur" }],
|
||||
shiftName: [{ required: true, message: "班次名称不能为空", trigger: "blur" }],
|
||||
startTime: [{ required: true, message: "开始时间不能为空", trigger: "change" }],
|
||||
endTime: [{ required: true, message: "结束时间不能为空", trigger: "change" }]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listShiftTemplate(this.queryParams).then(response => {
|
||||
this.shiftTemplateList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
reset() {
|
||||
this.form = {
|
||||
shiftId: undefined,
|
||||
shiftCode: undefined,
|
||||
shiftName: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
crossDay: 0,
|
||||
efficiencyRate: 1,
|
||||
remark: undefined
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.shiftId);
|
||||
this.single = selection.length !== 1;
|
||||
this.multiple = !selection.length;
|
||||
},
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加班次模板";
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const shiftId = row.shiftId || this.ids[0];
|
||||
getShiftTemplate(shiftId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改班次模板";
|
||||
});
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.shiftId != null) {
|
||||
updateShiftTemplate(this.form).then(() => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addShiftTemplate(this.form).then(() => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
const shiftIds = row.shiftId || this.ids;
|
||||
this.$modal.confirm('是否确认删除选中的班次模板数据?').then(() => {
|
||||
this.loading = true;
|
||||
return delShiftTemplate(shiftIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleExport() {
|
||||
this.download('aps/shift-template/export', {
|
||||
...this.queryParams
|
||||
}, `shift_template_${new Date().getTime()}.xlsx`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
::v-deep .el-table,
|
||||
::v-deep .el-table--border,
|
||||
::v-deep .el-table--group {
|
||||
border-color: #eef2f7;
|
||||
}
|
||||
|
||||
::v-deep .el-table td,
|
||||
::v-deep .el-table th.is-leaf {
|
||||
border-bottom: 1px solid #f3f5f8;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -40,12 +40,34 @@
|
||||
<dict-tag :options="dict.type.order_status" :value="item.orderStatus" />
|
||||
</template>
|
||||
|
||||
<!-- actions 插槽:el-button 为 Element UI Vue2 组件,用法不变 -->
|
||||
<!-- actions 插槽:操作区 -->
|
||||
<template slot="actions" slot-scope="{ item }">
|
||||
<el-button size="mini" plain title="预订单确认" type="success" icon="el-icon-check" v-if="isPre"
|
||||
@click.stop="handleStartProduction(item)"></el-button>
|
||||
<el-button size="small" type="text" style="color: red" icon="el-icon-delete"
|
||||
@click.stop="handleDelete(item)"></el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
plain
|
||||
title="预订单确认"
|
||||
type="success"
|
||||
icon="el-icon-check"
|
||||
v-if="isPre"
|
||||
@click.stop="handleStartProduction(item)"
|
||||
/>
|
||||
<el-button
|
||||
size="mini"
|
||||
plain
|
||||
type="primary"
|
||||
icon="el-icon-s-operation"
|
||||
title="进入排产中心"
|
||||
@click.stop="goApsSchedule(item)"
|
||||
>
|
||||
排产
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
style="color: red"
|
||||
icon="el-icon-delete"
|
||||
@click.stop="handleDelete(item)"
|
||||
/>
|
||||
</template>
|
||||
</klp-list>
|
||||
|
||||
@@ -64,6 +86,23 @@
|
||||
|
||||
<!-- 选中行时显示Tab详情 -->
|
||||
<div v-else>
|
||||
<!-- 订单详情操作区 -->
|
||||
<div class="order-detail-header">
|
||||
<div class="order-detail-title">
|
||||
当前订单:{{ form.orderCode || form.orderId }}
|
||||
</div>
|
||||
<div class="order-detail-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
icon="el-icon-magic-stick"
|
||||
:disabled="!form.orderId"
|
||||
@click="goApsOneKeySchedule"
|
||||
>
|
||||
排产
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab容器 -->
|
||||
<el-tabs v-loading="loading" v-model="activeTab" type="border-card" class="mt-2">
|
||||
<!-- 订单信息Tab -->
|
||||
@@ -460,6 +499,34 @@ export default {
|
||||
}).catch(() => {
|
||||
this.$modal.msgError("转化失败");
|
||||
});
|
||||
},
|
||||
|
||||
/** 跳转到 APS 排产中心,携带当前订单标识 */
|
||||
goApsSchedule(row) {
|
||||
if (!row || !row.orderId) {
|
||||
this.$modal.msgWarning("当前订单数据异常,无法进入排产");
|
||||
return;
|
||||
}
|
||||
this.$router
|
||||
.push({
|
||||
path: "/aps/index",
|
||||
query: { orderId: row.orderId, orderCode: row.orderCode }
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
/** 从订单详情一键排产并查看结果 */
|
||||
goApsOneKeySchedule() {
|
||||
if (!this.form || !this.form.orderId) {
|
||||
this.$modal.msgWarning("请先选择需要排产的订单");
|
||||
return;
|
||||
}
|
||||
this.$router
|
||||
.push({
|
||||
path: "/aps/index",
|
||||
query: { orderId: this.form.orderId, orderCode: this.form.orderCode, autoOneKey: "1" }
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -485,6 +552,17 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.order-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.order-detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
::v-deep .el-input-group__append,
|
||||
::v-deep .el-input-group__prepend {
|
||||
width: 20px !important;
|
||||
|
||||
Reference in New Issue
Block a user