完成排产(测试过了)
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user