lsh
2026-04-21 7443e8040d9a7669a8117c8a6937dbd4bd792709
src/main/webapp/views/basMap/editor.html
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <head>
    <meta charset="utf-8">
    <title>自由画布地图编辑器</title>
    <meta name="renderer" content="webkit">
@@ -9,797 +9,1178 @@
    <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;
        }
      :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; }
      [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;
        }
      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;
        }
      .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);
        }
      .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;
        }
      .workspace {
        min-height: 0;
        flex: 1 1 auto;
        display: flex;
      }
        .panel-card {
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
      .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 {
        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-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;
        }
      .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,
      .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 {
        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-section-label-sub {
        font-size: 12px;
        color: var(--text-sub);
        letter-spacing: 0.08em;
        text-transform: uppercase;
        line-height: 1.75;
      }
        .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-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
      }
        .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-el-popover {
        height: 100%;
      }
        .tool-card-btn strong {
            font-size: 13px;
            font-weight: 700;
        }
      .tool-el-popover > .el-popover__reference-wrapper {
        display: block;
        height: 100%;
      }
        .tool-card-btn span {
            font-size: 12px;
            color: var(--text-sub);
            line-height: 1.5;
        }
      .tool-el-popover .tool-card-btn {
        height: 100%;
      }
        .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;
        }
      .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;
      }
        .status-card strong,
        .selection-summary strong,
        .note-card strong {
            display: block;
            font-size: 13px;
            margin-bottom: 6px;
        }
      .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);
      }
        .status-card span,
        .selection-summary span,
        .note-card span {
            display: block;
            font-size: 12px;
            color: var(--text-sub);
            line-height: 1.65;
        }
      .tool-card-btn strong {
        font-size: 13px;
        font-weight: 700;
      }
        .selection-summary strong {
            font-size: 14px;
            color: var(--text-main);
        }
      .tool-card-btn span {
        font-size: 12px;
        color: var(--text-sub);
        line-height: 1.5;
      }
        .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;
        }
      .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;
      }
        .canvas-toolbar-main {
            flex: 1 1 420px;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
      .status-card strong,
      .selection-summary strong,
      .note-card strong {
        display: block;
        font-size: 13px;
        margin-bottom: 6px;
      }
        .canvas-toolbar-title {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }
      .status-card span,
      .selection-summary span,
      .note-card span {
        display: block;
        font-size: 12px;
        color: var(--text-sub);
        line-height: 1.65;
      }
        .canvas-toolbar-title h1 {
            margin: 0;
            font-size: 24px;
            font-weight: 700;
            letter-spacing: 0.3px;
        }
      .selection-summary strong {
        font-size: 14px;
        color: var(--text-main);
      }
        .canvas-toolbar-title span {
            font-size: 13px;
            line-height: 1.65;
            color: var(--text-sub);
        }
      .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-meta,
        .canvas-toolbar-actions {
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }
      .canvas-toolbar-main {
        flex: 1 1 420px;
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
        .canvas-toolbar-actions {
            justify-content: flex-end;
            flex: 0 1 760px;
        }
      .canvas-toolbar-title {
        display: flex;
        flex-direction: column;
        gap: 4px;
      }
        .canvas-toolbar-actions .el-input__inner,
        .canvas-toolbar-actions .el-button {
            border-radius: 10px;
        }
      .canvas-toolbar-title h1 {
        margin: 0;
        font-size: 24px;
        font-weight: 700;
        letter-spacing: 0.3px;
      }
        .canvas-meta {
            font-size: 12px;
            color: var(--text-sub);
        }
      .canvas-toolbar-title span {
        font-size: 13px;
        line-height: 1.65;
        color: var(--text-sub);
      }
        .canvas-card {
            flex: 1 1 auto;
            min-width: 0;
            min-height: 0;
        }
      .canvas-toolbar-meta,
      .canvas-toolbar-actions {
        display: flex;
        align-items: center;
        gap: 8px;
        flex-wrap: wrap;
      }
        .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-toolbar-actions {
        justify-content: flex-end;
        flex: 0 1 760px;
      }
        .canvas-stage {
            position: absolute;
            inset: 0;
            overflow: hidden;
            background: #f6f9fc;
        }
      .canvas-toolbar-actions .el-input__inner,
      .canvas-toolbar-actions .el-button {
        border-radius: 10px;
      }
        .canvas-host {
            position: absolute;
            inset: 0;
            background: #f6f9fc;
        }
      .canvas-meta {
        font-size: 12px;
        color: var(--text-sub);
      }
        .canvas-overlay-layer {
            position: absolute;
            inset: 0;
            pointer-events: none;
            z-index: 5;
        }
      .canvas-card {
        flex: 1 1 auto;
        min-width: 0;
        min-height: 0;
      }
        .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-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-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-stage {
        position: absolute;
        inset: 0;
        overflow: hidden;
        background: #f6f9fc;
      }
        .canvas-loading-card strong {
            font-size: 18px;
        }
      .canvas-host {
        position: absolute;
        inset: 0;
        background: #f6f9fc;
      }
        .canvas-loading-card span {
            font-size: 13px;
            color: var(--text-sub);
        }
      .canvas-overlay-layer {
        position: absolute;
        inset: 0;
        pointer-events: none;
        z-index: 5;
      }
        .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;
        }
      .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 {
            left: 14px;
          width: 260px;
        }
        .overlay-right {
            right: 14px;
            width: 340px;
          width: 300px;
        }
      }
      @media (max-width: 980px) {
        .canvas-card {
          min-height: 0;
        }
        .overlay-panel.collapsed {
            width: 68px;
            bottom: auto;
        .canvas-wrap {
          min-height: 0;
        }
        .overlay-panel.collapsed .panel-body {
            display: none;
        .overlay-left,
        .overlay-right {
          width: min(280px, calc(100% - 28px));
        }
        .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">
  </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>WCS地图编辑器</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 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-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>
                        </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>
                    </aside>
                </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>
      </section>
    <el-dialog title="新建自由画布" :visible.sync="blankDialogVisible" width="420px" class="dialog-panel" append-to-body>
      <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-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>
          <el-button @click="blankDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="createBlankMap">创建</el-button>
        </div>
    </el-dialog>
      </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>
      <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=20260321e"></script>
</body>
    <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>