<!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-section-label-sub {
|
font-size: 12px;
|
color: var(--text-sub);
|
letter-spacing: 0.08em;
|
text-transform: uppercase;
|
line-height: 1.75;
|
}
|
|
.tool-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 8px;
|
}
|
|
.tool-el-popover {
|
height: 100%;
|
}
|
|
.tool-el-popover > .el-popover__reference-wrapper {
|
display: block;
|
height: 100%;
|
}
|
|
.tool-el-popover .tool-card-btn {
|
height: 100%;
|
}
|
|
.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 .device-divider {
|
margin: 0px 0 6px;
|
}
|
|
.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>
|
</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">
|
<template v-for="tool in drawTools">
|
<el-popover
|
class="tool-el-popover"
|
v-if="tool.key === 'annulus'"
|
:key="tool.key"
|
placement="right"
|
title="请选择绘制的形状"
|
width="520"
|
trigger="click"
|
>
|
<el-radio-group v-model="annulusShape" size="mini">
|
<el-radio-button label="rect">圆角矩形</el-radio-button>
|
<el-radio-button label="L1">圆角L型</el-radio-button>
|
<el-radio-button label="L2">圆角L型旋转90°</el-radio-button>
|
<el-radio-button label="L3">圆角L型旋转180°</el-radio-button>
|
<el-radio-button label="L4">圆角L型旋转270°</el-radio-button>
|
</el-radio-group>
|
<button
|
slot="reference"
|
type="button"
|
class="tool-card-btn"
|
:class="{ active: activeTool === tool.key }"
|
@click="setTool(tool.key)"
|
>
|
<strong>{{ tool.label }}</strong>
|
<span>{{ tool.desc }}</span>
|
</button>
|
</el-popover>
|
<button
|
v-else
|
: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>
|
</template>
|
</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>
|
</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="轨道ID" disabled></el-input>
|
<el-input
|
v-model.trim="deviceForm.trackId"
|
size="small"
|
placeholder="轨道ID(默认递增,从 1 开始)"
|
></el-input>
|
<el-input size="small" value="条码起始值" disabled></el-input>
|
<el-input
|
v-model.trim="deviceForm.barCodeStart"
|
size="small"
|
placeholder="轨道条码起始值,默认 0"
|
></el-input>
|
<el-input size="small" value="条码结束值" disabled></el-input>
|
<el-input
|
v-model.trim="deviceForm.barCodeEnd"
|
size="small"
|
placeholder="轨道条码结束值,默认 100000"
|
></el-input>
|
<div
|
class="tool-section-label-sub span-2"
|
v-if="deviceForm.deviceList.length > 0"
|
>
|
设备列表
|
</div>
|
<template v-for="(item, index) in deviceForm.deviceList">
|
<el-input
|
size="small"
|
:value="getDeviceConfigKeyLabel(singleSelectedDeviceElement.type, item.valueKey)"
|
disabled
|
></el-input>
|
<el-input
|
v-model.trim="item.deviceNo"
|
size="small"
|
placeholder="设备编号"
|
></el-input>
|
<el-input size="small" value="起始位置" disabled></el-input>
|
<div
|
style="display: flex; align-items: center; gap: 10px; margin-left: 12px"
|
>
|
<el-slider
|
v-model="item.progress"
|
:max="100"
|
:min="0"
|
show-tooltip
|
style="flex: 1"
|
></el-slider>
|
<span
|
style="
|
font-size: 12px;
|
color: var(--text-sub);
|
width: 30px;
|
text-align: right;
|
"
|
>{{ item.progress }}%</span
|
>
|
</div>
|
|
<el-input size="small" value="设备长(沿轨道)" disabled></el-input>
|
<el-input
|
v-model.trim="item.deviceLength"
|
size="small"
|
:placeholder="getDeviceLengthPlaceholder()"
|
></el-input>
|
<el-input size="small" value="设备宽(垂直轨道)" disabled></el-input>
|
<el-input
|
v-model.trim="item.deviceWidth"
|
size="small"
|
:placeholder="getDeviceWidthPlaceholder()"
|
></el-input>
|
<!-- <div class="field-help span-2">默认(自动): 长 {{ getAutoTrackDeviceLengthValue() }} | 宽 {{ getAutoTrackDeviceWidthValue() }}</div> -->
|
<el-divider
|
class="device-divider span-2"
|
v-if="index < deviceForm.deviceList.length - 1"
|
></el-divider>
|
</template>
|
|
<el-button size="small" type="primary" plain @click="addDeviceForm"
|
>添加设备</el-button
|
>
|
<el-button 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>
|
</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/mapTrackGeometry.js?v=20260406b"
|
></script>
|
<script type="text/javascript" src="../../static/js/basMap/editor.js?v=20260321e"></script>
|
</body>
|
</html>
|