<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="utf-8">
|
<title>输送路径策略管理</title>
|
<meta name="renderer" content="webkit">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<link rel="stylesheet" href="../../static/vue/element/element.css">
|
<link rel="stylesheet" href="../../static/css/cool.css">
|
<style>
|
:root {
|
--page-bg:
|
radial-gradient(1200px 560px at -10% -20%, rgba(53, 117, 200, 0.14), transparent 58%),
|
radial-gradient(900px 420px at 110% -10%, rgba(38, 164, 138, 0.12), transparent 56%),
|
linear-gradient(180deg, #eff4fa 0%, #f7fafc 100%);
|
--card-bg: rgba(255, 255, 255, 0.94);
|
--card-border: rgba(216, 226, 238, 0.95);
|
--text-main: #223449;
|
--text-sub: #607286;
|
--primary: #2f79d6;
|
--accent: #20a98a;
|
--warn: #ef8b3b;
|
--danger: #dc5c5c;
|
--path: #1f9f89;
|
--preferred: #2f79d6;
|
--forbid: #d9534f;
|
--must-pass: #9b59b6;
|
--waypoint: #ff9f1c;
|
}
|
|
[v-cloak] { display: none; }
|
|
html, body {
|
margin: 0;
|
min-height: 100%;
|
height: auto;
|
font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
|
background: var(--page-bg);
|
color: var(--text-main);
|
}
|
|
.page-shell {
|
min-height: 100%;
|
height: auto;
|
box-sizing: border-box;
|
width: min(1880px, calc(100% - 24px));
|
margin: 0 auto;
|
padding: 20px 8px 24px;
|
display: flex;
|
flex-direction: column;
|
gap: 18px;
|
}
|
|
.hero-card,
|
.panel-card {
|
background: var(--card-bg);
|
border: 1px solid var(--card-border);
|
border-radius: 24px;
|
box-shadow: 0 16px 32px rgba(39, 62, 92, 0.08);
|
}
|
|
.hero-card {
|
padding: 24px 28px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 20px;
|
flex-wrap: wrap;
|
}
|
|
.hero-title {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.hero-title h1 {
|
margin: 0;
|
font-size: 24px;
|
font-weight: 700;
|
letter-spacing: 0.4px;
|
}
|
|
.hero-actions {
|
display: flex;
|
gap: 10px;
|
flex-wrap: wrap;
|
justify-content: flex-end;
|
}
|
|
.hero-grid {
|
display: grid;
|
grid-template-columns: minmax(340px, 380px) minmax(0, 1fr);
|
grid-template-areas:
|
"profile rule"
|
"profile preview";
|
gap: 16px;
|
align-items: start;
|
}
|
|
.profile-panel {
|
grid-area: profile;
|
min-width: 0;
|
}
|
|
.rule-panel {
|
grid-area: rule;
|
min-width: 0;
|
}
|
|
.panel-card {
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
}
|
|
.preview-panel {
|
grid-area: preview;
|
min-width: 0;
|
}
|
|
.panel-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 10px;
|
padding: 20px 22px 16px;
|
border-bottom: 1px solid rgba(221, 230, 239, 0.94);
|
}
|
|
.panel-head h2 {
|
margin: 0;
|
font-size: 17px;
|
font-weight: 700;
|
}
|
|
.panel-body {
|
padding: 18px 20px 20px;
|
flex: 0 0 auto;
|
overflow: visible;
|
}
|
|
.setting-grid {
|
display: grid;
|
grid-template-columns: 1fr;
|
gap: 12px;
|
}
|
|
.hint-card {
|
border-radius: 18px;
|
background: linear-gradient(180deg, rgba(243, 248, 255, 0.94) 0%, rgba(248, 251, 255, 0.9) 100%);
|
border: 1px solid rgba(212, 223, 236, 0.92);
|
padding: 14px 16px;
|
}
|
|
.hint-card strong {
|
display: block;
|
margin-bottom: 6px;
|
font-size: 13px;
|
}
|
|
.hint-card span {
|
color: var(--text-sub);
|
font-size: 12px;
|
line-height: 1.7;
|
}
|
|
.stat-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 10px;
|
}
|
|
.stat-card {
|
border: 1px solid rgba(217, 227, 236, 0.94);
|
background: rgba(255, 255, 255, 0.95);
|
border-radius: 18px;
|
padding: 16px;
|
}
|
|
.stat-card .label {
|
color: var(--text-sub);
|
font-size: 12px;
|
}
|
|
.stat-card .value {
|
margin-top: 6px;
|
font-size: 24px;
|
font-weight: 700;
|
}
|
|
.entity-list {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.entity-empty.el-empty {
|
padding: 18px 0 4px;
|
}
|
|
.entity-empty .el-empty__image {
|
height: 96px;
|
}
|
|
.entity-empty .el-empty__description p {
|
color: var(--text-sub);
|
font-size: 13px;
|
}
|
|
.entity-item {
|
border: 1px solid rgba(215, 226, 238, 0.94);
|
background: rgba(255, 255, 255, 0.96);
|
border-radius: 18px;
|
padding: 16px 16px 14px;
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
}
|
|
.entity-item.is-active {
|
border-color: rgba(47, 121, 214, 0.65);
|
box-shadow: 0 10px 24px rgba(47, 121, 214, 0.12);
|
transform: translateY(-1px);
|
}
|
|
.entity-item-head {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 10px;
|
}
|
|
.entity-title {
|
font-size: 14px;
|
font-weight: 700;
|
line-height: 1.5;
|
}
|
|
.entity-meta {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 6px;
|
margin-top: 8px;
|
}
|
|
.meta-pill {
|
display: inline-flex;
|
align-items: center;
|
padding: 4px 8px;
|
font-size: 12px;
|
border-radius: 999px;
|
background: rgba(242, 246, 252, 0.94);
|
color: var(--text-sub);
|
border: 1px solid rgba(220, 229, 238, 0.9);
|
}
|
|
.entity-desc {
|
margin-top: 10px;
|
color: var(--text-sub);
|
font-size: 12px;
|
line-height: 1.7;
|
}
|
|
.entity-actions {
|
margin-top: 12px;
|
display: flex;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.entity-actions .el-button {
|
padding: 7px 11px;
|
border-radius: 10px;
|
}
|
|
.empty-shell {
|
border: 1px dashed rgba(193, 207, 221, 0.96);
|
background: rgba(248, 251, 254, 0.84);
|
border-radius: 18px;
|
padding: 22px 16px;
|
}
|
|
.preview-toolbar {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
margin-bottom: 0;
|
}
|
|
.preview-toolbar-row {
|
display: grid;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.preview-toolbar-row-main {
|
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.2fr) minmax(180px, 220px);
|
}
|
|
.preview-toolbar-row-secondary {
|
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
}
|
|
.preview-toolbar-actions {
|
display: flex;
|
gap: 10px;
|
justify-content: flex-end;
|
align-items: center;
|
flex-wrap: wrap;
|
}
|
|
.preview-zoom-card {
|
display: flex;
|
align-items: center;
|
gap: 14px;
|
padding: 10px 14px;
|
border-radius: 18px;
|
background: linear-gradient(180deg, rgba(247, 250, 255, 0.96) 0%, rgba(243, 248, 254, 0.96) 100%);
|
border: 1px solid rgba(217, 227, 236, 0.96);
|
}
|
|
.preview-zoom-meta {
|
min-width: 74px;
|
}
|
|
.preview-zoom-meta strong {
|
display: block;
|
font-size: 12px;
|
margin-bottom: 2px;
|
}
|
|
.preview-zoom-meta span {
|
font-size: 12px;
|
color: var(--text-sub);
|
}
|
|
.preview-panel-body {
|
display: flex;
|
flex-direction: column;
|
gap: 14px;
|
}
|
|
.preview-info-grid {
|
display: grid;
|
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.95fr);
|
gap: 12px;
|
align-items: start;
|
}
|
|
.preview-summary-card {
|
border-radius: 16px;
|
border: 1px solid rgba(217, 227, 236, 0.96);
|
background: rgba(255, 255, 255, 0.96);
|
padding: 12px 14px;
|
}
|
|
.preview-summary {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
margin-bottom: 0;
|
}
|
|
.preview-summary-actions {
|
margin-top: 10px;
|
display: flex;
|
justify-content: flex-end;
|
}
|
|
.summary-chip {
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
padding: 7px 10px;
|
border-radius: 999px;
|
background: rgba(244, 248, 252, 0.96);
|
border: 1px solid rgba(220, 229, 238, 0.96);
|
font-size: 12px;
|
}
|
|
.summary-chip strong {
|
color: var(--text-main);
|
}
|
|
.route-strip {
|
border-radius: 18px;
|
border: 1px solid rgba(217, 227, 236, 0.94);
|
background: rgba(255, 255, 255, 0.94);
|
padding: 12px;
|
margin-bottom: 12px;
|
}
|
|
.route-strip-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
margin-bottom: 10px;
|
}
|
|
.route-tag-row {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
max-height: 98px;
|
overflow-y: auto;
|
padding-right: 4px;
|
}
|
|
.route-tag {
|
border-radius: 999px;
|
padding: 6px 10px;
|
background: rgba(47, 121, 214, 0.08);
|
color: #245a95;
|
font-size: 12px;
|
border: 1px solid rgba(47, 121, 214, 0.12);
|
}
|
|
.route-strip-more {
|
margin-top: 8px;
|
color: var(--text-sub);
|
font-size: 12px;
|
}
|
|
.map-legend {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 10px;
|
margin-bottom: 10px;
|
}
|
|
.legend-item {
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
font-size: 12px;
|
color: var(--text-sub);
|
}
|
|
.legend-dot {
|
width: 10px;
|
height: 10px;
|
border-radius: 50%;
|
display: inline-block;
|
}
|
|
.map-shell {
|
position: relative;
|
flex: 1;
|
min-height: 620px;
|
border-radius: 22px;
|
border: 1px solid rgba(216, 226, 238, 0.95);
|
background:
|
linear-gradient(180deg, rgba(251, 253, 255, 0.96) 0%, rgba(247, 250, 253, 0.94) 100%);
|
overflow: hidden;
|
}
|
|
.map-toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 10px;
|
padding: 12px 14px;
|
border-bottom: 1px solid rgba(221, 230, 239, 0.94);
|
background: rgba(249, 251, 254, 0.9);
|
}
|
|
.map-toolbar-left,
|
.map-toolbar-right {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
flex-wrap: wrap;
|
}
|
|
.map-canvas-wrap {
|
position: absolute;
|
top: 56px;
|
right: 0;
|
bottom: 0;
|
left: 0;
|
overflow: hidden;
|
padding: 20px;
|
box-sizing: border-box;
|
cursor: grab;
|
user-select: none;
|
touch-action: none;
|
}
|
|
.map-stage {
|
position: absolute;
|
top: 0;
|
left: 0;
|
transform-origin: left top;
|
will-change: transform;
|
}
|
|
.map-stage svg {
|
position: absolute;
|
top: 0;
|
left: 0;
|
overflow: visible;
|
}
|
|
.map-node {
|
position: absolute;
|
width: 18px;
|
height: 18px;
|
margin-left: -9px;
|
margin-top: -9px;
|
border-radius: 50%;
|
border: 2px solid rgba(165, 181, 198, 0.88);
|
background: rgba(255, 255, 255, 0.95);
|
cursor: pointer;
|
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-sizing: border-box;
|
}
|
|
.map-node:hover {
|
transform: scale(1.14);
|
box-shadow: 0 8px 18px rgba(58, 82, 111, 0.16);
|
z-index: 4;
|
}
|
|
.map-canvas-wrap.is-dragging {
|
cursor: grabbing;
|
}
|
|
.map-canvas-wrap.is-dragging .map-node {
|
cursor: grabbing;
|
}
|
|
.map-node.is-picked {
|
width: 26px;
|
height: 26px;
|
margin-left: -13px;
|
margin-top: -13px;
|
border-color: #101828;
|
box-shadow: 0 10px 24px rgba(16, 24, 40, 0.16);
|
z-index: 6;
|
}
|
|
.map-node.is-path {
|
border-color: rgba(31, 159, 137, 0.92);
|
background: rgba(31, 159, 137, 0.12);
|
}
|
|
.map-node.is-preferred {
|
border-color: rgba(47, 121, 214, 0.92);
|
background: rgba(47, 121, 214, 0.09);
|
}
|
|
.map-node.is-waypoint {
|
box-shadow: 0 0 0 4px rgba(255, 159, 28, 0.16);
|
}
|
|
.map-node.is-forbid {
|
border-color: rgba(220, 92, 92, 0.94);
|
background: rgba(220, 92, 92, 0.12);
|
}
|
|
.map-node.is-must-pass {
|
box-shadow: 0 0 0 4px rgba(155, 89, 182, 0.14);
|
}
|
|
.map-node.is-start,
|
.map-node.is-end {
|
width: 28px;
|
height: 28px;
|
margin-left: -14px;
|
margin-top: -14px;
|
}
|
|
.map-node.is-start {
|
border-color: rgba(47, 121, 214, 0.92);
|
background: rgba(47, 121, 214, 0.18);
|
}
|
|
.map-node.is-end {
|
border-color: rgba(32, 169, 138, 0.92);
|
background: rgba(32, 169, 138, 0.16);
|
}
|
|
.map-node-label {
|
position: absolute;
|
top: 16px;
|
left: 50%;
|
transform: translateX(-50%);
|
white-space: nowrap;
|
font-size: 11px;
|
padding: 2px 6px;
|
border-radius: 999px;
|
background: rgba(16, 24, 40, 0.76);
|
color: #fff;
|
pointer-events: none;
|
}
|
|
.rule-json {
|
margin-top: 12px;
|
border-radius: 18px;
|
background: rgba(16, 24, 40, 0.92);
|
color: #d6e5ff;
|
padding: 12px 14px;
|
font-family: Menlo, Monaco, Consolas, monospace;
|
font-size: 12px;
|
line-height: 1.7;
|
max-height: 220px;
|
overflow: auto;
|
white-space: pre-wrap;
|
word-break: break-all;
|
}
|
|
.mono {
|
font-family: Menlo, Monaco, Consolas, monospace;
|
}
|
|
.sequence-card {
|
margin-top: 10px;
|
border-radius: 16px;
|
border: 1px solid rgba(218, 228, 238, 0.96);
|
background: rgba(255, 255, 255, 0.92);
|
padding: 8px;
|
}
|
|
.sequence-row {
|
display: grid;
|
grid-template-columns: 28px minmax(0, 1fr) auto;
|
align-items: center;
|
gap: 10px;
|
border-radius: 12px;
|
padding: 6px 8px;
|
}
|
|
.sequence-row + .sequence-row {
|
margin-top: 6px;
|
}
|
|
.sequence-row:hover {
|
background: rgba(243, 247, 252, 0.92);
|
}
|
|
.sequence-index {
|
width: 24px;
|
height: 24px;
|
border-radius: 50%;
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
font-size: 12px;
|
color: #245a95;
|
background: rgba(47, 121, 214, 0.08);
|
border: 1px solid rgba(47, 121, 214, 0.14);
|
}
|
|
.sequence-label {
|
min-width: 0;
|
font-size: 12px;
|
line-height: 1.6;
|
color: var(--text-main);
|
word-break: break-all;
|
}
|
|
.sequence-actions {
|
display: inline-flex;
|
align-items: center;
|
gap: 4px;
|
}
|
|
.sequence-actions .el-button {
|
padding: 5px 7px;
|
border-radius: 10px;
|
}
|
|
.dialog-picked-bar {
|
margin: 0 0 12px;
|
border-radius: 16px;
|
border: 1px solid rgba(217, 227, 236, 0.96);
|
background: rgba(247, 250, 254, 0.95);
|
padding: 10px 12px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
|
.dialog-picked-bar .help {
|
color: var(--text-sub);
|
font-size: 12px;
|
line-height: 1.6;
|
}
|
|
.dialog-panel .el-dialog {
|
border-radius: 24px;
|
overflow: hidden;
|
}
|
|
.dialog-panel .el-dialog__header {
|
padding: 22px 24px 14px;
|
background: linear-gradient(180deg, #f7fbff 0%, #f2f7fb 100%);
|
border-bottom: 1px solid rgba(223, 231, 240, 0.94);
|
}
|
|
.dialog-panel .el-dialog__body {
|
padding: 18px 24px 10px;
|
}
|
|
.dialog-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 12px;
|
}
|
|
.dialog-grid .span-2 {
|
grid-column: span 2;
|
}
|
|
.section-card {
|
border: 1px solid rgba(219, 229, 238, 0.96);
|
border-radius: 18px;
|
padding: 12px 14px 2px;
|
background: rgba(250, 252, 254, 0.9);
|
margin-bottom: 12px;
|
}
|
|
.section-card h3 {
|
margin: 0 0 10px;
|
font-size: 13px;
|
font-weight: 700;
|
}
|
|
.section-title-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
flex-wrap: wrap;
|
margin-bottom: 10px;
|
}
|
|
.section-title-row h3 {
|
margin-bottom: 0;
|
}
|
|
.section-inline-actions {
|
display: flex;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.section-help {
|
color: var(--text-sub);
|
font-size: 12px;
|
margin: -2px 0 10px;
|
line-height: 1.6;
|
}
|
|
.selected-station-bar {
|
border-radius: 16px;
|
border: 1px solid rgba(217, 227, 236, 0.96);
|
background: rgba(255, 255, 255, 0.95);
|
padding: 10px 12px;
|
margin-bottom: 12px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
|
@media (max-width: 1480px) {
|
.hero-grid {
|
grid-template-columns: minmax(320px, 360px) minmax(0, 1fr);
|
}
|
|
.preview-info-grid {
|
grid-template-columns: 1fr;
|
}
|
}
|
|
@media (max-width: 1280px) {
|
.hero-grid {
|
grid-template-columns: 1fr;
|
grid-template-areas:
|
"profile"
|
"rule"
|
"preview";
|
}
|
.map-shell {
|
min-height: 560px;
|
}
|
.preview-toolbar-row,
|
.preview-toolbar-row-main,
|
.preview-toolbar-row-secondary {
|
grid-template-columns: 1fr;
|
}
|
.preview-toolbar-actions {
|
justify-content: flex-start;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<div id="app" v-cloak class="page-shell">
|
<div class="hero-card">
|
<div class="hero-title">
|
<h1>输送路径策略管理</h1>
|
</div>
|
<div class="hero-actions">
|
<el-button icon="el-icon-refresh" @click="loadData">刷新</el-button>
|
<el-button type="primary" icon="el-icon-check" :loading="saving" @click="saveAll">保存全部</el-button>
|
</div>
|
</div>
|
|
<div class="hero-grid">
|
<div class="panel-card profile-panel">
|
<div class="panel-head">
|
<div><h2>模板与模式</h2></div>
|
<el-button type="primary" plain size="small" icon="el-icon-plus" @click="openProfileDialog()">新增模板</el-button>
|
</div>
|
<div class="panel-body">
|
<div class="setting-grid">
|
<div class="section-card">
|
<h3>全局开关</h3>
|
<el-form label-position="top">
|
<div class="dialog-grid">
|
<el-form-item label="评分模式" class="span-2">
|
<el-radio-group v-model="scoreMode">
|
<el-radio-button label="legacy">legacy</el-radio-button>
|
<el-radio-button label="twoStage">twoStage</el-radio-button>
|
</el-radio-group>
|
</el-form-item>
|
<el-form-item label="默认模板" class="span-2">
|
<el-select v-model="defaultProfileCode" placeholder="请选择默认模板" filterable style="width: 100%;">
|
<el-option v-for="item in profiles" :key="item.profileCode" :label="item.profileName + ' (' + item.profileCode + ')'" :value="item.profileCode"></el-option>
|
</el-select>
|
</el-form-item>
|
</div>
|
</el-form>
|
</div>
|
|
<div class="stat-grid">
|
<div class="stat-card">
|
<div class="label">模板数量</div>
|
<div class="value">{{ profiles.length }}</div>
|
</div>
|
<div class="stat-card">
|
<div class="label">规则数量</div>
|
<div class="value">{{ rules.length }}</div>
|
</div>
|
</div>
|
|
<div class="entity-list" v-if="profiles.length">
|
<div class="entity-item" v-for="item in profiles" :key="item.profileCode" :class="{ 'is-active': selectedProfileCode === item.profileCode }" @click="selectedProfileCode = item.profileCode">
|
<div class="entity-item-head">
|
<div>
|
<div class="entity-title">{{ item.profileName }}</div>
|
<div class="entity-meta">
|
<span class="meta-pill mono">{{ item.profileCode }}</span>
|
<span class="meta-pill">优先级 {{ item.priority }}</span>
|
<span class="meta-pill">{{ item.status === 1 ? '启用' : '禁用' }}</span>
|
</div>
|
</div>
|
<el-tag size="mini" :type="defaultProfileCode === item.profileCode ? 'success' : 'info'">{{ defaultProfileCode === item.profileCode ? '默认' : '模板' }}</el-tag>
|
</div>
|
<div class="entity-desc">
|
S1: 长度 {{ item.config.s1LenWeight }} / 拐点 {{ item.config.s1TurnWeight }} / 顶升 {{ item.config.s1LiftWeight }}<br>
|
S2: 忙站 {{ item.config.s2BusyWeight }} / 堵塞 {{ item.config.s2RunBlockWeight }} / 环线 {{ item.config.s2LoopLoadWeight }}
|
</div>
|
<div class="entity-actions">
|
<el-button size="mini" @click.stop="openProfileDialog(item)">编辑</el-button>
|
<el-button size="mini" type="primary" plain @click.stop="cloneProfile(item)">复制</el-button>
|
<el-button size="mini" type="danger" plain @click.stop="removeProfile(item)">删除</el-button>
|
</div>
|
</div>
|
</div>
|
<el-empty v-else class="entity-empty" description="还没有路径模板"></el-empty>
|
</div>
|
</div>
|
</div>
|
|
<div class="panel-card rule-panel">
|
<div class="panel-head">
|
<div><h2>人工规则</h2></div>
|
<el-button type="primary" plain size="small" icon="el-icon-plus" @click="openRuleDialog()">新增规则</el-button>
|
</div>
|
<div class="panel-body">
|
<div class="entity-list" v-if="rules.length">
|
<div class="entity-item" v-for="item in rules" :key="item.ruleCode" :class="{ 'is-active': selectedRuleCode === item.ruleCode }" @click="selectRule(item)">
|
<div class="entity-item-head">
|
<div>
|
<div class="entity-title">{{ item.ruleName }}</div>
|
<div class="entity-meta">
|
<span class="meta-pill mono">{{ item.ruleCode }}</span>
|
<span class="meta-pill">优先级 {{ item.priority }}</span>
|
<span class="meta-pill">{{ item.status === 1 ? '启用' : '禁用' }}</span>
|
<span class="meta-pill">{{ item.profileCode || '未绑定模板' }}</span>
|
</div>
|
</div>
|
<el-tag size="mini" :type="item.startStationId && item.endStationId ? 'success' : 'warning'">{{ routeLabel(item) }}</el-tag>
|
</div>
|
<div class="entity-desc">
|
硬约束 {{ ruleSummaryCount(item.hard) }} 项 · 途经点 {{ (item.waypoint.stations || []).length }} 个 · 软偏好 {{ (item.soft.preferredPath || []).length }} 段
|
</div>
|
<div class="entity-actions">
|
<el-button size="mini" @click.stop="openRuleDialog(item)">编辑</el-button>
|
<el-button size="mini" type="primary" plain @click.stop="cloneRule(item)">复制</el-button>
|
<el-button size="mini" @click.stop="previewRule(item)" :disabled="!item.startStationId || !item.endStationId">试算</el-button>
|
<el-button size="mini" type="danger" plain @click.stop="removeRule(item)">删除</el-button>
|
</div>
|
</div>
|
</div>
|
<el-empty v-else class="entity-empty" description="还没有人工规则"></el-empty>
|
</div>
|
</div>
|
|
<div class="panel-card preview-panel">
|
<div class="panel-head">
|
<div><h2>可视化预览</h2></div>
|
</div>
|
<div class="panel-body preview-panel-body">
|
<div class="preview-toolbar">
|
<div class="preview-toolbar-row preview-toolbar-row-main">
|
<el-select v-model="previewForm.startStationId" filterable clearable placeholder="起点站点">
|
<el-option v-for="item in stationOptions" :key="'s-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<el-select v-model="previewForm.endStationId" filterable clearable placeholder="终点站点">
|
<el-option v-for="item in stationOptions" :key="'e-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<el-select v-model="activeMapLev" placeholder="楼层" clearable @change="loadMapByLev" :disabled="previewLoading">
|
<el-option v-for="lev in levList" :key="lev" :label="'楼层 ' + lev" :value="lev"></el-option>
|
</el-select>
|
</div>
|
<div class="preview-toolbar-row preview-toolbar-row-secondary">
|
<div class="preview-zoom-card">
|
<div class="preview-zoom-meta">
|
<strong>地图缩放</strong>
|
<span>{{ mapZoomPercent }}%</span>
|
</div>
|
<el-slider :value="mapZoomPercent" @input="updateMapZoom" :min="60" :max="220" :step="10" :show-tooltip="false" style="flex: 1;"></el-slider>
|
</div>
|
<div class="preview-toolbar-actions">
|
<el-button @click="fitMap" :disabled="!mapContext.nodes.length">适配地图</el-button>
|
<el-button @click="resetPreview">清空</el-button>
|
<el-button type="primary" :loading="previewLoading" @click="loadPreview" :disabled="!previewForm.startStationId || !previewForm.endStationId">解析预览</el-button>
|
</div>
|
</div>
|
</div>
|
|
<div class="preview-info-grid">
|
<div class="selected-station-bar">
|
<div>
|
<strong>当前选中站点:</strong>
|
<span v-if="pickedStation">{{ stationOptionLabel(pickedStation) }}</span>
|
<span v-else style="color: var(--text-sub);">点击地图上的输送站点</span>
|
</div>
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
<el-button size="mini" :disabled="!pickedStation" @click="applyPickedStation('start')">设为预览起点</el-button>
|
<el-button size="mini" :disabled="!pickedStation" @click="applyPickedStation('end')">设为预览终点</el-button>
|
</div>
|
</div>
|
<div class="preview-summary-card">
|
<div class="preview-summary">
|
<span class="summary-chip"><strong>模式</strong> {{ scoreMode }}</span>
|
<span class="summary-chip"><strong>默认模板</strong> {{ defaultProfileCode || '-' }}</span>
|
<span class="summary-chip" v-if="previewResult"><strong>命中模板</strong> {{ previewResult.resolvedPolicy && previewResult.resolvedPolicy.profileEntity ? previewResult.resolvedPolicy.profileEntity.profileCode : '-' }}</span>
|
<span class="summary-chip" v-if="previewResult"><strong>命中规则</strong> {{ previewResult.resolvedPolicy && previewResult.resolvedPolicy.ruleEntity ? previewResult.resolvedPolicy.ruleEntity.ruleCode : '无' }}</span>
|
<span class="summary-chip" v-if="previewResult"><strong>路径长度</strong> {{ previewResult.pathLength }}</span>
|
<span class="summary-chip" v-if="previewResult"><strong>拐点数</strong> {{ previewResult.turnCount }}</span>
|
<span class="summary-chip" v-if="previewResult"><strong>顶升点</strong> {{ previewResult.liftTransferCount }}</span>
|
</div>
|
<div class="preview-summary-actions" v-if="activeRulePreviewJson">
|
<el-button type="text" @click="showRuleJson = !showRuleJson">{{ showRuleJson ? '收起规则 JSON' : '查看规则 JSON' }}</el-button>
|
</div>
|
</div>
|
</div>
|
|
<div class="route-strip" v-if="previewPathTags.length">
|
<div class="route-strip-head">
|
<strong>路径站点序列</strong>
|
<div style="display: flex; align-items: center; gap: 12px;">
|
<span style="color: var(--text-sub); font-size: 12px;">{{ previewPathTags.length }} 个站点</span>
|
<el-button type="text" v-if="hiddenPathTagCount || showAllPathTags" @click="showAllPathTags = !showAllPathTags">{{ showAllPathTags ? '收起' : '展开全部' }}</el-button>
|
</div>
|
</div>
|
<div class="route-tag-row">
|
<span class="route-tag" v-for="item in visiblePreviewPathTags" :key="'path-' + item.stationId">{{ item.label }}</span>
|
</div>
|
<div class="route-strip-more" v-if="hiddenPathTagCount && !showAllPathTags">还有 {{ hiddenPathTagCount }} 个站点未展开</div>
|
</div>
|
|
<div class="map-legend">
|
<span class="legend-item"><i class="legend-dot" style="background: rgba(255,255,255,0.95); border: 2px solid rgba(165,181,198,0.88);"></i>输送站点</span>
|
<span class="legend-item"><i class="legend-dot" style="background: rgba(31,159,137,0.12); border: 2px solid rgba(31,159,137,0.92);"></i>实际路径</span>
|
<span class="legend-item"><i class="legend-dot" style="background: rgba(47,121,214,0.09); border: 2px solid rgba(47,121,214,0.92);"></i>软偏好</span>
|
<span class="legend-item"><i class="legend-dot" style="background: rgba(220,92,92,0.12); border: 2px solid rgba(220,92,92,0.94);"></i>禁用站点</span>
|
<span class="legend-item"><i class="legend-dot" style="background: rgba(255,159,28,0.16); border: 2px solid rgba(255,159,28,0.92);"></i>途经点</span>
|
<span class="legend-item"><i class="legend-dot" style="background: rgba(155,89,182,0.14); border: 2px solid rgba(155,89,182,0.92);"></i>必经站点</span>
|
</div>
|
|
<div class="map-shell">
|
<div class="map-toolbar">
|
<div class="map-toolbar-left">
|
<strong>楼层 {{ activeMapLev || '-' }}</strong>
|
<span style="color: var(--text-sub); font-size: 12px;">节点 {{ mapContext.nodes.length }}</span>
|
</div>
|
<div class="map-toolbar-right">
|
<el-button size="mini" @click="centerOnPath" :disabled="!hasActualPath">聚焦路径</el-button>
|
</div>
|
</div>
|
<div class="map-canvas-wrap"
|
ref="mapCanvasWrap"
|
:class="{ 'is-dragging': mapDragActive }"
|
@mousedown.left="beginMapDrag"
|
@wheel.prevent="handleMapWheel"
|
@dragstart.prevent>
|
<div v-if="!mapContext.nodes.length" class="empty-shell" style="margin: 14px;">请选择楼层或执行一次路径预览。</div>
|
<div v-else class="map-stage" :style="mapStageStyle" ref="mapStage">
|
<svg :width="mapContext.width" :height="mapContext.height">
|
<polyline v-if="preferredPathPolyline" :points="preferredPathPolyline" fill="none" stroke="var(--preferred)" stroke-width="4" stroke-dasharray="10 8" opacity="0.72"></polyline>
|
<polyline v-if="actualPathPolyline" :points="actualPathPolyline" fill="none" stroke="var(--path)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"></polyline>
|
</svg>
|
<div v-for="node in renderedMapNodes"
|
:key="'node-' + node.stationId"
|
class="map-node"
|
:class="node.classes"
|
:style="{ left: node.left, top: node.top }"
|
:title="node.title"
|
@click="pickNode(node)">
|
<div class="map-node-label" v-if="node.showLabel">{{ node.stationId }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="hint-card" v-if="previewResult && !hasActualPath" style="margin-top: 12px;">
|
<strong>当前无可行路径</strong>
|
<span>系统已经解析出命中规则和模板,但在现有地图、硬约束、途经点与实时条件下没有得到可行路线。可以先检查禁用站点、途经点顺序和楼层选择。</span>
|
</div>
|
|
<div class="rule-json" v-if="showRuleJson && activeRulePreviewJson">
|
{{ activeRulePreviewJson }}
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<el-dialog title="路径模板" :visible.sync="profileDialogVisible" width="820px" class="dialog-panel" append-to-body :destroy-on-close="true">
|
<div class="dialog-grid">
|
<el-form label-position="top" label-width="120px" style="width: 100%;">
|
<div class="section-card">
|
<h3>基础信息</h3>
|
<div class="dialog-grid">
|
<el-form-item label="模板编码">
|
<el-input v-model.trim="profileForm.profileCode" placeholder="如 default / loop_safe"></el-input>
|
</el-form-item>
|
<el-form-item label="模板名称">
|
<el-input v-model.trim="profileForm.profileName" placeholder="模板名称"></el-input>
|
</el-form-item>
|
<el-form-item label="优先级">
|
<el-input-number v-model="profileForm.priority" :min="1" :max="9999" style="width: 100%;"></el-input-number>
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-switch v-model="profileForm.status" :active-value="1" :inactive-value="0"></el-switch>
|
</el-form-item>
|
<el-form-item label="备注" class="span-2">
|
<el-input v-model.trim="profileForm.memo" placeholder="可填写适用场景"></el-input>
|
</el-form-item>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<h3>候选生成</h3>
|
<div class="dialog-grid">
|
<el-form-item label="最大深度"><el-input-number v-model="profileForm.config.calcMaxDepth" :min="20" :max="500" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="最大路径数"><el-input-number v-model="profileForm.config.calcMaxPaths" :min="10" :max="3000" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="最大代价"><el-input-number v-model="profileForm.config.calcMaxCost" :min="10" :max="5000" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="S1 保留 TopK"><el-input-number v-model="profileForm.config.s1TopK" :min="1" :max="50" style="width: 100%;"></el-input-number></el-form-item>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<h3>第一阶段静态评分</h3>
|
<div class="dialog-grid">
|
<el-form-item label="长度权重"><el-input-number v-model="profileForm.config.s1LenWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="拐点权重"><el-input-number v-model="profileForm.config.s1TurnWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="顶升权重"><el-input-number v-model="profileForm.config.s1LiftWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="偏离人工路径权重"><el-input-number v-model="profileForm.config.s1SoftDeviationWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="长度放宽比例"><el-input-number v-model="profileForm.config.s1MaxLenRatio" :min="1" :step="0.05" :precision="2" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="允许多拐点数"><el-input-number v-model="profileForm.config.s1MaxTurnDiff" :min="0" :max="20" style="width: 100%;"></el-input-number></el-form-item>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<h3>第二阶段动态评分</h3>
|
<div class="dialog-grid">
|
<el-form-item label="忙站权重"><el-input-number v-model="profileForm.config.s2BusyWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="堵塞权重"><el-input-number v-model="profileForm.config.s2RunBlockWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
<el-form-item label="环线负载权重"><el-input-number v-model="profileForm.config.s2LoopLoadWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
|
</div>
|
</div>
|
</el-form>
|
</div>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="profileDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="confirmProfileDialog">确定</el-button>
|
</span>
|
</el-dialog>
|
|
<el-dialog title="人工规则" :visible.sync="ruleDialogVisible" width="980px" class="dialog-panel" append-to-body :modal="false" :close-on-click-modal="false" :lock-scroll="false" :destroy-on-close="true" top="24px">
|
<el-form label-position="top" label-width="120px">
|
<div class="dialog-picked-bar">
|
<div class="help">{{ ruleDialogPickedHint }}</div>
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
<el-button size="mini" :disabled="!hasPickedStation" @click="applyPickedStation('ruleStart')">带入规则起点</el-button>
|
<el-button size="mini" :disabled="!hasPickedStation" @click="applyPickedStation('ruleEnd')">带入规则终点</el-button>
|
<el-button size="mini" type="primary" plain :disabled="!hasPickedStation" @click="applyPickedStation('mustPass')">加入必经</el-button>
|
<el-button size="mini" type="danger" plain :disabled="!hasPickedStation" @click="applyPickedStation('forbid')">加入禁用</el-button>
|
<el-button size="mini" type="warning" plain :disabled="!hasPickedStation" @click="applyPickedStation('waypoint')">加入途经点</el-button>
|
<el-button size="mini" type="success" plain :disabled="!hasPickedStation" @click="applyPickedStation('preferred')">加入偏好路径</el-button>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<h3>基础信息</h3>
|
<div class="dialog-grid">
|
<el-form-item label="规则编码">
|
<el-input v-model.trim="ruleForm.ruleCode" placeholder="如 R-IN-101-221"></el-input>
|
</el-form-item>
|
<el-form-item label="规则名称">
|
<el-input v-model.trim="ruleForm.ruleName" placeholder="规则名称"></el-input>
|
</el-form-item>
|
<el-form-item label="优先级">
|
<el-input-number v-model="ruleForm.priority" :min="1" :max="9999" style="width: 100%;"></el-input-number>
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-switch v-model="ruleForm.status" :active-value="1" :inactive-value="0"></el-switch>
|
</el-form-item>
|
<el-form-item label="起点站点">
|
<el-select v-model="ruleForm.startStationId" filterable clearable placeholder="可留空表示通配">
|
<el-option v-for="item in stationOptions" :key="'rs-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="终点站点">
|
<el-select v-model="ruleForm.endStationId" filterable clearable placeholder="可留空表示通配">
|
<el-option v-for="item in stationOptions" :key="'re-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="绑定模板">
|
<el-select v-model="ruleForm.profileCode" filterable clearable placeholder="为空时走默认模板">
|
<el-option v-for="item in profiles" :key="'rp-' + item.profileCode" :label="item.profileName + ' (' + item.profileCode + ')'" :value="item.profileCode"></el-option>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="场景类型">
|
<el-input v-model.trim="ruleForm.sceneType" placeholder="可选,如 inbound / outbound"></el-input>
|
</el-form-item>
|
<el-form-item label="备注" class="span-2">
|
<el-input v-model.trim="ruleForm.memo" placeholder="补充说明规则目的"></el-input>
|
</el-form-item>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<h3>硬约束</h3>
|
<div class="section-help">硬约束不会被动态评分推翻。支持必经站点、禁用站点、必经边、禁用边。</div>
|
<div class="dialog-grid">
|
<el-form-item label="必经站点" class="span-2">
|
<el-select v-model="ruleForm.hard.mustPassStations" multiple filterable collapse-tags placeholder="选择必经站点">
|
<el-option v-for="item in stationOptions" :key="'must-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<div class="sequence-card" v-if="ruleForm.hard.mustPassStations.length">
|
<div class="sequence-row" v-for="(stationId, index) in ruleForm.hard.mustPassStations" :key="'must-row-' + stationId + '-' + index">
|
<span class="sequence-index">{{ index + 1 }}</span>
|
<span class="sequence-label">{{ stationLabel(stationId) }}</span>
|
<div class="sequence-actions">
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.hard.mustPassStations, index)"></el-button>
|
</div>
|
</div>
|
</div>
|
</el-form-item>
|
<el-form-item label="禁用站点" class="span-2">
|
<el-select v-model="ruleForm.hard.forbidStations" multiple filterable collapse-tags placeholder="选择禁用站点">
|
<el-option v-for="item in stationOptions" :key="'forbid-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<div class="sequence-card" v-if="ruleForm.hard.forbidStations.length">
|
<div class="sequence-row" v-for="(stationId, index) in ruleForm.hard.forbidStations" :key="'forbid-row-' + stationId + '-' + index">
|
<span class="sequence-index">{{ index + 1 }}</span>
|
<span class="sequence-label">{{ stationLabel(stationId) }}</span>
|
<div class="sequence-actions">
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.hard.forbidStations, index)"></el-button>
|
</div>
|
</div>
|
</div>
|
</el-form-item>
|
<el-form-item label="必经边" class="span-2">
|
<el-input type="textarea" :rows="3" v-model="ruleForm.hard.mustPassEdgesText" placeholder="每行一条,如 101->102"></el-input>
|
</el-form-item>
|
<el-form-item label="禁用边" class="span-2">
|
<el-input type="textarea" :rows="3" v-model="ruleForm.hard.forbidEdgesText" placeholder="每行一条,如 215->216"></el-input>
|
</el-form-item>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<h3>关键途经点</h3>
|
<div class="section-help">按顺序经过这些站点。若开启严格模式,未命中途经点时会直接判定无路径。</div>
|
<div class="dialog-grid">
|
<el-form-item label="途经点序列" class="span-2">
|
<el-select v-model="ruleForm.waypoint.stations" multiple filterable collapse-tags placeholder="按顺序选择途经点">
|
<el-option v-for="item in stationOptions" :key="'wp-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<div class="sequence-card" v-if="ruleForm.waypoint.stations.length">
|
<div class="sequence-row" v-for="(stationId, index) in ruleForm.waypoint.stations" :key="'waypoint-row-' + stationId + '-' + index">
|
<span class="sequence-index">{{ index + 1 }}</span>
|
<span class="sequence-label">{{ stationLabel(stationId) }}</span>
|
<div class="sequence-actions">
|
<el-button size="mini" plain icon="el-icon-top" :disabled="index === 0" @click="moveListItem(ruleForm.waypoint.stations, index, -1)"></el-button>
|
<el-button size="mini" plain icon="el-icon-bottom" :disabled="index === ruleForm.waypoint.stations.length - 1" @click="moveListItem(ruleForm.waypoint.stations, index, 1)"></el-button>
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.waypoint.stations, index)"></el-button>
|
</div>
|
</div>
|
</div>
|
</el-form-item>
|
<el-form-item label="严格途经点模式">
|
<el-switch v-model="ruleForm.fallback.strictWaypoint"></el-switch>
|
</el-form-item>
|
</div>
|
</div>
|
|
<div class="section-card">
|
<div class="section-title-row">
|
<h3>软偏好路径</h3>
|
<div class="section-inline-actions">
|
<el-button size="mini" plain @click="importPreviewPathToRule" :disabled="!hasPreviewPath">导入当前预览</el-button>
|
<el-button size="mini" type="primary" plain @click="expandRuleSoftPreferredPath" :loading="softExpandLoading">按关键点展开</el-button>
|
<el-button size="mini" @click="clearSoftPreferredPath" :disabled="!(ruleForm.soft.keyStations.length || ruleForm.soft.preferredPath.length)">清空</el-button>
|
</div>
|
</div>
|
<div class="section-help">用于表达“人觉得正常应这样走”的推荐线路,算法允许偏离,但偏离会被罚分。</div>
|
<div class="dialog-grid">
|
<el-form-item label="关键点序列" class="span-2">
|
<el-select v-model="ruleForm.soft.keyStations" multiple filterable collapse-tags placeholder="只选关键点,系统会自动补全整条偏好路径">
|
<el-option v-for="item in stationOptions" :key="'soft-key-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<div class="sequence-card" v-if="ruleForm.soft.keyStations.length">
|
<div class="sequence-row" v-for="(stationId, index) in ruleForm.soft.keyStations" :key="'soft-key-row-' + stationId + '-' + index">
|
<span class="sequence-index">{{ index + 1 }}</span>
|
<span class="sequence-label">{{ stationLabel(stationId) }}</span>
|
<div class="sequence-actions">
|
<el-button size="mini" plain icon="el-icon-top" :disabled="index === 0" @click="moveListItem(ruleForm.soft.keyStations, index, -1)"></el-button>
|
<el-button size="mini" plain icon="el-icon-bottom" :disabled="index === ruleForm.soft.keyStations.length - 1" @click="moveListItem(ruleForm.soft.keyStations, index, 1)"></el-button>
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.soft.keyStations, index)"></el-button>
|
</div>
|
</div>
|
</div>
|
</el-form-item>
|
<el-form-item label="完整偏好路径" class="span-2">
|
<el-select v-model="ruleForm.soft.preferredPath" multiple filterable collapse-tags placeholder="按顺序选择偏好路径">
|
<el-option v-for="item in stationOptions" :key="'pref-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
|
</el-select>
|
<div class="sequence-card" v-if="ruleForm.soft.preferredPath.length">
|
<div class="sequence-row" v-for="(stationId, index) in ruleForm.soft.preferredPath" :key="'pref-row-' + stationId + '-' + index">
|
<span class="sequence-index">{{ index + 1 }}</span>
|
<span class="sequence-label">{{ stationLabel(stationId) }}</span>
|
<div class="sequence-actions">
|
<el-button size="mini" plain icon="el-icon-top" :disabled="index === 0" @click="moveListItem(ruleForm.soft.preferredPath, index, -1)"></el-button>
|
<el-button size="mini" plain icon="el-icon-bottom" :disabled="index === ruleForm.soft.preferredPath.length - 1" @click="moveListItem(ruleForm.soft.preferredPath, index, 1)"></el-button>
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.soft.preferredPath, index)"></el-button>
|
</div>
|
</div>
|
</div>
|
</el-form-item>
|
<el-form-item label="偏离罚分权重">
|
<el-input-number v-model="ruleForm.soft.deviationWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number>
|
</el-form-item>
|
<el-form-item label="允许偏离站点数">
|
<el-input-number v-model="ruleForm.soft.maxOffPathCount" :min="0" :max="50" style="width: 100%;"></el-input-number>
|
</el-form-item>
|
<el-form-item label="允许软偏好降级">
|
<el-switch v-model="ruleForm.fallback.allowSoftDegrade"></el-switch>
|
</el-form-item>
|
</div>
|
</div>
|
</el-form>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="ruleDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="confirmRuleDialog">确定</el-button>
|
</span>
|
</el-dialog>
|
</div>
|
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></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/common.js" charset="utf-8"></script>
|
<script type="text/javascript" src="../../static/js/stationPathPolicy/stationPathPolicy.js?v=20260316e"></script>
|
</body>
|
</html>
|