feat(linkage): 移动改为任意入口位置选择,仅上卷鞍座触发生产

- 计划新增 position 字段;新增 /plan/{id}/move?position=… 与 /plan/positions/all
- line_service.place_at_position:放到任意位置(位置唯一占用),上卷鞍座单独触发生产联动
- 入口跟踪:新增入口位置图(单一鞍座)显示占位;移动按钮弹出位置选择框
- 计划管理:移动按钮同样弹出位置选择框

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 14:10:16 +08:00
parent 9fb3dcb785
commit 03709da1ca
8 changed files with 233 additions and 68 deletions

View File

@@ -78,8 +78,8 @@
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
<td @click.stop>
<span class="action-link" @click="openDialog(row)">编辑</span>
<span v-if="row.status === 'online'"
class="action-link" style="color:var(--accent-green)" @click="moveToProducing(row)">移动</span>
<span v-if="row.status === 'online' || row.status === 'ready'"
class="action-link" style="color:var(--accent-green)" @click="openMove(row)">移动</span>
</td>
</tr>
<tr v-if="!tableData.length && !loading">
@@ -239,11 +239,42 @@
</div>
</div>
</div>
<!-- 移动-位置选择弹窗 -->
<div v-if="moveDialog.visible" class="modal-mask" @click.self="moveDialog.visible=false">
<div class="modal-box" style="width:480px;">
<div class="modal-header">
移动计划 {{ moveDialog.plan && (moveDialog.plan.cold_coil_no || moveDialog.plan.plan_no) }}
<span class="modal-close" @click="moveDialog.visible=false"></span>
</div>
<div class="modal-body">
<div class="kv-label" style="margin-bottom:8px;">选择目标位置只有上卷鞍座会触发生产</div>
<div class="pos-pick">
<span
v-for="pos in positions"
:key="pos"
:class="['pick-item', { active: moveDialog.target === pos, saddle: pos === '上卷鞍座' }]"
@click="moveDialog.target = pos"
>{{ pos }}</span>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="moveDialog.visible=false">取消</button>
<button class="btn btn-primary" :disabled="!moveDialog.target || moving" @click="confirmMove">{{ moving ? '移动中...' : '确定' }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getPlans, createPlan, updatePlan, confirmPlan as apiConfirm, startProducing, getLastPlanTemplate } from '@/api'
import { getPlans, createPlan, updatePlan, confirmPlan as apiConfirm, movePlan, getLastPlanTemplate } from '@/api'
const SADDLE = '上卷鞍座'
const POSITIONS = [
'1#上卷小车', '2#上卷小车', '1#称重位', '2#称重位',
'1#地辊', '2#地辊', '1#倒卷小车', '2#倒卷小车', SADDLE,
]
const STATUS_MAP = {
ready: { label: '准备好', badge: 'badge-gray' },
@@ -269,6 +300,8 @@ export default {
dialogVisible: false, editRow: null,
form: this.emptyForm(),
selectedRow: null,
positions: POSITIONS, moving: false,
moveDialog: { visible: false, plan: null, target: '' },
}
},
created() { this.fetchData() },
@@ -348,15 +381,21 @@ export default {
this.$message.success('已上线')
this.fetchData()
},
async moveToProducing(row) {
if (!confirm(`将计划 ${row.cold_coil_no || row.plan_no} 移动到上卷鞍座?`)) return
openMove(row) {
this.moveDialog = { visible: true, plan: row, target: row.position || '' }
},
async confirmMove() {
const { plan, target } = this.moveDialog
if (!target) return
this.moving = true
try {
await startProducing(row.id)
this.$message.success('已移动到上卷鞍座')
await movePlan(plan.id, target)
this.$message.success(target === SADDLE ? '已移动到上卷鞍座' : `已移动到 ${target}`)
this.moveDialog.visible = false
this.fetchData()
} catch (e) {
this.$message.error(e?.response?.data?.detail || '操作失败')
}
} finally { this.moving = false }
},
async save() {
if (!this.form.plan_no) { this.$message.error('计划号不能为空'); return }
@@ -405,4 +444,13 @@ export default {
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
.modal-body { padding: 16px; overflow-y: auto; }
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
.pos-pick { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.pick-item {
padding: 9px 12px; font-size: 12px; text-align: center; border-radius: 6px; cursor: pointer;
border: 1px solid $border; color: $text-secondary; background: $bg-card;
&:hover { border-color: $sms-teal; color: $sms-teal; }
&.active { color: #fff; background: $sms-teal; border-color: $sms-teal; }
&.saddle { grid-column: 1 / -1; border-style: dashed; border-color: $accent-yellow; color: $accent-yellow;
&.active { color: #fff; background: $accent-yellow; border-style: solid; } }
}
</style>