513 lines
22 KiB
Vue
513 lines
22 KiB
Vue
|
|
<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>
|