function createDefaultProfileConfig() { return { calcMaxDepth: 120, calcMaxPaths: 500, calcMaxCost: 300, s1TopK: 5, s1LenWeight: 1.0, s1TurnWeight: 3.0, s1LiftWeight: 8.0, s1SoftDeviationWeight: 4.0, s1MaxLenRatio: 1.15, s1MaxTurnDiff: 1, s2BusyWeight: 2.0, s2RunBlockWeight: 10.0, s2LoopLoadWeight: 12.0 } } function createDefaultProfile() { return { id: null, profileCode: '', profileName: '', priority: 100, status: 1, memo: '', config: createDefaultProfileConfig(), _originCode: null } } function createDefaultRule(defaultProfileCode) { return { id: null, ruleCode: '', ruleName: '', priority: 100, status: 1, sceneType: 'station', startStationId: null, endStationId: null, profileCode: defaultProfileCode || 'default', memo: '', hard: { mustPassStations: [], forbidStations: [], mustPassEdges: [], forbidEdges: [], mustPassEdgesText: '', forbidEdgesText: '' }, waypoint: { stations: [] }, soft: { keyStations: [], preferredPath: [], deviationWeight: 6.0, maxOffPathCount: 2 }, fallback: { strictWaypoint: false, allowSoftDegrade: true }, _originCode: null } } var app = new Vue({ el: '#app', data: function () { return { loading: false, saving: false, scoreMode: 'legacy', defaultProfileCode: 'default', profiles: [], rules: [], stations: [], levList: [], selectedProfileCode: '', selectedRuleCode: '', profileDialogVisible: false, ruleDialogVisible: false, profileForm: createDefaultProfile(), ruleForm: createDefaultRule('default'), previewForm: { startStationId: null, endStationId: null }, previewLoading: false, previewResult: null, activeMapLev: null, mapContext: { lev: null, width: 0, height: 0, nodes: [], nodeMap: {} }, mapZoomPercent: 100, pickedStationId: null, showRuleJson: false, showAllPathTags: false, softExpandLoading: false, mapDragActive: false, mapDragMoved: false, mapDragStartX: 0, mapDragStartY: 0, mapDragOriginPanX: 0, mapDragOriginPanY: 0, suppressNodeClick: false, mapPanX: 20, mapPanY: 20 } }, computed: { pickedStation: function () { return this.findStation(this.pickedStationId) }, hasPickedStation: function () { return this.pickedStationId != null }, stationMapById: function () { var map = {} ;(this.stations || []).forEach(function (station) { if (station && station.stationId != null) { map[String(station.stationId)] = station } }) return map }, stationOptions: function () { return (this.stations || []).map(function (station) { return { stationId: station.stationId, label: this.stationOptionLabel(station) } }.bind(this)) }, selectedRule: function () { var code = this.selectedRuleCode if (!code) { return null } for (var i = 0; i < this.rules.length; i++) { if (this.rules[i].ruleCode === code) { return this.rules[i] } } return null }, activeRuleForVisual: function () { if (this.ruleDialogVisible && this.ruleForm) { return this.ruleForm } if (this.selectedRule) { return this.selectedRule } if (this.previewResult && this.previewResult.resolvedPolicy && this.previewResult.resolvedPolicy.ruleConfig) { return { startStationId: this.previewForm.startStationId, endStationId: this.previewForm.endStationId, hard: this.previewResult.resolvedPolicy.ruleConfig.hard || this.defaultRule().hard, waypoint: this.previewResult.resolvedPolicy.ruleConfig.waypoint || this.defaultRule().waypoint, soft: this.previewResult.resolvedPolicy.ruleConfig.soft || this.defaultRule().soft, fallback: this.previewResult.resolvedPolicy.ruleConfig.fallback || this.defaultRule().fallback } } return null }, previewPathTags: function () { var result = this.previewResult if (!result || !result.pathStationIds) { return [] } return result.pathStationIds.map(function (stationId) { return { stationId: stationId, label: this.stationLabel(stationId) } }.bind(this)) }, visiblePreviewPathTags: function () { if (this.showAllPathTags) { return this.previewPathTags } return this.previewPathTags.slice(0, 14) }, hiddenPathTagCount: function () { return Math.max(this.previewPathTags.length - this.visiblePreviewPathTags.length, 0) }, hasActualPath: function () { return !!this.actualPathPolyline }, hasPreviewPath: function () { return !!(this.previewResult && this.previewResult.pathStationIds && this.previewResult.pathStationIds.length) }, pathStationLookup: function () { return this.buildLookup(this.previewResult && this.previewResult.pathStationIds) }, preferredStationLookup: function () { return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.soft ? this.activeRuleForVisual.soft.preferredPath : []) }, waypointStationLookup: function () { return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.waypoint ? this.activeRuleForVisual.waypoint.stations : []) }, forbidStationLookup: function () { return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.hard ? this.activeRuleForVisual.hard.forbidStations : []) }, mustPassStationLookup: function () { return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.hard ? this.activeRuleForVisual.hard.mustPassStations : []) }, renderedMapNodes: function () { var stationMap = this.stationMapById var pickedId = String(this.pickedStationId == null ? '' : this.pickedStationId) var startId = String(this.previewForm.startStationId == null ? '' : this.previewForm.startStationId) var endId = String(this.previewForm.endStationId == null ? '' : this.previewForm.endStationId) var pathLookup = this.pathStationLookup var preferredLookup = this.preferredStationLookup var waypointLookup = this.waypointStationLookup var forbidLookup = this.forbidStationLookup var mustPassLookup = this.mustPassStationLookup return (this.mapContext.nodes || []).map(function (node) { var stationId = node.stationId var key = String(stationId == null ? '' : stationId) var classes = [] if (pickedId && pickedId === key) { classes.push('is-picked') } if (startId && startId === key) { classes.push('is-start') } if (endId && endId === key) { classes.push('is-end') } if (pathLookup[key]) { classes.push('is-path') } if (preferredLookup[key]) { classes.push('is-preferred') } if (waypointLookup[key]) { classes.push('is-waypoint') } if (forbidLookup[key]) { classes.push('is-forbid') } if (mustPassLookup[key]) { classes.push('is-must-pass') } var station = stationMap[key] return { stationId: stationId, x: node.x, y: node.y, left: node.x + 'px', top: node.y + 'px', classes: classes, title: station ? this.stationOptionLabel(station) : String(stationId || ''), showLabel: !!(startId === key || endId === key || pathLookup[key] || waypointLookup[key] || forbidLookup[key] || mustPassLookup[key] || pickedId === key) } }.bind(this)) }, mapStageStyle: function () { return { width: this.mapContext.width + 'px', height: this.mapContext.height + 'px', transform: 'translate(' + this.mapPanX + 'px, ' + this.mapPanY + 'px) scale(' + (this.mapZoomPercent / 100) + ')' } }, actualPathPolyline: function () { var stationIds = this.previewResult && this.previewResult.pathStationIds ? this.previewResult.pathStationIds : [] return this.buildPolyline(stationIds) }, preferredPathPolyline: function () { var rule = this.activeRuleForVisual var preferredPath = rule && rule.soft ? rule.soft.preferredPath : [] return this.buildPolyline(preferredPath) }, activeRulePreviewJson: function () { if (!this.activeRuleForVisual) { return '' } return JSON.stringify(this.sanitizeRuleForSave(this.activeRuleForVisual), null, 2) }, ruleDialogPickedHint: function () { if (!this.ruleDialogVisible) { return '' } if (!this.pickedStation) { return '规则弹窗开启时,仍可在右侧地图点击站点,再一键加入必经/禁用/途经/偏好。' } return '当前地图选中:' + this.stationOptionLabel(this.pickedStation) } }, mounted: function () { this.loadData() }, beforeDestroy: function () { this.detachMapDragListeners() }, methods: { loadData: function () { var that = this this.loading = true $.ajax({ url: baseUrl + '/basStationPathPolicy/data/auth', method: 'GET', headers: { token: localStorage.getItem('token') }, success: function (res) { that.loading = false if (res.code !== 200) { that.$message.error('加载失败: ' + res.msg) return } var data = res.data || {} that.scoreMode = data.scoreMode || 'legacy' that.defaultProfileCode = data.defaultProfileCode || 'default' that.showRuleJson = false that.showAllPathTags = false that.stations = (data.stations || []).sort(function (a, b) { if ((a.stationLev || 0) !== (b.stationLev || 0)) { return (a.stationLev || 0) - (b.stationLev || 0) } return (a.stationId || 0) - (b.stationId || 0) }) that.levList = data.levList || [] that.profiles = (data.profiles || []).map(that.normalizeProfile) that.rules = (data.rules || []).map(that.normalizeRule) if (!that.defaultProfileCode && that.profiles.length) { that.defaultProfileCode = that.profiles[0].profileCode } if (!that.selectedProfileCode && that.profiles.length) { that.selectedProfileCode = that.defaultProfileCode || that.profiles[0].profileCode } if (!that.selectedRuleCode && that.rules.length) { that.selectedRuleCode = that.rules[0].ruleCode } if (that.selectedRule) { that.loadMapByRule(that.selectedRule) } else if (that.levList.length && !that.activeMapLev) { that.loadMapByLev(that.levList[0]) } }, error: function () { that.loading = false that.$message.error('加载请求异常') } }) }, saveAll: function () { if (!this.profiles.length) { this.$message.warning('至少需要保留一个模板') return } var hasDefault = this.profiles.some(function (item) { return item.profileCode === this.defaultProfileCode }.bind(this)) if (!hasDefault) { this.$message.warning('默认模板编码没有对应模板') return } var payload = { scoreMode: this.scoreMode, defaultProfileCode: this.defaultProfileCode, profiles: this.profiles.map(this.sanitizeProfileForSave), rules: this.rules.map(this.sanitizeRuleForSave) } var that = this this.saving = true $.ajax({ url: baseUrl + '/basStationPathPolicy/save/auth', method: 'POST', headers: { token: localStorage.getItem('token') }, contentType: 'application/json', data: JSON.stringify(payload), success: function (res) { that.saving = false if (res.code !== 200) { that.$message.error('保存失败: ' + res.msg) return } that.$message.success('保存成功') that.loadData() }, error: function () { that.saving = false that.$message.error('保存请求异常') } }) }, openProfileDialog: function (item) { this.profileForm = item ? this.cloneProfileModel(item) : this.defaultProfile() this.profileDialogVisible = true }, confirmProfileDialog: function () { var form = this.profileForm if (this.isBlank(form.profileCode)) { this.$message.warning('模板编码不能为空') return } if (this.isBlank(form.profileName)) { this.$message.warning('模板名称不能为空') return } var existsIndex = this.findProfileIndex(form.profileCode) if (existsIndex >= 0 && (!form._originCode || form._originCode !== form.profileCode)) { this.$message.warning('模板编码已存在') return } var profile = this.cloneProfileModel(form) delete profile._originCode if (form._originCode) { var originIndex = this.findProfileIndex(form._originCode) if (originIndex >= 0) { this.$set(this.profiles, originIndex, profile) if (this.defaultProfileCode === form._originCode) { this.defaultProfileCode = profile.profileCode } if (this.selectedProfileCode === form._originCode) { this.selectedProfileCode = profile.profileCode } this.rules.forEach(function (rule) { if (rule.profileCode === form._originCode) { rule.profileCode = profile.profileCode } }) } } else { this.profiles.push(profile) this.selectedProfileCode = profile.profileCode if (!this.defaultProfileCode) { this.defaultProfileCode = profile.profileCode } } this.profileDialogVisible = false }, cloneProfile: function (item) { var copy = this.cloneProfileModel(item) copy.profileCode = item.profileCode + '_copy' copy.profileName = item.profileName + ' - 副本' copy._originCode = null this.profileForm = copy this.profileDialogVisible = true }, removeProfile: function (item) { if (!item) { return } if (this.profiles.length <= 1) { this.$message.warning('至少保留一个模板') return } var used = this.rules.some(function (rule) { return rule.profileCode === item.profileCode }) if (used) { this.$message.warning('该模板仍被规则引用,不能删除') return } var that = this this.$confirm('确认删除模板 ' + item.profileCode + ' 吗?', '提示', { type: 'warning' }) .then(function () { that.profiles = that.profiles.filter(function (profile) { return profile.profileCode !== item.profileCode }) if (that.defaultProfileCode === item.profileCode) { that.defaultProfileCode = that.profiles[0] ? that.profiles[0].profileCode : '' } if (that.selectedProfileCode === item.profileCode) { that.selectedProfileCode = that.defaultProfileCode } }) .catch(function () {}) }, openRuleDialog: function (item) { this.ruleForm = item ? this.cloneRuleModel(item) : this.defaultRule() this.ruleDialogVisible = true }, confirmRuleDialog: function () { var form = this.ruleForm if (this.isBlank(form.ruleCode)) { this.$message.warning('规则编码不能为空') return } if (this.isBlank(form.ruleName)) { this.$message.warning('规则名称不能为空') return } var existsIndex = this.findRuleIndex(form.ruleCode) if (existsIndex >= 0 && (!form._originCode || form._originCode !== form.ruleCode)) { this.$message.warning('规则编码已存在') return } var rule = this.cloneRuleModel(form) delete rule._originCode if (form._originCode) { var originIndex = this.findRuleIndex(form._originCode) if (originIndex >= 0) { this.$set(this.rules, originIndex, rule) if (this.selectedRuleCode === form._originCode) { this.selectedRuleCode = rule.ruleCode } } } else { this.rules.push(rule) this.selectedRuleCode = rule.ruleCode } this.ruleDialogVisible = false this.loadMapByRule(rule) }, importPreviewPathToRule: function () { if (!this.hasPreviewPath) { this.$message.warning('请先在右侧完成一次路径预览') return } if (!this.ruleForm || !this.ruleForm.soft) { return } this.ruleForm.soft.preferredPath = (this.previewResult.pathStationIds || []).slice() this.ruleForm.soft.keyStations = [] if (!this.ruleForm.startStationId && this.previewForm.startStationId) { this.ruleForm.startStationId = this.previewForm.startStationId } if (!this.ruleForm.endStationId && this.previewForm.endStationId) { this.ruleForm.endStationId = this.previewForm.endStationId } this.$message.success('已导入当前预览路径,共 ' + this.ruleForm.soft.preferredPath.length + ' 个站点') }, expandRuleSoftPreferredPath: function () { if (this.softExpandLoading) { return } if (!this.ruleForm || !this.ruleForm.soft) { return } var startStationId = this.toNumberSafe(this.ruleForm.startStationId) || this.toNumberSafe(this.previewForm.startStationId) var endStationId = this.toNumberSafe(this.ruleForm.endStationId) || this.toNumberSafe(this.previewForm.endStationId) if (startStationId == null || endStationId == null) { this.$message.warning('请先为规则设置起点和终点,或先在右侧预览一条路径') return } var keyStations = this.uniqueNumbers((this.ruleForm.soft.keyStations || []).slice()) var that = this this.softExpandLoading = true $.ajax({ url: baseUrl + '/basStationPathPolicy/expandSoftPath/auth', method: 'POST', headers: { token: localStorage.getItem('token') }, contentType: 'application/json', data: JSON.stringify({ startStationId: startStationId, endStationId: endStationId, keyStations: keyStations }), success: function (res) { that.softExpandLoading = false if (res.code !== 200) { that.$message.error('展开失败: ' + res.msg) return } var data = res.data || {} var pathStationIds = data.pathStationIds || [] if (!pathStationIds.length) { that.$message.warning('没有生成可用的软偏好路径') return } that.ruleForm.startStationId = startStationId that.ruleForm.endStationId = endStationId that.ruleForm.soft.keyStations = keyStations that.ruleForm.soft.preferredPath = pathStationIds.slice() that.$message.success(keyStations.length ? '已按关键点展开完整软偏好路径' : '已按起终点生成完整软偏好路径') }, error: function () { that.softExpandLoading = false that.$message.error('展开软偏好路径请求异常') } }) }, clearSoftPreferredPath: function () { if (!this.ruleForm || !this.ruleForm.soft) { return } this.ruleForm.soft.keyStations = [] this.ruleForm.soft.preferredPath = [] }, cloneRule: function (item) { var copy = this.cloneRuleModel(item) copy.ruleCode = item.ruleCode + '_copy' copy.ruleName = item.ruleName + ' - 副本' copy._originCode = null this.ruleForm = copy this.ruleDialogVisible = true }, removeRule: function (item) { var that = this this.$confirm('确认删除规则 ' + item.ruleCode + ' 吗?', '提示', { type: 'warning' }) .then(function () { that.rules = that.rules.filter(function (rule) { return rule.ruleCode !== item.ruleCode }) if (that.selectedRuleCode === item.ruleCode) { that.selectedRuleCode = that.rules[0] ? that.rules[0].ruleCode : '' } }) .catch(function () {}) }, selectRule: function (item) { this.selectedRuleCode = item.ruleCode this.loadMapByRule(item) }, previewRule: function (item) { this.selectRule(item) this.previewForm.startStationId = item.startStationId this.previewForm.endStationId = item.endStationId this.showRuleJson = false this.showAllPathTags = false if (item.startStationId && item.endStationId) { this.loadPreview() } }, loadPreview: function () { if (!this.previewForm.startStationId || !this.previewForm.endStationId) { this.$message.warning('请选择起点和终点') return } var that = this this.previewLoading = true $.ajax({ url: baseUrl + '/basStationPathPolicy/preview/auth', method: 'GET', headers: { token: localStorage.getItem('token') }, data: { startStationId: this.previewForm.startStationId, endStationId: this.previewForm.endStationId }, success: function (res) { that.previewLoading = false if (res.code !== 200) { that.$message.error('预览失败: ' + res.msg) return } that.showRuleJson = false that.showAllPathTags = false that.previewResult = res.data || null if (that.previewResult && that.previewResult.lev) { that.activeMapLev = that.previewResult.lev } if (that.previewResult && that.previewResult.mapData) { that.applyMapData(that.previewResult.mapData, that.previewResult.lev) } else if (that.activeMapLev && that.mapContext.lev !== that.activeMapLev) { that.loadMapByLev(that.activeMapLev) } that.$nextTick(function () { that.centerOnPath() }) if (!that.hasActualPath) { that.$message.warning('当前起终点未计算到可行路径,请检查规则或楼层地图') } }, error: function () { that.previewLoading = false that.$message.error('预览请求异常') } }) }, loadMapByRule: function (rule) { if (!rule) { return } var station = this.findStation(rule.startStationId || rule.endStationId) if (station && station.stationLev && this.mapContext.lev !== station.stationLev) { this.loadMapByLev(station.stationLev) } }, loadMapByLev: function (lev) { if (!lev) { return } if (this.mapContext.lev === lev && this.mapContext.nodes && this.mapContext.nodes.length) { return } var that = this $.ajax({ url: baseUrl + '/basMap/lev/' + lev + '/auth', method: 'GET', headers: { token: localStorage.getItem('token') }, success: function (res) { if (res.code !== 200) { that.$message.error('加载楼层地图失败: ' + res.msg) return } that.applyMapData(res.data, lev) }, error: function () { that.$message.error('加载楼层地图请求异常') } }) }, applyMapData: function (mapData, lev) { var parsed = mapData if (typeof mapData === 'string') { try { parsed = JSON.parse(mapData) } catch (e) { parsed = [] } } if (!Array.isArray(parsed)) { this.mapContext = { lev: lev, width: 0, height: 0, nodes: [], nodeMap: {} } return } var cellStep = 26 var margin = 22 var nodes = [] var nodeMap = {} var rows = parsed.length var cols = 0 for (var r = 0; r < parsed.length; r++) { var row = parsed[r] || [] cols = Math.max(cols, row.length) for (var c = 0; c < row.length; c++) { var cell = row[c] || {} var type = cell.type var mergeType = cell.mergeType if (!(type === 'devp' || (type === 'merge' && mergeType === 'devp'))) { continue } var value = this.parseJson(cell.value) var stationId = value ? value.stationId : null if (!stationId) { continue } var node = { stationId: stationId, row: r, col: c, x: margin + c * cellStep, y: margin + r * cellStep, stationAlias: value.stationAlias || '', isLiftTransfer: value.isLiftTransfer === 1 || value.isLiftTransfer === true } nodes.push(node) nodeMap[String(stationId)] = node } } this.mapContext = { lev: lev, width: margin * 2 + Math.max(cols - 1, 0) * cellStep + 40, height: margin * 2 + Math.max(rows - 1, 0) * cellStep + 40, nodes: nodes, nodeMap: nodeMap } this.fitMap() }, fitMap: function () { var wrap = this.$refs.mapCanvasWrap if (!wrap || !this.mapContext.width || !this.mapContext.height) { return } var bounds = this.getMapContentBounds() var contentWidth = Math.max((bounds.maxX - bounds.minX), 1) var contentHeight = Math.max((bounds.maxY - bounds.minY), 1) var usableWidth = Math.max(wrap.clientWidth - 30, 320) var usableHeight = Math.max(wrap.clientHeight - 30, 240) var scaleX = usableWidth / contentWidth var scaleY = usableHeight / contentHeight var scale = Math.min(scaleX, scaleY, 1.7) var zoomPercent = Math.max(60, Math.min(220, Math.floor(scale * 100))) var centerX = (bounds.minX + bounds.maxX) / 2 var centerY = (bounds.minY + bounds.maxY) / 2 this.mapZoomPercent = zoomPercent this.mapPanX = Math.round(wrap.clientWidth / 2 - centerX * (zoomPercent / 100)) this.mapPanY = Math.round(wrap.clientHeight / 2 - centerY * (zoomPercent / 100)) }, centerOnPath: function () { var wrap = this.$refs.mapCanvasWrap var stage = this.$refs.mapStage if (!wrap || !stage || !this.actualPathPolyline) { return } var stationIds = this.previewResult && this.previewResult.pathStationIds ? this.previewResult.pathStationIds : [] var points = this.resolvePoints(stationIds) if (!points.length) { return } var minX = points[0].x var maxX = points[0].x var minY = points[0].y var maxY = points[0].y points.forEach(function (point) { minX = Math.min(minX, point.x) maxX = Math.max(maxX, point.x) minY = Math.min(minY, point.y) maxY = Math.max(maxY, point.y) }) var scale = this.mapZoomPercent / 100 this.mapPanX = Math.round(wrap.clientWidth / 2 - ((minX + maxX) / 2) * scale) this.mapPanY = Math.round(wrap.clientHeight / 2 - ((minY + maxY) / 2) * scale) }, resetPreview: function () { this.previewForm.startStationId = null this.previewForm.endStationId = null this.previewResult = null this.pickedStationId = null this.showRuleJson = false this.showAllPathTags = false }, updateMapZoom: function (nextPercent) { var wrap = this.$refs.mapCanvasWrap var zoomPercent = this.toNumberSafe(nextPercent) if (zoomPercent == null) { return } zoomPercent = Math.max(60, Math.min(220, zoomPercent)) if (!wrap || !this.mapContext.width || !this.mapContext.height) { this.mapZoomPercent = zoomPercent return } this.setMapZoomAroundPoint(zoomPercent, wrap.clientWidth / 2, wrap.clientHeight / 2) }, setMapZoomAroundPoint: function (nextPercent, anchorX, anchorY) { var currentPercent = this.mapZoomPercent if (!currentPercent || currentPercent === nextPercent) { this.mapZoomPercent = nextPercent return } var currentScale = currentPercent / 100 var nextScale = nextPercent / 100 if (!currentScale || !nextScale) { this.mapZoomPercent = nextPercent return } var mapX = (anchorX - this.mapPanX) / currentScale var mapY = (anchorY - this.mapPanY) / currentScale this.mapZoomPercent = nextPercent this.mapPanX = Math.round(anchorX - mapX * nextScale) this.mapPanY = Math.round(anchorY - mapY * nextScale) }, handleMapWheel: function (event) { if (!this.mapContext.nodes.length) { return } if (event.ctrlKey || event.metaKey) { var wrap = this.$refs.mapCanvasWrap if (!wrap) { return } var rect = wrap.getBoundingClientRect() var delta = event.deltaY < 0 ? 10 : -10 var nextPercent = Math.max(60, Math.min(220, this.mapZoomPercent + delta)) this.setMapZoomAroundPoint(nextPercent, event.clientX - rect.left, event.clientY - rect.top) return } this.mapPanX -= event.deltaX this.mapPanY -= event.deltaY }, beginMapDrag: function (event) { var wrap = this.$refs.mapCanvasWrap if (!wrap || !this.mapContext.nodes.length) { return } if (event && event.button != null && event.button !== 0) { return } this.mapDragActive = true this.mapDragMoved = false this.mapDragStartX = event.clientX this.mapDragStartY = event.clientY this.mapDragOriginPanX = this.mapPanX this.mapDragOriginPanY = this.mapPanY document.addEventListener('mousemove', this.handleMapDragMove) document.addEventListener('mouseup', this.endMapDrag) if (event.preventDefault) { event.preventDefault() } }, handleMapDragMove: function (event) { if (!this.mapDragActive) { return } var wrap = this.$refs.mapCanvasWrap if (!wrap) { this.endMapDrag() return } var deltaX = event.clientX - this.mapDragStartX var deltaY = event.clientY - this.mapDragStartY if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) { this.mapDragMoved = true } this.mapPanX = this.mapDragOriginPanX + deltaX this.mapPanY = this.mapDragOriginPanY + deltaY }, endMapDrag: function () { if (!this.mapDragActive) { this.detachMapDragListeners() return } this.mapDragActive = false this.detachMapDragListeners() if (this.mapDragMoved) { this.suppressNodeClick = true var that = this window.setTimeout(function () { that.suppressNodeClick = false }, 0) } }, detachMapDragListeners: function () { document.removeEventListener('mousemove', this.handleMapDragMove) document.removeEventListener('mouseup', this.endMapDrag) }, pickNode: function (node) { if (this.suppressNodeClick) { return } this.pickedStationId = node.stationId }, applyPickedStation: function (field) { if (!this.pickedStationId) { return } if (field === 'start') { this.previewForm.startStationId = this.pickedStationId } else if (field === 'end') { this.previewForm.endStationId = this.pickedStationId } else if (field === 'ruleStart' && this.ruleForm) { this.ruleForm.startStationId = this.pickedStationId } else if (field === 'ruleEnd' && this.ruleForm) { this.ruleForm.endStationId = this.pickedStationId } else if (field === 'mustPass' && this.ruleForm) { this.pushUniqueStation(this.ruleForm.hard.mustPassStations, this.pickedStationId) } else if (field === 'forbid' && this.ruleForm) { this.pushUniqueStation(this.ruleForm.hard.forbidStations, this.pickedStationId) } else if (field === 'waypoint' && this.ruleForm) { this.pushUniqueStation(this.ruleForm.waypoint.stations, this.pickedStationId) } else if (field === 'preferred' && this.ruleForm) { this.pushUniqueStation(this.ruleForm.soft.preferredPath, this.pickedStationId) } }, pushUniqueStation: function (list, stationId) { if (!Array.isArray(list)) { return } var value = this.toNumberSafe(stationId) if (value == null) { return } var exists = list.some(function (item) { return String(item) === String(value) }) if (!exists) { list.push(value) } }, moveListItem: function (list, index, offset) { if (!Array.isArray(list)) { return } var targetIndex = index + offset if (index < 0 || targetIndex < 0 || index >= list.length || targetIndex >= list.length) { return } var moved = list.splice(index, 1)[0] list.splice(targetIndex, 0, moved) }, removeListItem: function (list, index) { if (!Array.isArray(list) || index < 0 || index >= list.length) { return } list.splice(index, 1) }, buildLookup: function (list) { var lookup = {} ;(list || []).forEach(function (item) { if (item != null && item !== '') { lookup[String(item)] = true } }) return lookup }, nodeStyle: function (node) { return { left: node.x + 'px', top: node.y + 'px' } }, nodeClasses: function (node) { var stationId = node.stationId var classes = [] if (String(this.pickedStationId || '') === String(stationId)) { classes.push('is-picked') } if (this.previewForm.startStationId === stationId) { classes.push('is-start') } if (this.previewForm.endStationId === stationId) { classes.push('is-end') } if (this.pathStationSet()[stationId]) { classes.push('is-path') } if (this.preferredStationSet()[stationId]) { classes.push('is-preferred') } if (this.waypointStationSet()[stationId]) { classes.push('is-waypoint') } if (this.forbidStationSet()[stationId]) { classes.push('is-forbid') } if (this.mustPassStationSet()[stationId]) { classes.push('is-must-pass') } return classes }, showNodeLabel: function (node) { var stationId = node.stationId return !!( this.previewForm.startStationId === stationId || this.previewForm.endStationId === stationId || this.pathStationSet()[stationId] || this.waypointStationSet()[stationId] || this.forbidStationSet()[stationId] || this.mustPassStationSet()[stationId] || String(this.pickedStationId || '') === String(stationId) ) }, stationNodeTitle: function (node) { return this.stationLabel(node.stationId) }, stationOptionLabel: function (station) { if (!station) { return '' } var alias = station.stationAlias ? ' · ' + station.stationAlias : '' return 'L' + (station.stationLev || '-') + ' · ' + station.stationId + alias }, stationLabel: function (stationId) { var station = this.findStation(stationId) return station ? this.stationOptionLabel(station) : String(stationId || '') }, findStation: function (stationId) { if (stationId == null) { return null } return this.stationMapById[String(stationId)] || null }, routeLabel: function (rule) { if (!rule.startStationId && !rule.endStationId) { return '通配规则' } return (rule.startStationId || '*') + ' → ' + (rule.endStationId || '*') }, ruleSummaryCount: function (hard) { hard = hard || {} return (hard.mustPassStations || []).length + (hard.forbidStations || []).length + (hard.mustPassEdges || []).length + (hard.forbidEdges || []).length }, pathStationSet: function () { return this.pathStationLookup }, preferredStationSet: function () { return this.preferredStationLookup }, waypointStationSet: function () { return this.waypointStationLookup }, forbidStationSet: function () { return this.forbidStationLookup }, mustPassStationSet: function () { return this.mustPassStationLookup }, buildPolyline: function (stationIds) { var points = this.resolvePoints(stationIds) if (!points.length) { return '' } return points.map(function (point) { return point.x + ',' + point.y }).join(' ') }, resolvePoints: function (stationIds) { var points = [] var map = this.mapContext.nodeMap || {} ;(stationIds || []).forEach(function (stationId) { var node = map[String(stationId)] if (node) { points.push({ x: node.x, y: node.y }) } }) return points }, getMapContentBounds: function () { var nodes = this.mapContext && this.mapContext.nodes ? this.mapContext.nodes : [] if (!nodes.length) { return { minX: 0, maxX: this.mapContext.width || 0, minY: 0, maxY: this.mapContext.height || 0 } } var minX = null var maxX = null var minY = null var maxY = null nodes.forEach(function (node) { if (!node) { return } minX = minX == null ? node.x : Math.min(minX, node.x) maxX = maxX == null ? node.x : Math.max(maxX, node.x) minY = minY == null ? node.y : Math.min(minY, node.y) maxY = maxY == null ? node.y : Math.max(maxY, node.y) }) var padding = 48 return { minX: Math.max((minX == null ? 0 : minX) - padding, 0), maxX: Math.min((maxX == null ? 0 : maxX) + padding, this.mapContext.width || Number.MAX_SAFE_INTEGER), minY: Math.max((minY == null ? 0 : minY) - padding, 0), maxY: Math.min((maxY == null ? 0 : maxY) + padding, this.mapContext.height || Number.MAX_SAFE_INTEGER) } }, defaultProfileConfig: function () { return createDefaultProfileConfig() }, defaultProfile: function () { return createDefaultProfile() }, defaultRule: function () { return createDefaultRule(this.defaultProfileCode || 'default') }, normalizeProfile: function (raw) { var config = Object.assign({}, this.defaultProfileConfig(), this.parseJson(raw.configJson) || raw.config || {}) return { id: raw.id || null, profileCode: raw.profileCode || '', profileName: raw.profileName || raw.profileCode || '', priority: raw.priority == null ? 100 : Number(raw.priority), status: raw.status == null ? 1 : Number(raw.status), memo: raw.memo || '', config: config } }, normalizeRule: function (raw) { var rule = this.defaultRule() var hard = Object.assign({}, rule.hard, this.parseJson(raw.hardJson) || raw.hard || {}) var waypoint = Object.assign({}, rule.waypoint, this.parseJson(raw.waypointJson) || raw.waypoint || {}) var soft = Object.assign({}, rule.soft, this.parseJson(raw.softJson) || raw.soft || {}) var fallback = Object.assign({}, rule.fallback, this.parseJson(raw.fallbackJson) || raw.fallback || {}) hard.mustPassEdgesText = (hard.mustPassEdges || []).join('\n') hard.forbidEdgesText = (hard.forbidEdges || []).join('\n') rule.id = raw.id || null rule.ruleCode = raw.ruleCode || '' rule.ruleName = raw.ruleName || raw.ruleCode || '' rule.priority = raw.priority == null ? 100 : Number(raw.priority) rule.status = raw.status == null ? 1 : Number(raw.status) rule.sceneType = raw.sceneType || 'station' rule.startStationId = raw.startStationId == null ? null : Number(raw.startStationId) rule.endStationId = raw.endStationId == null ? null : Number(raw.endStationId) rule.profileCode = raw.profileCode || '' rule.memo = raw.memo || '' rule.hard = hard rule.waypoint = waypoint rule.soft = soft rule.fallback = fallback return rule }, cloneProfileModel: function (item) { var model = this.normalizeProfile(item) model._originCode = item.profileCode || item._originCode || null return JSON.parse(JSON.stringify(model)) }, cloneRuleModel: function (item) { var model = this.normalizeRule(item) model._originCode = item.ruleCode || item._originCode || null return JSON.parse(JSON.stringify(model)) }, sanitizeProfileForSave: function (item) { return { id: item.id || null, profileCode: item.profileCode, profileName: item.profileName, priority: Number(item.priority || 100), status: Number(item.status || 0), memo: item.memo || '', config: Object.assign({}, item.config || {}) } }, sanitizeRuleForSave: function (item) { var hard = Object.assign({}, item.hard || {}) hard.mustPassEdges = this.parseLines(hard.mustPassEdgesText || hard.mustPassEdges || []) hard.forbidEdges = this.parseLines(hard.forbidEdgesText || hard.forbidEdges || []) delete hard.mustPassEdgesText delete hard.forbidEdgesText return { id: item.id || null, ruleCode: item.ruleCode, ruleName: item.ruleName, priority: Number(item.priority || 100), status: Number(item.status || 0), sceneType: item.sceneType || '', startStationId: item.startStationId == null ? null : Number(item.startStationId), endStationId: item.endStationId == null ? null : Number(item.endStationId), profileCode: item.profileCode || '', memo: item.memo || '', hard: { mustPassStations: this.uniqueNumbers(hard.mustPassStations || []), forbidStations: this.uniqueNumbers(hard.forbidStations || []), mustPassEdges: hard.mustPassEdges, forbidEdges: hard.forbidEdges }, waypoint: { stations: this.uniqueNumbers((item.waypoint && item.waypoint.stations) || []) }, soft: { keyStations: this.uniqueNumbers((item.soft && item.soft.keyStations) || []), preferredPath: this.uniqueNumbers((item.soft && item.soft.preferredPath) || []), deviationWeight: this.toNumberSafe(item.soft && item.soft.deviationWeight) || 0, maxOffPathCount: this.toNumberSafe(item.soft && item.soft.maxOffPathCount) || 0 }, fallback: { strictWaypoint: !!(item.fallback && item.fallback.strictWaypoint), allowSoftDegrade: !(item.fallback && item.fallback.allowSoftDegrade === false) } } }, findProfileIndex: function (profileCode) { for (var i = 0; i < this.profiles.length; i++) { if (this.profiles[i].profileCode === profileCode) { return i } } return -1 }, findRuleIndex: function (ruleCode) { for (var i = 0; i < this.rules.length; i++) { if (this.rules[i].ruleCode === ruleCode) { return i } } return -1 }, parseJson: function (value) { if (!value) { return null } if (typeof value === 'object') { return value } try { return JSON.parse(value) } catch (e) { return null } }, parseLines: function (value) { if (Array.isArray(value)) { return value.filter(function (item) { return !!String(item || '').trim() }) } return String(value || '') .split('\n') .map(function (item) { return item.trim() }) .filter(function (item) { return !!item }) }, toNumberSafe: function (value) { if (value == null || value === '') { return null } var num = Number(value) return isNaN(num) ? null : num }, notNull: function (value) { return value != null }, uniqueNumbers: function (list) { var result = [] var seen = {} ;(list || []).forEach(function (item) { var num = this.toNumberSafe(item) if (num == null) { return } if (!seen[String(num)]) { seen[String(num)] = true result.push(num) } }.bind(this)) return result }, isBlank: function (text) { return text == null || String(text).trim() === '' } } })