<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="utf-8">
|
<title>自由画布地图编辑器</title>
|
<meta name="renderer" content="webkit">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<link rel="stylesheet" href="../../static/vue/element/element.css">
|
<link rel="stylesheet" href="../../static/css/cool.css">
|
<style>
|
:root {
|
--page-bg:
|
radial-gradient(1180px 540px at -10% -16%, rgba(24, 113, 181, 0.14), transparent 58%),
|
radial-gradient(920px 480px at 110% -12%, rgba(14, 148, 136, 0.12), transparent 56%),
|
linear-gradient(180deg, #eef4f9 0%, #f8fbfd 100%);
|
--card-bg: rgba(255, 255, 255, 0.94);
|
--card-border: rgba(216, 226, 238, 0.96);
|
--text-main: #213448;
|
--text-sub: #63788e;
|
--primary: #2f79d6;
|
--accent: #169a82;
|
--warn: #f08a3c;
|
--danger: #d85a5a;
|
}
|
|
[v-cloak] { display: none; }
|
|
html, body {
|
margin: 0;
|
height: 100%;
|
font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
|
color: var(--text-main);
|
background: var(--page-bg);
|
overflow: hidden;
|
}
|
|
.editor-shell {
|
width: 100%;
|
height: 100vh;
|
margin: 0 auto;
|
padding: 8px;
|
box-sizing: border-box;
|
display: flex;
|
flex-direction: column;
|
gap: 0;
|
}
|
|
.panel-card {
|
border-radius: 24px;
|
border: 1px solid var(--card-border);
|
background: var(--card-bg);
|
box-shadow: 0 16px 32px rgba(39, 62, 92, 0.08);
|
}
|
|
.workspace {
|
min-height: 0;
|
flex: 1 1 auto;
|
display: flex;
|
}
|
|
.panel-card {
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
}
|
|
.panel-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 10px;
|
padding: 18px 20px 14px;
|
border-bottom: 1px solid rgba(221, 230, 239, 0.94);
|
}
|
|
.panel-head h2 {
|
margin: 0;
|
font-size: 16px;
|
font-weight: 700;
|
}
|
|
.panel-body {
|
padding: 16px 18px 18px;
|
display: flex;
|
flex-direction: column;
|
gap: 14px;
|
min-height: 0;
|
overflow: auto;
|
}
|
|
.tool-section,
|
.status-stack,
|
.action-list {
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
}
|
|
.tool-section-label {
|
font-size: 12px;
|
color: var(--text-sub);
|
letter-spacing: 0.08em;
|
text-transform: uppercase;
|
}
|
|
.tool-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 8px;
|
}
|
|
.tool-card-btn {
|
appearance: none;
|
border: 1px solid rgba(193, 205, 219, 0.9);
|
background: rgba(255, 255, 255, 0.96);
|
border-radius: 14px;
|
padding: 10px 12px;
|
text-align: left;
|
cursor: pointer;
|
transition: all 0.18s ease;
|
color: var(--text-main);
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
min-height: 68px;
|
}
|
|
.tool-card-btn.active {
|
border-color: rgba(55, 127, 212, 0.45);
|
background: rgba(235, 244, 253, 0.98);
|
box-shadow: 0 8px 18px rgba(47, 121, 214, 0.14);
|
}
|
|
.tool-card-btn strong {
|
font-size: 13px;
|
font-weight: 700;
|
}
|
|
.tool-card-btn span {
|
font-size: 12px;
|
color: var(--text-sub);
|
line-height: 1.5;
|
}
|
|
.status-card,
|
.selection-summary,
|
.note-card {
|
border-radius: 16px;
|
border: 1px solid rgba(218, 227, 236, 0.92);
|
background: rgba(248, 251, 254, 0.92);
|
padding: 12px 14px;
|
}
|
|
.status-card strong,
|
.selection-summary strong,
|
.note-card strong {
|
display: block;
|
font-size: 13px;
|
margin-bottom: 6px;
|
}
|
|
.status-card span,
|
.selection-summary span,
|
.note-card span {
|
display: block;
|
font-size: 12px;
|
color: var(--text-sub);
|
line-height: 1.65;
|
}
|
|
.selection-summary strong {
|
font-size: 14px;
|
color: var(--text-main);
|
}
|
|
.canvas-toolbar {
|
padding: 16px 18px 14px;
|
border-bottom: 1px solid rgba(221, 230, 239, 0.94);
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
|
.canvas-toolbar-main {
|
flex: 1 1 420px;
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
}
|
|
.canvas-toolbar-title {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.canvas-toolbar-title h1 {
|
margin: 0;
|
font-size: 24px;
|
font-weight: 700;
|
letter-spacing: 0.3px;
|
}
|
|
.canvas-toolbar-title span {
|
font-size: 13px;
|
line-height: 1.65;
|
color: var(--text-sub);
|
}
|
|
.canvas-toolbar-meta,
|
.canvas-toolbar-actions {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.canvas-toolbar-actions {
|
justify-content: flex-end;
|
flex: 0 1 760px;
|
}
|
|
.canvas-toolbar-actions .el-input__inner,
|
.canvas-toolbar-actions .el-button {
|
border-radius: 10px;
|
}
|
|
.canvas-meta {
|
font-size: 12px;
|
color: var(--text-sub);
|
}
|
|
.canvas-card {
|
flex: 1 1 auto;
|
min-width: 0;
|
min-height: 0;
|
}
|
|
.canvas-wrap {
|
position: relative;
|
flex: 1 1 auto;
|
min-height: 0;
|
background:
|
linear-gradient(180deg, rgba(245, 249, 252, 0.95) 0%, rgba(251, 252, 254, 0.98) 100%);
|
}
|
|
.canvas-stage {
|
position: absolute;
|
inset: 0;
|
overflow: hidden;
|
background: #f6f9fc;
|
}
|
|
.canvas-host {
|
position: absolute;
|
inset: 0;
|
background: #f6f9fc;
|
}
|
|
.canvas-overlay-layer {
|
position: absolute;
|
inset: 0;
|
pointer-events: none;
|
z-index: 5;
|
}
|
|
.canvas-loading-mask {
|
position: absolute;
|
inset: 0;
|
z-index: 4;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: rgba(246, 249, 252, 0.82);
|
backdrop-filter: blur(2px);
|
}
|
|
.canvas-loading-card {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 8px;
|
min-width: 220px;
|
padding: 18px 22px;
|
border-radius: 18px;
|
background: rgba(255, 255, 255, 0.96);
|
border: 1px solid rgba(55, 127, 212, 0.16);
|
box-shadow: 0 18px 40px rgba(66, 94, 136, 0.12);
|
color: var(--text-main);
|
}
|
|
.canvas-loading-card strong {
|
font-size: 18px;
|
}
|
|
.canvas-loading-card span {
|
font-size: 13px;
|
color: var(--text-sub);
|
}
|
|
.overlay-panel {
|
position: absolute;
|
top: 14px;
|
bottom: 14px;
|
width: 300px;
|
display: flex;
|
flex-direction: column;
|
border-radius: 22px;
|
border: 1px solid rgba(214, 224, 236, 0.96);
|
background: #ffffff;
|
box-shadow: 0 8px 18px rgba(31, 55, 82, 0.06);
|
overflow: hidden;
|
pointer-events: auto;
|
contain: layout paint;
|
}
|
|
.overlay-left {
|
left: 14px;
|
}
|
|
.overlay-right {
|
right: 14px;
|
width: 340px;
|
}
|
|
.overlay-panel.collapsed {
|
width: 68px;
|
bottom: auto;
|
}
|
|
.overlay-panel.collapsed .panel-body {
|
display: none;
|
}
|
|
.overlay-panel.collapsed .panel-head {
|
align-items: flex-start;
|
padding: 12px 10px;
|
}
|
|
.overlay-panel.collapsed .panel-head h2 {
|
writing-mode: vertical-rl;
|
text-orientation: mixed;
|
font-size: 14px;
|
line-height: 1;
|
}
|
|
.overlay-panel.collapsed .canvas-meta {
|
display: none;
|
}
|
|
.panel-head-actions {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.panel-toggle {
|
appearance: none;
|
border: 1px solid rgba(190, 203, 217, 0.96);
|
background: rgba(255, 255, 255, 0.96);
|
color: var(--text-main);
|
width: 30px;
|
height: 30px;
|
border-radius: 10px;
|
cursor: pointer;
|
font-size: 14px;
|
font-weight: 700;
|
line-height: 1;
|
}
|
|
.panel-toggle:hover {
|
border-color: rgba(55, 127, 212, 0.4);
|
color: var(--primary);
|
}
|
|
.prop-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 10px;
|
}
|
|
.prop-grid .span-2 {
|
grid-column: span 2;
|
}
|
|
.field-stack {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.field-label {
|
font-size: 12px;
|
color: var(--text-sub);
|
line-height: 1.4;
|
}
|
|
.field-required {
|
color: #d85b52;
|
font-weight: 700;
|
}
|
|
.field-help {
|
font-size: 12px;
|
color: var(--text-sub);
|
line-height: 1.6;
|
}
|
|
.direction-grid {
|
display: grid;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
gap: 8px;
|
}
|
|
.direction-chip {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
gap: 6px;
|
min-height: 34px;
|
border: 1px solid rgba(55, 127, 212, 0.18);
|
border-radius: 12px;
|
background: #fff;
|
color: var(--text-main);
|
cursor: pointer;
|
transition: all 0.16s ease;
|
}
|
|
.direction-chip:hover {
|
border-color: rgba(55, 127, 212, 0.45);
|
color: var(--primary);
|
}
|
|
.direction-chip.active {
|
border-color: rgba(55, 127, 212, 0.85);
|
background: rgba(85, 145, 227, 0.12);
|
color: var(--primary);
|
box-shadow: inset 0 0 0 1px rgba(85, 145, 227, 0.1);
|
}
|
|
.direction-arrow {
|
font-size: 16px;
|
font-weight: 700;
|
line-height: 1;
|
}
|
|
.check-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 8px 12px;
|
}
|
|
.json-box {
|
font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
}
|
|
.footer-note {
|
font-size: 12px;
|
color: var(--text-sub);
|
line-height: 1.75;
|
}
|
|
@media (max-width: 1380px) {
|
.overlay-left {
|
width: 260px;
|
}
|
|
.overlay-right {
|
width: 300px;
|
}
|
}
|
|
@media (max-width: 980px) {
|
.canvas-card {
|
min-height: 0;
|
}
|
|
.canvas-wrap {
|
min-height: 0;
|
}
|
|
.overlay-left,
|
.overlay-right {
|
width: min(280px, calc(100% - 28px));
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<div id="app" class="editor-shell" v-cloak>
|
<section class="workspace">
|
<main class="panel-card canvas-panel canvas-card">
|
<div class="canvas-toolbar">
|
<div class="canvas-toolbar-main">
|
<div class="canvas-toolbar-title">
|
<h1>PixiJS 自由画布地图编辑器</h1>
|
<span>编辑态使用自由画布 JSON,保存时再编译成现有运行地图,所以 `MapCanvas` 和后端算法继续只读 `BasMap.data`。</span>
|
</div>
|
<div class="canvas-toolbar-meta">
|
<span class="canvas-meta">楼层: {{ currentLev ? currentLev + 'F' : '--' }}</span>
|
<span class="canvas-meta">缩放: {{ viewPercent }}%</span>
|
<span class="canvas-meta">模式: {{ toolLabel(activeTool) }}</span>
|
<span class="canvas-meta">选中: {{ selectedIds.length }}</span>
|
<span class="canvas-meta">元素: {{ doc ? doc.elements.length : 0 }}</span>
|
<span class="canvas-meta">画布: {{ Math.round(doc ? doc.canvasWidth : 0) }} x {{ Math.round(doc ? doc.canvasHeight : 0) }}</span>
|
<span class="canvas-meta">渲染倍率: {{ formatNumber(pixiResolution) }}x</span>
|
<span class="canvas-meta">FPS: {{ fpsText }}</span>
|
</div>
|
</div>
|
<div class="canvas-toolbar-actions">
|
<el-select v-model="floorPickerLev" size="small" placeholder="选择楼层" @change="handleFloorChange" style="width: 120px;">
|
<el-option v-for="lev in levOptions" :key="'lev-' + lev" :label="lev + 'F'" :value="lev"></el-option>
|
</el-select>
|
<el-button size="small" plain @click="openBlankDialog">新建自由画布</el-button>
|
<el-button size="small" plain @click="triggerImportExcel">导入 Excel</el-button>
|
<el-button size="small" plain @click="triggerImportMap">导入地图</el-button>
|
<el-button size="small" plain @click="exportMapPackage">导出地图</el-button>
|
<el-button size="small" plain @click="loadCurrentFloor">重新读取</el-button>
|
<el-button size="small" plain @click="fitContent">适配全图</el-button>
|
<el-button size="small" plain @click="resetView">回到画布</el-button>
|
<el-button size="small" @click="undo" :disabled="undoStack.length === 0">撤销</el-button>
|
<el-button size="small" @click="redo" :disabled="redoStack.length === 0">重做</el-button>
|
<el-button size="small" type="primary" plain :loading="savingAll" :disabled="dirtyDraftCount === 0 || saving" @click="saveAllDocs">保存全部楼层<span v-if="dirtyDraftCount > 0">({{ dirtyDraftCount }})</span></el-button>
|
<el-button size="small" type="primary" :loading="saving" :disabled="savingAll" @click="saveDoc">保存当前楼层</el-button>
|
</div>
|
</div>
|
<div class="canvas-wrap">
|
<div class="canvas-stage" ref="canvasStage">
|
<div class="canvas-host" ref="canvasHost"></div>
|
<div v-if="loadingFloor" class="canvas-loading-mask">
|
<div class="canvas-loading-card">
|
<strong>正在加载 {{ switchingFloorLev || floorPickerLev || currentLev || '--' }}F</strong>
|
<span>画布和缓存正在切换,请稍候。</span>
|
</div>
|
</div>
|
</div>
|
<div class="canvas-overlay-layer">
|
<aside class="overlay-panel overlay-left" :class="{ collapsed: toolPanelCollapsed }">
|
<div class="panel-head">
|
<h2>工具面板</h2>
|
<div class="panel-head-actions">
|
<span class="canvas-meta">{{ toolLabel(activeTool) }}</span>
|
<button type="button" class="panel-toggle" @click="toggleToolPanel">{{ toolPanelCollapsed ? '>' : '<' }}</button>
|
</div>
|
</div>
|
<div class="panel-body">
|
<div class="tool-section">
|
<div class="tool-section-label">交互</div>
|
<div class="tool-grid">
|
<button
|
v-for="tool in interactionTools"
|
:key="tool.key"
|
type="button"
|
class="tool-card-btn"
|
:class="{ active: activeTool === tool.key }"
|
@click="setTool(tool.key)">
|
<strong>{{ tool.label }}</strong>
|
<span>{{ tool.desc }}</span>
|
</button>
|
</div>
|
</div>
|
|
<div class="tool-section">
|
<div class="tool-section-label">绘制元素</div>
|
<div class="tool-grid">
|
<button
|
v-for="tool in drawTools"
|
:key="tool.key"
|
type="button"
|
class="tool-card-btn"
|
:class="{ active: activeTool === tool.key }"
|
@click="setTool(tool.key)">
|
<strong>{{ tool.label }}</strong>
|
<span>{{ tool.desc }}</span>
|
</button>
|
</div>
|
</div>
|
|
<div class="tool-section">
|
<div class="tool-section-label">编辑动作</div>
|
<div class="action-list">
|
<el-button size="small" plain @click="copySelection" :disabled="selectedIds.length === 0">复制</el-button>
|
<el-button size="small" plain @click="pasteClipboard" :disabled="clipboard.length === 0">粘贴</el-button>
|
<el-button size="small" plain @click="duplicateSelection" :disabled="selectedIds.length === 0">复制偏移</el-button>
|
<el-button size="small" plain @click="fitSelection" :disabled="selectedIds.length === 0">聚焦选中</el-button>
|
<el-button size="small" type="danger" plain @click="deleteSelection" :disabled="selectedIds.length === 0">删除选中</el-button>
|
</div>
|
</div>
|
|
<div class="status-stack">
|
<div class="status-card">
|
<strong>快捷键</strong>
|
<span>`Delete` 删除,`Ctrl/Cmd + Z` 撤销,`Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y` 重做。</span>
|
<span>`Ctrl/Cmd + C / V` 复制粘贴,按住空格可临时拖动画布,`Shift + 点击` 可增减单个选中。</span>
|
<span>`阵列` 工具: 先选中一个货架 / 轨道模板,再拖一条水平或竖直线自动补齐一排;货架会按 `排-列` 规则继续编号。</span>
|
</div>
|
<div class="status-card">
|
<strong>当前状态</strong>
|
<span>楼层: {{ currentLev ? currentLev + 'F' : '--' }}</span>
|
<span>指针: {{ pointerStatus }}</span>
|
<span v-if="arrayPreviewCount > 0">阵列预览: 将生成 {{ arrayPreviewCount }} 个</span>
|
<span>未保存: {{ isDirty ? '是' : '否' }}</span>
|
</div>
|
<div class="note-card">
|
<strong>运行边界</strong>
|
<span>画布里是自由拖拉拽,但运行侧仍只接受轴对齐矩形元素。保存时会编译回当前运行地图,不支持斜线、旋转和任意多边形。</span>
|
</div>
|
</div>
|
</div>
|
</aside>
|
|
<aside class="overlay-panel overlay-right" :class="{ collapsed: inspectorPanelCollapsed }">
|
<div class="panel-head">
|
<h2>属性面板</h2>
|
<div class="panel-head-actions">
|
<span class="canvas-meta" v-if="singleSelectedElement">{{ singleSelectedElement.type }}</span>
|
<span class="canvas-meta" v-else>{{ selectedIds.length > 1 ? '多选' : '画布' }}</span>
|
<button type="button" class="panel-toggle" @click="toggleInspectorPanel">{{ inspectorPanelCollapsed ? '<' : '>' }}</button>
|
</div>
|
</div>
|
<div class="panel-body">
|
<div class="selection-summary">
|
<strong v-if="singleSelectedElement">单元素编辑</strong>
|
<strong v-else-if="selectedIds.length > 1">多选编辑</strong>
|
<strong v-else>未选中元素</strong>
|
<span v-if="singleSelectedElement">位置 {{ formatNumber(singleSelectedElement.x) }}, {{ formatNumber(singleSelectedElement.y) }} | 尺寸 {{ formatNumber(singleSelectedElement.width) }} x {{ formatNumber(singleSelectedElement.height) }}</span>
|
<span v-else-if="selectedIds.length > 1">当前已选 {{ selectedIds.length }} 个元素,可整体移动、复制或删除。</span>
|
<span v-else-if="activeTool === 'array'">先选中一个货架 / 轨道元素,再拖一条线生成阵列。</span>
|
<span v-else>选择工具后点击元素,或切换框选工具框出一组元素。</span>
|
</div>
|
|
<div class="tool-section">
|
<div class="tool-section-label">画布设置</div>
|
<div class="prop-grid">
|
<el-input v-model.trim="canvasForm.width" size="small" placeholder="画布宽度"></el-input>
|
<el-input v-model.trim="canvasForm.height" size="small" placeholder="画布高度"></el-input>
|
<el-button class="span-2" size="small" plain @click="applyCanvasSize">应用画布尺寸</el-button>
|
</div>
|
</div>
|
|
<template v-if="singleSelectedElement">
|
<div class="tool-section">
|
<div class="tool-section-label">几何属性</div>
|
<div class="prop-grid">
|
<el-input size="small" :value="singleSelectedElement.type" disabled></el-input>
|
<el-input size="small" :value="singleSelectedElement.id" disabled></el-input>
|
<el-input v-model.trim="geometryForm.x" size="small" placeholder="X"></el-input>
|
<el-input v-model.trim="geometryForm.y" size="small" placeholder="Y"></el-input>
|
<el-input v-model.trim="geometryForm.width" size="small" placeholder="宽度"></el-input>
|
<el-input v-model.trim="geometryForm.height" size="small" placeholder="高度"></el-input>
|
<el-button class="span-2" size="small" plain @click="applyGeometry">应用几何</el-button>
|
</div>
|
</div>
|
|
<div v-if="singleSelectedElement.type === 'devp'" class="tool-section">
|
<div class="tool-section-label">输送站点配置</div>
|
<div class="prop-grid">
|
<div class="field-stack">
|
<span class="field-label">站号</span>
|
<el-input v-model.trim="devpForm.stationId" size="small" placeholder="请输入输送站点站号"></el-input>
|
</div>
|
<div class="field-stack">
|
<span class="field-label">PLC 编号</span>
|
<el-input v-model.trim="devpForm.deviceNo" size="small" placeholder="请输入输送站点 PLC 编号"></el-input>
|
</div>
|
<div class="field-stack span-2">
|
<span class="field-label">方向</span>
|
<div class="direction-grid">
|
<button
|
v-for="item in devpDirectionOptions"
|
:key="item.key"
|
type="button"
|
class="direction-chip"
|
:class="{ active: isDevpDirectionActive(item.key) }"
|
@click="toggleDevpDirection(item.key)">
|
<span class="direction-arrow">{{ item.arrow }}</span>
|
<span>{{ item.label }}</span>
|
</button>
|
</div>
|
<div class="field-help">点击箭头切换方向,可同时选择多个方向。</div>
|
</div>
|
<div class="field-stack span-2">
|
<span class="field-label">站点类型</span>
|
<div class="check-grid">
|
<el-checkbox v-model="devpForm.isBarcodeStation">条码站</el-checkbox>
|
<el-checkbox v-model="devpForm.isInStation">入站点</el-checkbox>
|
<el-checkbox v-model="devpForm.isOutStation">出站点</el-checkbox>
|
<el-checkbox v-model="devpForm.runBlockReassign">堵塞重分配</el-checkbox>
|
<el-checkbox v-model="devpForm.isOutOrder">出库排序</el-checkbox>
|
<el-checkbox v-model="devpForm.isLiftTransfer">顶升移栽</el-checkbox>
|
</div>
|
</div>
|
<div class="field-stack">
|
<span class="field-label">条码索引<span v-if="devpRequiresBarcodeIndex" class="field-required"> 必填</span></span>
|
<el-input v-model.trim="devpForm.barcodeIdx" size="small" placeholder="条码站时必填,例如 1"></el-input>
|
</div>
|
<div class="field-stack">
|
<span class="field-label">条码站站号<span v-if="devpRequiresBarcodeLink" class="field-required"> 必填</span></span>
|
<el-input v-model.trim="devpForm.barcodeStation" size="small" placeholder="入站点时必填,填写条码站站号"></el-input>
|
</div>
|
<div class="field-stack">
|
<span class="field-label">条码站 PLC 编号<span v-if="devpRequiresBarcodeLink" class="field-required"> 必填</span></span>
|
<el-input v-model.trim="devpForm.barcodeStationDeviceNo" size="small" placeholder="入站点时必填,填写条码站 PLC 编号"></el-input>
|
</div>
|
<div class="field-stack">
|
<span class="field-label">退回站站号<span v-if="devpRequiresBackStation" class="field-required"> 必填</span></span>
|
<el-input v-model.trim="devpForm.backStation" size="small" placeholder="条码站时必填,填写退回站站号"></el-input>
|
</div>
|
<div class="field-stack">
|
<span class="field-label">退回站 PLC 编号<span v-if="devpRequiresBackStation" class="field-required"> 必填</span></span>
|
<el-input v-model.trim="devpForm.backStationDeviceNo" size="small" placeholder="条码站时必填,填写退回站 PLC 编号"></el-input>
|
</div>
|
<div class="footer-note span-2">
|
勾选“入站点”后,必须填写条码站站号和条码站 PLC 编号。
|
勾选“条码站”后,必须填写条码索引、退回站站号和退回站 PLC 编号。
|
</div>
|
<el-button class="span-2" size="small" type="primary" plain @click="applyDevpForm">应用输送线配置</el-button>
|
</div>
|
</div>
|
|
<div v-if="singleSelectedDeviceElement" class="tool-section">
|
<div class="tool-section-label">{{ getDeviceConfigLabel(singleSelectedDeviceElement.type) }}</div>
|
<div class="prop-grid">
|
<el-input size="small" :value="getDeviceConfigKeyLabel(singleSelectedDeviceElement.type, deviceForm.valueKey)" disabled></el-input>
|
<el-input v-model.trim="deviceForm.deviceNo" size="small" placeholder="设备编号"></el-input>
|
<el-button class="span-2" size="small" type="primary" plain @click="applyDeviceForm">应用设备参数</el-button>
|
</div>
|
<div class="footer-note" style="padding-top: 8px;">
|
这里只改设备编号相关键,原始 JSON 里的其他字段会保留;下面仍可直接查看或手工修改 JSON。
|
</div>
|
</div>
|
|
<div class="tool-section">
|
<div class="tool-section-label">{{ singleSelectedElement.type === 'devp' ? '原始 JSON 预览' : (singleSelectedDeviceElement ? '原始 JSON 预览 / 手工编辑' : '值 / JSON 编辑') }}</div>
|
<el-input
|
class="json-box"
|
type="textarea"
|
:rows="8"
|
v-model="valueEditorText"
|
:readonly="singleSelectedElement.type === 'devp'">
|
</el-input>
|
<el-button
|
v-if="singleSelectedElement.type !== 'devp'"
|
size="small"
|
type="primary"
|
plain
|
@click="applyRawValue"
|
>应用值</el-button>
|
</div>
|
</template>
|
|
<div v-if="selectedShelfElements.length > 0" class="tool-section">
|
<div class="tool-section-label">货架自动填充</div>
|
<div class="prop-grid">
|
<el-input v-model.trim="shelfFillForm.startValue" size="small" placeholder="起始值,例如 12-1"></el-input>
|
<el-input size="small" :value="'已选货架 ' + selectedShelfElements.length + ' 个'" disabled></el-input>
|
<el-select v-model="shelfFillForm.rowStep" size="small" placeholder="排方向">
|
<el-option label="上到下递减" value="desc"></el-option>
|
<el-option label="上到下递增" value="asc"></el-option>
|
</el-select>
|
<el-select v-model="shelfFillForm.colStep" size="small" placeholder="列方向">
|
<el-option label="左到右递增" value="asc"></el-option>
|
<el-option label="左到右递减" value="desc"></el-option>
|
</el-select>
|
<el-button class="span-2" size="small" type="primary" plain @click="applyShelfAutoFill">按排列填充货架值</el-button>
|
</div>
|
<div class="footer-note" style="padding-top: 8px;">
|
会按选中货架的实际空间排列分组填充。默认规则是上到下排号递减、左到右列号递增。
|
</div>
|
</div>
|
|
<div class="footer-note">
|
编辑器只负责自由画布编辑,运行地图继续走当前 `BasMap.data`。所以这里允许自由拖拉矩形元素,但保存前会校验重叠、尺寸越界和 `devp` 必填字段,防止影响现有显示和算法。
|
</div>
|
</div>
|
</aside>
|
</div>
|
</div>
|
</main>
|
</section>
|
|
<el-dialog title="新建自由画布" :visible.sync="blankDialogVisible" width="420px" class="dialog-panel" append-to-body>
|
<el-form label-width="90px" size="small">
|
<el-form-item label="楼层">
|
<el-input v-model.trim="blankForm.lev"></el-input>
|
</el-form-item>
|
<el-form-item label="宽度">
|
<el-input v-model.trim="blankForm.width"></el-input>
|
</el-form-item>
|
<el-form-item label="高度">
|
<el-input v-model.trim="blankForm.height"></el-input>
|
</el-form-item>
|
</el-form>
|
<div slot="footer">
|
<el-button @click="blankDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="createBlankMap">创建</el-button>
|
</div>
|
</el-dialog>
|
|
<input ref="importInput" type="file" style="display:none;" @change="handleImportExcel">
|
<input ref="mapImportInput" type="file" accept=".json,application/json" style="display:none;" @change="handleImportMap">
|
</div>
|
|
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
|
<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
|
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
|
<script type="text/javascript" src="../../static/vue/element/element.js"></script>
|
<script type="text/javascript" src="../../static/js/pixi-legacy.min.js"></script>
|
<script type="text/javascript" src="../../static/js/basMap/editor.js?v=20260321d"></script>
|
</body>
|
</html>
|