| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <head> |
| | | <meta charset="utf-8"> |
| | | <title>自由画布地图编辑器</title> |
| | | <meta name="renderer" content="webkit"> |
| | |
| | | <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> |