zhaoyk 3 gadi atpakaļ
revīzija
c5bb7ba5e0
38 mainītis faili ar 19826 papildinājumiem un 0 dzēšanām
  1. 24 0
      adm_comp/README.md
  2. 4 0
      adm_comp/babel.config.js
  3. 13775 0
      adm_comp/package-lock.json
  4. 74 0
      adm_comp/package.json
  5. BIN
      adm_comp/public/favicon.ico
  6. 18 0
      adm_comp/public/index.html
  7. 2 0
      adm_comp/public/serverAddress.js
  8. 45 0
      adm_comp/src/App.vue
  9. BIN
      adm_comp/src/assets/logo.png
  10. 37 0
      adm_comp/src/common/Common.ts
  11. 225 0
      adm_comp/src/common/Diagram.ts
  12. 365 0
      adm_comp/src/common/Map.ts
  13. 264 0
      adm_comp/src/common/Map0.ts
  14. 151 0
      adm_comp/src/components/FilterPanel.vue
  15. 60 0
      adm_comp/src/lib/ContainerRefPoint.ts
  16. 444 0
      adm_comp/src/lib/DiagramEditor.ts
  17. 281 0
      adm_comp/src/lib/DiagramModel.ts
  18. 106 0
      adm_comp/src/lib/Editor.ts
  19. 324 0
      adm_comp/src/lib/LegendEditor.ts
  20. 486 0
      adm_comp/src/lib/TemplateEditor.ts
  21. 143 0
      adm_comp/src/lib/UIUtils.ts
  22. 22 0
      adm_comp/src/main.ts
  23. 20 0
      adm_comp/src/polyfills.ts
  24. 55 0
      adm_comp/src/router/index.ts
  25. 13 0
      adm_comp/src/shims-tsx.d.ts
  26. 4 0
      adm_comp/src/shims-vue.d.ts
  27. 11 0
      adm_comp/src/store/index.ts
  28. 1 0
      adm_comp/src/styles/var.scss
  29. 413 0
      adm_comp/src/views/Diagram.vue
  30. 583 0
      adm_comp/src/views/DiagramLegend.vue
  31. 909 0
      adm_comp/src/views/DiagramTemplate.vue
  32. 44 0
      adm_comp/src/views/DiagramView.vue
  33. 259 0
      adm_comp/src/views/Jobs.vue
  34. 204 0
      adm_comp/src/views/LegendIcon.vue
  35. 117 0
      adm_comp/src/views/Map.vue
  36. 60 0
      adm_comp/src/views/Map0.vue
  37. 40 0
      adm_comp/tsconfig.json
  38. 243 0
      adm_comp/vue.config.js

+ 24 - 0
adm_comp/README.md

@@ -0,0 +1,24 @@
+# zhifa_analy
+
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm run serve
+```
+
+### Compiles and minifies for production
+```
+npm run build
+```
+
+### Lints and fixes files
+```
+npm run lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 4 - 0
adm_comp/babel.config.js

@@ -0,0 +1,4 @@
+module.exports = {
+  presets: ["@vue/cli-plugin-babel/preset"],
+  sourceType: "unambiguous"
+};

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 13775 - 0
adm_comp/package-lock.json


+ 74 - 0
adm_comp/package.json

@@ -0,0 +1,74 @@
+{
+  "name": "adm",
+  "description": "数字化交付开发测试",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "axios": "^0.19.2",
+    "core-js": "^3.6.5",
+    "vue": "^2.6.11",
+    "vue-class-component": "^7.2.3",
+    "vue-property-decorator": "^8.4.2",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0",
+    "element-ui": "^2.13.2",
+    "lodash": "^4.17.19",
+    "reflect-metadata": "^0.1.13",
+    "echarts": "^4.4.0"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^2.33.0",
+    "@typescript-eslint/parser": "^2.33.0",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-typescript": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "@vue/eslint-config-typescript": "^5.0.2",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.1.3",
+    "eslint-plugin-vue": "^6.2.2",
+    "prettier": "^1.19.1",
+    "mockjs": "^1.0.1-beta3",
+    "node-sass": "^4.12.0",
+    "sass-loader": "^8.0.2",
+    "typescript": "~3.9.3",
+    "vue-template-compiler": "^2.6.11",
+    "@types/lodash": "^4.14.158",
+    "compression-webpack-plugin": "^4.0.0",
+    "webpack-bundle-analyzer": "^3.8.0",
+    "webpack-theme-color-replacer": "^1.3.13"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended",
+      "@vue/typescript/recommended",
+      "@vue/prettier",
+      "@vue/prettier/@typescript-eslint"
+    ],
+    "parserOptions": {
+      "ecmaVersion": 2020
+    },
+    "rules": {
+      "vue/require-v-for-key": "off",
+      "no-mixed-spaces-and-tabs": "off"
+    }
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
adm_comp/public/favicon.ico


+ 18 - 0
adm_comp/public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= htmlWebpackPlugin.options.title %></title>
+	<script src="<%= BASE_URL %>serverAddress.js" ></script>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 2 - 0
adm_comp/public/serverAddress.js

@@ -0,0 +1,2 @@
+const serverAddress = 'http://192.168.0.48:8833';
+window['serverAddress'] = serverAddress;

+ 45 - 0
adm_comp/src/App.vue

@@ -0,0 +1,45 @@
+<template>
+	<!-- <router-view /> -->
+	<el-container>
+		<el-aside width="200px">
+			<el-menu :default-openeds="['1', '2']" :router="true" style="height: 100%;">
+				<el-submenu index="1">
+					<template slot="title"><i class="el-icon-message"></i>数字化交付-组件</template>
+					<el-menu-item index="/diagramTemplate">系统图-模板设计</el-menu-item>
+					<el-menu-item index="/diagramLegend">系统图-图例管理</el-menu-item>
+					<el-menu-item index="/diagram">系统图-编辑/展现</el-menu-item>
+					<el-menu-item index="/legendIcon">图例-图标管理</el-menu-item>
+					<!-- <el-menu-item index="/jobs">任务框架</el-menu-item>
+					<el-menu-item index="/map">平面图</el-menu-item> -->
+				</el-submenu>
+				<el-submenu index="2">
+					<template slot="title"><i class="el-icon-message"></i>系统图自动绘制演示</template>
+					<el-menu-item index="/diagramView/null/null">系统图 - 空模板</el-menu-item>
+					<el-menu-item index="/diagramView/test/null">系统图 - 示例</el-menu-item>
+					<el-menu-item index="/diagramView/test/test">系统图 - 泵反向</el-menu-item>
+				</el-submenu>
+			</el-menu>
+		</el-aside>
+
+		<el-container>
+			<el-main style="padding: 0;">
+				<router-view :key="key()"/>
+			</el-main>
+		</el-container>
+	</el-container>
+</template>
+
+<style>
+	body{
+		margin: 0;
+	}
+</style>
+
+<script lang="ts">
+	import Vue from "vue";
+	export default class AppMain extends Vue{
+		key(){
+			return Date.now() + '';
+		}
+	};
+</script>

BIN
adm_comp/src/assets/logo.png


+ 37 - 0
adm_comp/src/common/Common.ts

@@ -0,0 +1,37 @@
+export class Common {
+	
+	url:string = window['serverAddress'] + "/";
+	
+	getYear(){
+		return new Date().getFullYear() + '';
+	}
+	
+	getMonth(){
+		const date = new Date();
+		return date.getFullYear() + '-' + (date.getMonth() + 1);
+	}
+	
+	getDate(){
+		const date = new Date();
+		return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
+	}
+	
+	getMonthBegin(){
+		return this.getMonth() + '-1';
+	}
+	
+	getDateStr(date){
+		if(typeof date == 'string')
+			return date;
+		if(date instanceof Date)
+			return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
+	}
+	
+	isEmpty(str:string){
+		return str == null || str.trim() == '';
+	}
+		
+}
+
+const common = new Common();
+export default common;

+ 225 - 0
adm_comp/src/common/Diagram.ts

@@ -0,0 +1,225 @@
+export class Diagram {
+		
+	private container: any;
+	private canvas: HTMLCanvasElement | undefined;
+	private ctx: CanvasRenderingContext2D | undefined;
+	private initialised = false;
+	private unit = 10;
+	private xCount = 0;
+	private yCount = 0;
+	private dynLines = [];
+	
+	constructor(container: any) {
+		this.container = container;
+	}
+	
+	show(diagramData?: any, animation?: boolean): void {
+		this.initPanel();
+		
+		this.draw(diagramData, animation ? true : false);
+	}
+	
+	private initPanel(): void{
+		const w = this.container.clientWidth;
+		const h = this.container.clientHeight;
+				
+		const canvas = document.createElement('canvas');		
+		var ratio = 1; //window.devicePixelRatio || 1;
+		canvas.width = w * ratio; // 实际渲染像素
+		canvas.height = h * ratio; // 实际渲染像素
+		canvas.style.width = w + 'px'; // 控制显示大小
+		canvas.style.height = h + 'px'; // 控制显示大小
+		canvas.style.pointerEvents = 'none';
+		// canvas.style.position = 'absolute';
+		// canvas.style.left = '0';
+		// canvas.style.top = '0';
+								
+		this.container.appendChild(canvas);
+				
+		this.canvas = canvas;
+				
+		const ctx = canvas.getContext('2d');
+		// ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+		if (ctx) {
+			ctx.translate(0.5, 0.5);
+			this.ctx = ctx;
+		} else {
+			throw new Error('Canvas 2D context not found, please check it is running in Browser environment.');
+		}
+
+		ctx.lineWidth = 1;
+		for(var x = this.unit; x < this.canvas.width; x += this.unit){
+			this.xCount++;
+
+			if(this.xCount % 5 == 0)
+				ctx.strokeStyle = "#f1f1f1";
+			else
+				ctx.strokeStyle = "#f7f7f7";
+								
+			ctx.beginPath();
+			ctx.moveTo(x, 0);
+			ctx.lineTo(x, this.canvas.height);
+			ctx.stroke();
+		}
+		for(var y = this.unit; y < this.canvas.height; y += this.unit){
+			this.yCount++;
+			
+			if(this.yCount % 5 == 0)
+				ctx.strokeStyle = "#f4f4f4";
+			else
+				ctx.strokeStyle = "#fafafa";
+				
+			ctx.beginPath();
+			ctx.moveTo(0, y);
+			ctx.lineTo(this.canvas.width, y);
+			ctx.stroke();
+		}
+		
+		this.initialised = true;
+	}
+		
+	private draw(diagramData: any, animation: boolean):void {
+		for(var item of diagramData){
+			const dash = item.lineStyle == 1 ? [3, 2] : [];
+			const color = item.color ? item.color : "#000000";
+			const bg = item.background ? item.background : "#ffffff";
+			const lineWidth = item.lineWidth > 0 ? item.lineWidth : 1;
+			
+			this.ctx.setLineDash(dash);
+			this.ctx.strokeStyle = color;
+			this.ctx.fillStyle = bg;
+			this.ctx.lineWidth = lineWidth;
+			
+			if(item.label){
+				this.drawText(item.label);
+			} else {
+				this.ctx.beginPath();
+				if(item.rect){
+					const rect = item.rect;								
+					this.ctx.rect(rect.x * this.unit, rect.y * this.unit, rect.width * this.unit, rect.height * this.unit);					
+				} else if(item.polygon) {
+					const path = item.polygon.path;
+					for(var i=0;i<path.length;i++){
+						if(i == 0)
+							this.ctx.moveTo(path[i].x * this.unit, path[i].y * this.unit);
+						else
+							this.ctx.lineTo(path[i].x * this.unit, path[i].y * this.unit);
+					}
+					this.ctx.closePath();
+				} else if(item.line) {
+					const line = item.line;
+					if(animation)
+						this.drawLine(line[0].x * this.unit, line[0].y * this.unit, line[1].x * this.unit, line[1].y * this.unit, item);
+					else{
+						this.ctx.moveTo(line[0].x * this.unit, line[0].y * this.unit);
+						this.ctx.lineTo(line[1].x * this.unit, line[1].y * this.unit);	
+					}
+				} else if(item.arc) {
+					const arc = item.arc;
+					this.ctx.arc(arc.centre.x * this.unit, arc.centre.y * this.unit, arc.r * this.unit, arc.startAngle, arc.endAngle);
+				}
+				if(!item.fill)
+					this.ctx.stroke();
+				else
+					this.ctx.fill();
+			}
+		}
+		this.dynDrawLine(0);
+	}
+	
+	private drawText(label: any):void{
+		const fontSize = 10;
+		this.ctx.font = "lighter " + fontSize + "px Microsoft YaHei";
+		this.ctx.textBaseline = "bottom";
+		
+		var rows = [];
+		var tmp = "";
+		for(var i = 0; i < label.text.length; i++) {
+			tmp += label.text[i];
+			if(this.ctx.measureText(tmp).width >= label.size.x * this.unit) {
+				rows.push(tmp);
+				tmp = "";
+			}
+		}
+		if(tmp.length > 0)
+			rows.push(tmp);
+		
+		for(var i = 0; i < rows.length; i++) {
+			this.ctx.strokeText(rows[i], label.point.x * this.unit + 2, label.point.y * this.unit + 2 + (i * fontSize));
+		}
+	}
+	
+	private drawLine(x1, y1, x2, y2, dataItem):void{
+		this.dynLines.push([x1, y1, x2, y2, dataItem]);
+	}
+	
+	private dynDrawLine(idx):void{
+		if(idx < this.dynLines.length) {
+			this.ctx.beginPath();
+			const line = this.dynLines[idx];
+			this.dynPaintLine(line[0], line[1], line[2], line[3], line[4], idx);	
+		}
+	}
+	
+	private dynPaintLine(x1, y1, x2, y2, dataItem, idx):void{	
+		var step = 5;
+		const time = 20;
+		
+		this.ctx.setLineDash(dataItem.lineStyle == 1 ? [3, 2] : []);
+		this.ctx.moveTo(x1, y1);
+		var next;
+		const h = y1 == y2;
+		if(h){
+			var i = (x2 - x1);
+			if(i == 0) {
+				this.dynDrawLine(idx + 1);
+				return;	
+			}
+				
+			if(i < 0)
+				step *= -1;
+			if(Math.abs(i) < Math.abs(step))
+				next = x1 + i;
+			else
+				next = x1 + step;
+			
+			this.ctx.lineTo(next, y1);
+			this.ctx.stroke();
+			setTimeout(()=>{this.dynPaintLine(next, y1, x2, y2, dataItem, idx);}, time);
+		} else {
+			var i = (y2 - y1);
+			if(i == 0) {
+				this.dynDrawLine(idx + 1);
+				return;	
+			}
+				
+			if(i < 0)
+				step *= -1;
+			if(Math.abs(i) < Math.abs(step))
+				next = y1 + i;
+			else
+				next = y1 + step;
+				
+			this.ctx.lineTo(x1, next);
+			this.ctx.stroke();
+			setTimeout(()=>{this.dynPaintLine(x1, next, x2, y2, dataItem, idx);}, time);
+		}
+	}
+
+}
+
+class Point {
+	
+	private xIdx: number;
+	private yIdx: number;
+	
+	constructor(xIdx: number, yIdx: number) {
+		this.xIdx = xIdx;
+		this.yIdx = yIdx;
+	}
+	
+	toCanvas(unit: number): Point {
+		return new Point(this.xIdx * unit, this.yIdx * unit); 
+	}
+	
+}

+ 365 - 0
adm_comp/src/common/Map.ts

@@ -0,0 +1,365 @@
+export class Map {
+		
+	private container: any;
+	private div: any;
+	private canvas: HTMLCanvasElement | undefined;
+	private ctx: CanvasRenderingContext2D | undefined;
+	private initialised = false;
+	private unit = 10;
+	private xCount = 0;
+	private yCount = 0;
+
+	private mapData: any;
+	private clientW: number;
+	private clientH: number;
+	private margin = 20;
+	private scale: number;
+	
+	private clickEvent;
+	private selectedSpaces = [];
+	
+	constructor(container: any) {
+		this.container = container;
+	}
+	
+	show(data?: any): void {
+		this.mapData = data;
+		
+		this.initPanel(data);
+		
+		this.draw(data);
+	}
+	
+	addClickEvent(evt: any): void {
+		this.clickEvent = evt;
+	}
+	
+	private initPanel(data: any): void{
+		this.clientW = this.container.clientWidth;
+		this.clientH = this.container.clientHeight;
+		
+		this.div = document.createElement('div');
+		this.setScale(0);
+		this.container.appendChild(this.div);
+				
+		const canvas = document.createElement('canvas');		
+		canvas.width = this.clientW; // 实际渲染像素
+		canvas.height = this.clientH; // 实际渲染像素
+		canvas.style.width = this.clientW + 'px'; // 控制显示大小
+		canvas.style.height = this.clientH + 'px'; // 控制显示大小
+		canvas.style.pointerEvents = 'none';
+		canvas.style.position = 'relative';
+								
+		this.div.appendChild(canvas);
+		this.canvas = canvas;
+		
+		this.container.addEventListener("scroll", ()=>{this.redrawOnScroll(this)});
+		
+		this.container.addEventListener('click', event=>{this.doClick(event)}, true);
+				
+		const ctx = canvas.getContext('2d');
+		if (ctx) {
+			ctx.translate(0.5, 0.5);
+			this.ctx = ctx;
+		} else {
+			throw new Error('Canvas 2D context not found, please check it is running in Browser environment.');
+		}
+		
+		this.initialised = true;
+	}
+	
+	private setScale(scale: number){
+		if(scale == 0)
+			scale = Math.min((this.clientW - this.margin*2) / this.mapData.edge.xMax.x, (this.clientH - this.margin*2) / this.mapData.edge.yMax.y);
+			
+		this.scale = scale;
+		this.div.style.width = Math.round(this.mapData.edge.xMax.x * this.scale + 2 * this.margin) + 'px';
+		this.div.style.height = Math.round(this.mapData.edge.yMax.y * this.scale + 2 * this.margin) + 'px';
+	}
+	
+	scaleTo(scale: number):void{
+		const baseY = this.clientH / 2;
+		const baseX = this.clientW / 2;
+		const yRatio = this.container.scrollTop == 0 ? 0 : (this.container.scrollTop + baseY) / this.div.clientHeight;
+		const xRatio = this.container.scrollLeft == 0 ? 0 : (this.container.scrollLeft + baseX) / this.div.clientWidth;
+		
+		this.setScale(scale);
+		
+		var top = 0
+		var left = 0;
+		if(yRatio > 0) {
+			top = this.div.clientHeight * yRatio - baseY;
+			if(top > 0){
+				const tmp = this.div.clientHeight - (top + this.clientH);
+				if(tmp < 0)
+					top += tmp;
+			}
+		}
+		if(xRatio > 0) {
+			left = this.div.clientWidth * xRatio - baseX;	
+			if(left > 0){
+				const tmp = this.div.clientWidth - (left + this.clientW);
+				if(tmp < 0)
+					left += tmp;
+			}
+		}
+			
+		this.container.scrollTop = top > 0 ? top : 0;
+		this.container.scrollLeft = left > 0 ? left : 0;
+		
+		this.redrawOnScroll(this);
+	}
+	
+	private paintBg(){
+		for(var x = this.unit; x < this.canvas.width; x += this.unit){
+			this.xCount++;
+
+			if(this.xCount % 5 == 0)
+				this.ctx.strokeStyle = "#f1f1f1";
+			else
+				this.ctx.strokeStyle = "#f7f7f7";
+								
+			this.ctx.beginPath();
+			this.ctx.moveTo(x, 0);
+			this.ctx.lineTo(x, this.canvas.height);
+			this.ctx.stroke();
+		}
+		for(var y = this.unit; y < this.canvas.height; y += this.unit){
+			this.yCount++;
+			
+			if(this.yCount % 5 == 0)
+				this.ctx.strokeStyle = "#f4f4f4";
+			else
+				this.ctx.strokeStyle = "#fafafa";
+				
+			this.ctx.beginPath();
+			this.ctx.moveTo(0, y);
+			this.ctx.lineTo(this.canvas.width, y);
+			this.ctx.stroke();
+		}		
+	}
+	
+	private redrawOnScroll(_this) {
+		_this.ctx.clearRect(-1, -1, _this.canvas.width + 1, _this.canvas.height + 1);  
+				
+		const top = _this.container.scrollTop;
+		const left = _this.container.scrollLeft;
+		_this.canvas.style.top = top + 'px';
+		_this.canvas.style.left = left + 'px';
+		
+		_this.draw(_this.mapData);
+	}
+
+	private draw(data: any):void {
+		this.paintBg();
+		
+		for(const w of data.walls){
+			this.ctx.strokeStyle = "#cccccc";
+			this.paintPath(w, true);
+			
+			// this.ctx.strokeStyle = "green";
+			// if(w.location != null){
+			// 	this.paintPath(w.location);
+			// }
+			
+			// var p = w.path[0];
+			// this.drawText(w.id, p);
+		}
+		
+		this.ctx.strokeStyle = "#000000";
+		for(const pillar of data.pillars){
+			if(pillar.roomBoundary)
+				this.paintPath(pillar, true);
+		}
+		
+		this.ctx.strokeStyle = "#b65b00";
+		for(const d of data.doors){
+			this.paintPath(d, true);
+		}
+		
+		this.ctx.strokeStyle = "#0000ff";
+		this.ctx.setLineDash([2,4]);
+		for(const vw of data.vws){
+			this.paintPath(vw, true);
+		}
+		this.ctx.setLineDash([]);
+		
+		const fillColor = "rgba(0, 0, 255, 0.1)";
+		const fillColors = ["rgba(255, 0, 0, 0.3)","rgba(215, 0, 0, 0.3)","rgba(175, 0, 0, 0.3)","rgba(135, 0, 0, 0.3)","rgba(95, 0, 0, 0.3)","rgba(55, 0, 0, 0.3)","rgba(15, 0, 0, 0.3)","rgba(0, 255, 0, 0.3)","rgba(0, 215, 0, 0.3)","rgba(0, 175, 0, 0.3)","rgba(0, 135, 0, 0.3)","rgba(0, 95, 0, 0.3)","rgba(0, 55, 0, 0.3)","rgba(0, 15, 0, 0.3)","rgba(0, 0, 255, 0.3)","rgba(0, 0, 215, 0.3)","rgba(0, 0, 175, 0.3)","rgba(0, 0, 135, 0.3)","rgba(0, 0, 95, 0.3)","rgba(0, 0, 55, 0.3)","rgba(0, 0, 15, 0.3)"];
+		this.ctx.strokeStyle = "#00BB00";
+		var idx = 0;
+		for(const sp of data.spaces){
+			if(!sp.inner || (!sp.virtual && sp.divided))
+				continue;
+			
+			this.paintSpaceName(sp);
+			
+			// if(!sp.virtual){
+			// 	if(sp.inner && !sp.divided) {
+			// 		this.ctx.fillStyle = fillColor;
+			// 		this.fillPath(sp);	
+			// 	}
+			// } else {
+			// 	this.ctx.fillStyle = fillColors[idx++];
+			// 	this.fillPath(sp);
+			// }
+			
+			
+			//idx++;
+		}
+		
+		// for(const idx of [29]){
+		// 	var sp = data.spaces[idx];
+		// 	this.fillPath(sp);
+		// 	console.log(sp.path);	
+		// }
+	}
+	
+	private paintPath(element: any, close?: boolean): void{
+		// if(element.id != '10149')
+		// 	return;
+				
+		if(!element.path || element.path.length == 0)
+			return;
+		
+		// var p = element.path[0];
+		// this.drawText(element.id, p);
+		
+		this.ctx.beginPath();
+		
+		this.movePath(element.path); //element.handledPath || element.path
+		if(close)
+			this.ctx.closePath();
+			
+		this.ctx.stroke();
+			
+		// const spLines = element.spaceLines;
+		// if(!spLines)
+		// 	return;
+		// for(const l of spLines){
+		// 	this.ctx.moveTo(this.transX(l.point1.x), this.transY(l.point1.y));
+		// 	this.ctx.lineTo(this.transX(l.point2.x), this.transY(l.point2.y));	
+		// }
+		// this.ctx.stroke();
+	}
+	
+	private fillPath(element: any): void{
+		this.ctx.beginPath();
+		
+		this.movePath(element.path, true);
+		
+		if(element.holes){
+			for(const id of element.holes){
+				const hole = this.getSpace(id);
+				this.movePath([...hole.path].reverse(), true);
+			}
+		}
+
+		this.ctx.fill();
+		
+		// if(element.holes){
+		// 	this.ctx.beginPath();
+		// 	for(const id of element.holes){
+		// 		const hole = this.getSpace(id);
+		// 		this.movePath(hole.path, true);
+		// 	}
+		// 	this.ctx.stroke();
+		// }
+		
+		// this.ctx.beginPath();
+		// this.ctx.arc(this.transX(element.interior.x), this.transY(element.interior.y), 2, 0, 2 * Math.PI);
+		// this.ctx.stroke();
+		
+	}
+	
+	private paintSpaceName(space: any): void{
+		if(space.area > 1000 * 1000 * 4)
+			this.drawText(space.id, space.interior, true);
+	}
+	
+	private movePath(path: any[], manualClose?: boolean): void{
+		var pre = null;
+		for(const p of path){
+			if(!pre)
+				this.ctx.moveTo(this.transX(p.x), this.transY(p.y));
+			else
+				this.ctx.lineTo(this.transX(p.x), this.transY(p.y));
+			pre = p;
+		}
+		if(manualClose){
+			if(pre.x != path[0].x || pre.y != path[0].y)
+				this.ctx.lineTo(this.transX(path[0].x), this.transY(path[0].y));
+		}
+	}
+	
+	private getSpace(id: string): any {
+		for(const sp of this.mapData.spaces){
+			if(sp.id == id)
+				return sp;
+		}
+	}
+
+	private doClick(event): void {
+		const left = 200;
+		const top = 33;
+		var x = event.clientX - left + this.container.scrollLeft - this.margin;
+		var y = event.clientY - top + this.container.scrollTop - this.margin;
+		x = x / this.scale;
+		y = y / this.scale;
+		//alert(x + "," + y);
+		this.clickEvent.onClick(x, y);
+	}
+	
+	public selectSpace(spId): void {
+		this.redrawOnScroll(this);
+		
+		this.selectedSpaces = [];
+		if(spId){
+			this.ctx.fillStyle = "rgba(255, 0, 0, 0.1)";
+			const sp = this.getSpace(spId);
+			this.fillPath(sp);
+			this.selectedSpaces.push(sp);
+			
+			this.ctx.fillStyle = "rgba(0, 0, 255, 0.03)";
+			if(sp.nearby){
+				for(const id of sp.nearby){
+					const nsp = this.getSpace(id);
+					this.fillPath(nsp);
+				}
+			}
+		}
+	}
+	
+	public update(data: any): void {
+		this.mapData = data;
+		this.redrawOnScroll(this);
+	}
+
+	private transX(x: number): number {
+		return Math.round(x * this.scale + this.margin - this.container.scrollLeft);
+	}
+	
+	private transY(y: number): number {
+		return Math.round(y * this.scale + this.margin - this.container.scrollTop);
+	}
+	
+	private drawText(text: string, point: any, center?:boolean):void{
+		const fontSize = 10;
+		this.ctx.font = "lighter " + fontSize + "px Microsoft YaHei";
+		this.ctx.textBaseline = "bottom";
+		
+		const bakColor = this.ctx.strokeStyle;
+		this.ctx.strokeStyle = "#000000";
+		
+		var xOffset = 0;
+		var yOffset = 0;
+		if(center) {
+			xOffset = this.ctx.measureText(text).width / 2 * -1;
+			yOffset = fontSize / 2;		
+		}
+		this.ctx.strokeText(text, this.transX(point.x) + xOffset, this.transY(point.y) + yOffset);
+		
+		this.ctx.strokeStyle = bakColor;
+	}
+
+}

+ 264 - 0
adm_comp/src/common/Map0.ts

@@ -0,0 +1,264 @@
+export class Map {
+		
+	private mapData: any;
+	private container: any;
+	private div: any;
+	private canvas: HTMLCanvasElement | undefined;
+	private ctx: CanvasRenderingContext2D | undefined;
+	private initialised = false;
+	private unit = 10;
+	private xCount = 0;
+	private yCount = 0;
+	
+	private margin = 20;
+	private xMax: number = Number.MIN_VALUE;
+	private xMin: number = Number.MAX_VALUE;
+	private yMax: number = Number.MIN_VALUE;
+	private yMin: number = Number.MAX_VALUE;
+	private scale: number;
+	
+	constructor(container: any) {
+		this.container = container;
+	}
+	
+	show(data?: any): void {
+		this.mapData = data;
+		
+		this.initPanel(data);
+		this.draw(data);
+	}
+	
+	private initPanel(data: any): void{
+		const walls = data.EntityList[0].Elements.Walls;
+		const cols = data.EntityList[0].Elements.Columns;
+		const doors = data.EntityList[0].Elements.Doors;
+		
+		for(const w of walls){
+			if(w.OutLine.length > 0){
+				this.findEdge(w.OutLine);
+			} else {
+				for(const p of w.Location.Points){
+					this.xMax = Math.max(p.X, this.xMax);
+					this.xMin = Math.min(p.X, this.xMin);
+					this.yMax = Math.max(p.Y, this.yMax);
+					this.yMin = Math.min(p.Y, this.yMin);
+				}	
+			}
+		}
+		for(const c of cols){
+			this.findEdge(c.OutLine);
+		}
+		for(const d of doors){
+			this.findEdge(d.OutLine);
+		}
+				
+		for(const w of walls){
+			if(w.OutLine.length > 0){
+				this.moveToOrigin(w.OutLine);
+			}
+			for(const p of w.Location.Points){
+				p.X -= this.xMin;
+				p.Y -= this.yMin;
+			}	
+		}
+		for(const c of cols){
+			this.moveToOrigin(c.OutLine);
+		}
+		for(const d of doors){
+			this.moveToOrigin(d.OutLine);
+		}
+		this.xMax -= this.xMin;
+		this.xMin = 0;
+		this.yMax -= this.yMin;
+		this.yMin = 0;
+		
+		this.scale = 10;
+		
+		const w = this.container.clientWidth;
+		const h = this.container.clientHeight;
+		
+		this.div = document.createElement('div');
+		this.div.style.width = (this.trans(this.xMax, this.margin) + this.margin) + 'px';
+		this.div.style.height = (this.trans(this.yMax, this.margin) + this.margin) + 'px';
+		this.container.appendChild(this.div);
+		
+		// this.scale = 5;
+		// const w = 5000;
+		// const h = 5000;
+		
+		const canvas = document.createElement('canvas');		
+		var ratio = 1; //window.devicePixelRatio || 1;
+		canvas.width = w * ratio; // 实际渲染像素
+		canvas.height = h * ratio; // 实际渲染像素
+		canvas.style.width = w + 'px'; // 控制显示大小
+		canvas.style.height = h + 'px'; // 控制显示大小
+		canvas.style.pointerEvents = 'none';
+		canvas.style.position = 'relative';
+								
+		this.div.appendChild(canvas);
+		this.canvas = canvas;
+		
+		this.container.addEventListener("scroll", ()=>{this.redrawOnScroll(this)});
+				
+		const ctx = canvas.getContext('2d');
+		// ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+		if (ctx) {
+			ctx.translate(0.5, 0.5);
+			this.ctx = ctx;
+		} else {
+			throw new Error('Canvas 2D context not found, please check it is running in Browser environment.');
+		}
+
+		this.paintBg();
+		
+		this.initialised = true;
+	}
+	
+	private paintBg(){
+		for(var x = this.unit; x < this.canvas.width; x += this.unit){
+			this.xCount++;
+
+			if(this.xCount % 5 == 0)
+				this.ctx.strokeStyle = "#f1f1f1";
+			else
+				this.ctx.strokeStyle = "#f7f7f7";
+								
+			this.ctx.beginPath();
+			this.ctx.moveTo(x, 0);
+			this.ctx.lineTo(x, this.canvas.height);
+			this.ctx.stroke();
+		}
+		for(var y = this.unit; y < this.canvas.height; y += this.unit){
+			this.yCount++;
+			
+			if(this.yCount % 5 == 0)
+				this.ctx.strokeStyle = "#f4f4f4";
+			else
+				this.ctx.strokeStyle = "#fafafa";
+				
+			this.ctx.beginPath();
+			this.ctx.moveTo(0, y);
+			this.ctx.lineTo(this.canvas.width, y);
+			this.ctx.stroke();
+		}		
+	}
+	
+	private redrawOnScroll(_this) {
+		_this.ctx.clearRect(-1, -1, _this.canvas.width + 1, _this.canvas.height + 1);  
+				
+		const top = _this.container.scrollTop;
+		const left = _this.container.scrollLeft;
+		_this.canvas.style.top = top + 'px';
+		_this.canvas.style.left = left + 'px';
+		
+		_this.paintBg();
+		_this.draw(_this.mapData);
+	}
+	
+	private findEdge(outlines: any): void{
+		for(const o of outlines){
+			for(const p of o){
+				this.xMax = Math.max(p.X, this.xMax);
+				this.xMin = Math.min(p.X, this.xMin);
+				this.yMax = Math.max(p.Y, this.yMax);
+				this.yMin = Math.min(p.Y, this.yMin);
+			}	
+		}
+	}
+	
+	private moveToOrigin(outlines: any): void{
+		for(const o of outlines){
+			for(const p of o){
+				p.X -= this.xMin;
+				p.Y -= this.yMin;
+			}	
+		}
+	}
+	
+	private paintOutlines(outlines: any): void{
+		for(const o of outlines){
+			var pre = null;
+			for(const p of o){
+				if(pre)
+					this.drawLine(pre, p);
+				pre = p;
+			}	
+		}
+	}
+		
+	private draw(data: any):void {
+		const walls = data.EntityList[0].Elements.Walls;		
+		for(const w of walls){
+			if(w.OutLine.length > 0){
+				this.ctx.strokeStyle = "#cccccc";
+				this.ctx.beginPath();
+				
+				this.paintOutlines(w.OutLine);
+				
+				this.ctx.stroke();	
+				
+				// var p = w.OutLine[0][0];
+				// this.drawText(w.Id, p);
+			} 
+			/* else */ 
+			{
+				this.ctx.strokeStyle = "#78e28d";
+				this.ctx.beginPath();
+				
+				var pre = null;
+				for(const p of w.Location.Points){
+					if(pre)
+						this.drawLine(pre, p);
+					pre = p;						
+				}
+				
+				this.ctx.stroke();	
+			}
+		}
+		
+		this.ctx.strokeStyle = "#000000";
+		const cols = data.EntityList[0].Elements.Columns;
+		for(const c of cols){
+			if(!c.RoomBoundary)
+				continue;
+			this.ctx.beginPath();
+			this.paintOutlines(c.OutLine);
+			this.ctx.stroke();
+		}
+		
+		this.ctx.strokeStyle = "#b65b00";
+		const doors = data.EntityList[0].Elements.Doors;
+		for(const d of doors){
+			this.ctx.beginPath();
+			this.paintOutlines(d.OutLine);
+			this.ctx.stroke();
+		}
+	}
+	
+	private drawLine(p1: any, p2: any) {			
+		this.ctx.moveTo(this.transX(p1.X), this.transY(p1.Y));
+		this.ctx.lineTo(this.transX(p2.X), this.transY(p2.Y));
+	}
+	
+	private trans(x: number, offset: number): number {
+		return Math.round(x/this.scale + offset);
+	}
+	
+	private transX(x: number): number {
+		return Math.round(x/this.scale + this.margin - this.container.scrollLeft);
+	}
+	
+	private transY(y: number): number {
+		return Math.round(y/this.scale + this.margin - this.container.scrollTop);
+	}
+	
+	private drawText(text: string, point: any):void{
+		const fontSize = 10;
+		this.ctx.font = "lighter " + fontSize + "px Microsoft YaHei";
+		this.ctx.textBaseline = "bottom";
+		this.ctx.strokeStyle = "#000000";
+		
+		this.ctx.strokeText(text, this.transX(point.X), this.transY(point.Y));
+	}
+
+}

+ 151 - 0
adm_comp/src/components/FilterPanel.vue

@@ -0,0 +1,151 @@
+<template>
+	<div>
+		<el-form :inline="true">
+			<el-row v-for="(item, index) in filterItems" :key="item.key">
+				<el-select v-model="item.filterObj" style="width: 100px;">
+					<el-option label="名称" value="name"></el-option>
+					<el-option label="信息点" value="infos."></el-option>
+				</el-select>
+				&nbsp;
+				<el-input
+				  placeholder="信息点代码"
+				  v-model="item.infoCode"
+				  clearable
+				  :disabled="item.filterObj!='infos.'"
+				  style="width: 150px">
+				</el-input>
+				&nbsp;
+				<el-select v-model="item.filterOper" style="width: 100px;">
+					<el-option label="包含" value="contains"></el-option>
+					<el-option label="等于" value="equals"></el-option>
+				</el-select>
+				&nbsp;
+				<el-input
+				  placeholder="匹配值,多项间使用英文分号隔开"
+				  v-model="item.filterVal"
+				  clearable
+				  style="width: 30%;">
+				</el-input>
+				&nbsp;
+				<el-checkbox v-model="item.not">否定</el-checkbox>
+				&nbsp;
+				<el-button size="mini" @click.prevent="removeFilterItem(item)">删除</el-button>
+			</el-row>
+		</el-form>
+		<br>
+		<el-button size="mini" @click="addFilterItem()">添加条件(and)</el-button>
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+
+	@Component({})
+	export default class FilterPanel extends Vue {
+		
+		@Prop()
+		dataFilter: any;
+		
+		filterItems: any[] = [];
+		
+		mounted(){
+			this.buildFilterItems();
+		}
+		
+		@Watch("dataFilter")
+		buildFilterItems(){
+			this.filterItems = this.buildItems(this.dataFilter);
+		}
+		
+		buildItems(filter){
+			const items:any[] = [];
+			if(filter && filter.type) {
+				if(filter.type == 1) { // match
+					items.push(this.buildItem4Edit(filter));
+				} else if (filter.type == 2) { // and
+					for(const i of filter.items){
+						items.push(this.buildItem4Edit(i));	
+					}
+				}
+			} else {
+				items.push({});
+			}
+			return items;
+		}
+		
+		buildItem4Edit(filterItem){
+			const item:any = {};
+			const obj:string = filterItem.object;
+			const infosPre = 'infos.';
+			if(obj.indexOf(infosPre) == 0){
+				item.filterObj = infosPre;
+				item.infoCode = obj.substr(infosPre.length);
+			} else {
+				item.filterObj = obj;
+			}
+			item.filterOper = filterItem.operation;
+			item.filterVal = filterItem.value;
+			item.not = filterItem.not;
+			return item;
+		}
+		
+		//添加过滤条件条目
+		addFilterItem(){
+			this.filterItems.push({});
+		}
+		
+		//删除过滤条件条目
+		removeFilterItem(item){
+			const idx = this.filterItems.indexOf(item);
+			this.filterItems.splice(idx, 1);
+		}
+		
+		//创建设备过滤条件
+		buildFilter(){
+			var filter: any;
+			if(this.filterItems.length == 0)
+				filter = null;
+			else {
+				for(const item of this.filterItems) {
+					var flag = !this['$common'].isEmpty(item.filterObj) && !this['$common'].isEmpty(item.filterOper) && !this['$common'].isEmpty(item.filterVal);
+					if(flag) {
+						if(item.filterObj == 'infos.')
+							flag = !this['$common'].isEmpty(item.infoCode);
+					}
+					if(!flag)
+						return {errMsg: '缺少必要的信息'};
+				}
+				
+				if(this.filterItems.length == 1)
+					filter = this.buildItem4Commit(this.filterItems[0]);
+				else {
+					filter = {type: 2, items: []}; //and
+					for(const item of this.filterItems) {
+						filter.items.push(this.buildItem4Commit(item));
+					}
+				}
+			}
+			return filter;
+		}
+		
+		buildItem4Commit(item){
+			const filterItem:any = {type: 1};
+			filterItem.object = item.filterObj;
+			if(filterItem.object == 'infos.')
+				filterItem.object += item.infoCode;
+			filterItem.operation = item.filterOper;
+			filterItem.value = item.filterVal;	
+			filterItem.not = item.not;	
+			return filterItem;
+		}
+		
+	}
+</script>
+
+<style>
+</style>

+ 60 - 0
adm_comp/src/lib/ContainerRefPoint.ts

@@ -0,0 +1,60 @@
+import {Point, ViewTool} from './UIUtils';
+
+/**
+ * 容器边界上的参考点
+ * @author zhaoyk
+ */
+export class ContainerRefPoint {
+	
+	static TL = "TL";
+	static TR = "TR";
+	static BR = "BR";
+	static BL = "BL";
+	
+	containerId: string;
+	name: string;
+	location: Point;
+	
+	constructor(containerId:string, name:string){
+		this.containerId = containerId;
+		this.name = name;
+	}
+	
+	static getRefPoints(containerId:string, conLocation: Point, conSize: Point): Array<ContainerRefPoint> {
+		const points = new Array<ContainerRefPoint>();
+
+		ContainerRefPoint.addPoint(containerId, ContainerRefPoint.TL, new Point(conLocation.x, conLocation.y), points);
+		ContainerRefPoint.addPoint(containerId, ContainerRefPoint.TR, new Point(conLocation.x + conSize.x, conLocation.y), points);
+		ContainerRefPoint.addPoint(containerId, ContainerRefPoint.BR, new Point(conLocation.x + conSize.x, conLocation.y + conSize.y), points);
+		ContainerRefPoint.addPoint(containerId, ContainerRefPoint.BL, new Point(conLocation.x, conLocation.y + conSize.y), points);
+
+		var len = conSize.x / 4;
+		for (var i = 1; i <= 3; i++) {
+			ContainerRefPoint.addPoint(containerId, "T" + i, new Point(conLocation.x + len * i, conLocation.y), points);
+			ContainerRefPoint.addPoint(containerId, "B" + i, new Point(conLocation.x + len * i, conLocation.y + conSize.y), points);
+		}
+		len = conSize.y / 4;
+		for (var i = 1; i <= 3; i++) {
+			ContainerRefPoint.addPoint(containerId, "L" + i, new Point(conLocation.x, conLocation.y + len * i), points);
+			ContainerRefPoint.addPoint(containerId, "R" + i, new Point(conLocation.x + conSize.x, conLocation.y + len * i), points);
+		}
+
+		return points;
+	}
+
+	static addPoint(containerId: string, name: string, location: Point, points: Array<ContainerRefPoint>): void{
+		const refPoint = new ContainerRefPoint(containerId, name);
+		refPoint.location  = location;
+		points.push(refPoint);
+	}
+
+	static getRefPoint(containerId:string, conLocation: Point, conSize: Point, refName: string) : ContainerRefPoint{
+		const list = ContainerRefPoint.getRefPoints(containerId, conLocation, conSize);
+		for(const p of list) {
+			if(p.name.toLowerCase() == refName.toLowerCase())
+				return p;
+		}
+		return null;
+	}
+	
+}

+ 444 - 0
adm_comp/src/lib/DiagramEditor.ts

@@ -0,0 +1,444 @@
+import {Diagram, EquipmentNode, Template, Comp, Container, MainPipe, Legend, Anchor, Point4Anchor} from './DiagramModel';
+import {Point, BackgroundPaniter, ViewTool} from './UIUtils';
+import {Editor, Selection} from './Editor';
+
+/**
+ * 系统图编辑器,演示版本
+ * @author zhaoyk
+ */
+export class DiagramEditor extends Editor {
+
+	private util: EditUtil;
+	
+	diagram: Diagram;
+	templateData: Template;
+	libertyCons: Array<Container>; //自由定位的容器
+		
+	tmpMainPipe:Array<Array<Point>>;
+	currentTmpMainPipe:Array<Point>;
+	
+	showData(data?: any): void {
+		this.diagram = (<Diagram>data);
+
+		this.selection = new Selection();
+		
+		this.setState(this.getState());
+
+		this.util = new EditUtil(this);
+		
+		this.redraw();
+	}
+
+	protected initCanvasSize(): Point {
+		return new Point(3500, 3500);
+	}
+	
+	addListeners():void {
+		this.canvas.addEventListener('click', event=>{this.onClick(event)});
+		this.container.addEventListener("mousemove", event=>{this.onMouseMove(event)});
+	}
+	
+	setState(state: number): void{
+		this.state = state;
+		
+		if(this.state == 9) 
+			this.tmpMainPipe = new Array<Array<Point>>();
+		else
+			this.tmpMainPipe = null;
+		
+		this.currentTmpMainPipe = null;	
+	}
+		
+	redraw(): void {
+		this.beforeRedraw();
+		
+		if(!this.diagram.template)
+			return;
+		
+		this.templateData = this.diagram.template;
+		
+		this.libertyCons = new Array<Container>();
+		const root = this.templateData.frame;
+		this.util.prepareParent(root);
+
+		//绘制排列定位容器		
+		this.drawContainer(root);
+		
+		//绘制自由定位容器
+		const p = new Point(root.location.x, root.location.y);
+		p.y += root.size.y;
+		this.libertyCons.forEach(con => {
+			p.y += 20;
+			con.location = new Point(p.x, p.y);
+			this.drawContainer(con, true);
+			p.y += con.size.y;
+		});
+		
+		//绘制干管
+		if(this.templateData.mainPipes) {
+			this.templateData.mainPipes.forEach(item => {
+				this.ctx.strokeStyle = "#000000";
+				this.ctx.setLineDash([]);
+				this.ctx.lineWidth = 2;
+				if(this.getSelComp() == item)
+					this.ctx.strokeStyle = "#5783FA";
+					
+				this.ctx.beginPath();
+				for(const arr of item.locationPath){
+					var iter = 0;
+					for(var p of arr) {
+						p = this.util.toCanvas(p);
+						if(iter == 0)
+							this.ctx.moveTo(p.x, p.y);
+						else
+							this.ctx.lineTo(p.x, p.y);	
+						iter++;
+					}	
+				}
+				this.ctx.stroke();
+				this.ctx.closePath();
+			});
+		}
+	}
+	
+	private drawContainer(con:Container, drawLiberty?:boolean) {
+		if(!drawLiberty && con.position.absolute)
+			return;
+		if(con.size.x == 0 && con.size.y == 0)
+			return;
+		
+		const liberty = con.position.absolute;
+		const location = !liberty ? this.util.locationOnCanvas(con) : this.util.toCanvas(con.location);
+				
+		const equip = con.equipmentTypes && con.equipmentTypes.length > 0;
+		var color = !liberty ? "#9b9ea3": "#42B983";
+		var dash = [3, 2];
+	
+		if(equip) {
+			this.ctx.lineWidth = 1;
+			this.ctx.setLineDash(dash);
+			this.ctx.strokeStyle = color;
+			
+			this.ctx.beginPath();
+			this.ctx.rect(location.x, location.y, con.size.x, con.size.y);	
+			this.ctx.closePath();
+					
+			if(this.getSelComp() == con) {
+				this.ctx.fillStyle = "rgba(0, 127, 255, 0.1)";
+				this.ctx.fill();
+			}
+			this.ctx.stroke();	
+		}
+
+		if(con.children)
+			con.children.forEach(item => {
+				if(item.compType == 'container')
+					this.drawContainer(<Container>item);
+				else if(item.compType == 'equipmentNode')
+					this.drawEquipNode(<EquipmentNode>item);
+				else if(item.compType == 'packNode')
+					this.drawEquipNode(<EquipmentNode>item);
+			});
+	}
+	
+	private drawEquipNode(node: EquipmentNode): void {
+		const location = this.util.locationOnCanvas(node);
+		
+		this.ctx.lineWidth = 1;
+		this.ctx.setLineDash([]);
+		this.ctx.strokeStyle = "#000000";
+		
+		this.ctx.beginPath();
+		this.ctx.rect(location.x, location.y, node.size.x, node.size.y);	
+
+		this.ctx.stroke();
+		if(node.anchorLocations) {
+			for(const code in node.anchorLocations) {
+				this.ctx.beginPath();
+				var p: Point = node.anchorLocations[code];
+				p = new Point(p.x + location.x, p.y + location.y);
+				ViewTool.paintPoint(this.ctx, p, '#000000', true);	
+			}
+		}
+		if(node.label) {
+			const lbl = node.label;
+			ViewTool.drawText(this.ctx, lbl.content, new Point(location.x + lbl.location.x, location.y + lbl.location.y), node.size);
+		}
+	}
+			
+	// private redrawOnScroll(_this) {	
+	// 	const top = _this.container.scrollTop;
+	// 	const left = _this.container.scrollLeft;
+	// 	_this.canvas.style.top = top + 'px';
+	// 	_this.canvas.style.left = left + 'px';
+		
+	// 	_this.draw(_this.mapData);
+	// }
+	
+	private onClick(event): void {
+		if(this.state != 9) { //selection
+			var sel:any = this.util.findMainPipe(event.layerX, event.layerY);
+			if(sel == null)
+				sel = this.util.findComp(event.layerX, event.layerY);
+				
+			if(sel != this.getSelComp)
+				this.setSelComp(sel);
+		} else { //paint main pipe
+			if(this.currentTmpMainPipe == null) {
+				this.currentTmpMainPipe = new Array<Point>();
+				this.tmpMainPipe.push(this.currentTmpMainPipe);
+			}
+			
+			const point = new Point(event.layerX, event.layerY);
+			this.adjustPointForMp(point);
+			this.currentTmpMainPipe.push(point);
+			
+			this.redraw();
+		}
+	}
+	
+	setSelComp(comp: any): void{
+		this.selection.setSel(comp);
+		this.redraw();
+		
+		this.editorPart.onSelectionChange();
+	}
+	
+	getSelComp(): any {
+		if(!this.selection)
+			return null;
+		return this.selection.getSel();
+	}
+	
+	buildSelInfo(){
+		var info = null;
+
+		const sel = this.getSelComp();
+		if(sel != null) {
+			info = 'id: ' + sel.id;	
+			if(sel.name)
+				info += '  |  名称: ' + sel.name;
+			if(sel.layout) {
+				if(sel.layout.layout == 1)
+					info += '  |  布局方式: 行式';
+				else if(sel.layout.layout == 2)
+					info += '  |  布局方式: 列式';
+			}
+			if(sel.equipmentTypes && sel.equipmentTypes.length > 0){
+				info += '  |  设备类型: ' + sel.equipmentTypes;
+			}
+			if(sel.locationPath){
+				var str = '';
+				const arr:Array<Array<Point>> = new Array<Array<Point>>();
+				for(const line of sel.locationPath) {
+					const ps:Array<Point> = new Array<Point>();
+					arr.push(ps);
+					
+					line.forEach(item => {
+						ps.push(this.util.toCanvas(item));
+					});
+					
+					if(str.length > 0)
+						str += ", ";
+					str += ViewTool.pointsToStr(ps);		
+				}
+				info += '  |  ' + str;
+			}
+		}
+		
+		if(info == null)
+			info = '~';
+		return info;
+	}
+	
+	getState(): number {
+		const sel = this.getSelComp();
+		if(!sel)
+			return 0;
+		if(sel.locationPath)
+			return 2;
+		return 1;
+	}
+	
+	getCompById(id: string): any {
+		if(!id)
+			return null;
+			
+		if(this.templateData.mainPipes) {
+			for(const mp of this.templateData.mainPipes) {
+				if(mp.id == id)
+					return mp;
+			}
+		}
+		
+		return this.getConById(id);
+	}
+	
+	getConById(id:string): Container {
+		return this.findCon(id, this.templateData.frame);
+	}
+
+	private findCon(id:string, con: Container): Container {
+		if(con.id == id)
+			return con;
+		for(const sub of con.children) {
+			if(sub.compType == 'container') {
+				const rtn = this.findCon(id, <Container>sub);
+				if(rtn != null)
+					return rtn;	
+			}
+		}
+		return null;
+	}
+
+	//干管绘制时,自动调整横平竖直
+	private adjustPointForMp(point: Point) {
+		if(this.currentTmpMainPipe && this.currentTmpMainPipe.length > 0) {
+			const pre = this.currentTmpMainPipe[this.currentTmpMainPipe.length - 1];
+			if(Math.abs(pre.x - point.x) < Math.abs(pre.y - point.y))
+				point.x = pre.x;
+			else
+				point.y = pre.y;
+		}
+	}
+	
+	private onMouseMove(event): void {
+		if(this.state == 9) {
+			const point = new Point(event.layerX, event.layerY);
+			this.adjustPointForMp(point);
+			
+			//显示当前坐标位置
+			this.editorPart.dynInfo = ViewTool.pointToStr(point);
+			
+			this.redraw();
+		}
+	}
+
+}
+
+class EditUtil {
+	
+	private editor:DiagramEditor;
+	
+	private template:Template;
+
+	constructor(editor:DiagramEditor) {
+		this.editor = editor;
+		this.template = editor.templateData;
+	}
+	
+	prepareParent(con:Container) {
+		if(con.children) {
+			con.children.forEach(item => {
+				item.parent = con;
+				
+				if(item.compType == 'container') {
+					this.prepareParent(<Container>item);
+					
+					if(item.position.absolute)
+						this.editor.libertyCons.push(<Container>item);	
+				}
+			});	
+		}
+	}
+	
+	clearParent(con:Container){
+		con.parent = null;
+		con.children.forEach(item => {
+			item.parent = null;
+			
+			if(item.compType == 'container') 
+				this.clearParent(<Container>item);
+		});
+	}
+	
+	locationOnDiagram(con:Comp): Point {
+		const p:Point = new Point(0, 0);
+		var c = con;
+		while (c != null){
+			p.x += c.location.x;
+			p.y += c.location.y;
+	
+			c = c.parent;
+		}
+		return p;
+	}
+	
+	locationOnCanvas(con:Comp): Point {
+		return this.toCanvas(this.locationOnDiagram(con));
+	}
+	
+	toCanvas(point:Point): Point {
+		return new Point(point.x + this.editor.margin, point.y + this.editor.margin); 
+	}
+	
+	findComp(x:number, y:number): Comp {
+		var rtn = this.tryFindComp(x, y, this.template.frame);
+		if(!rtn) {
+			for(const con of this.editor.libertyCons){
+				rtn = this.tryFindComp(x, y, con);
+				if(rtn)
+					break;
+			}
+		}
+		return rtn;
+	}
+	
+	private tryFindComp(x:number, y:number, target:Comp): Comp {
+		if(this.containsPoint(target, x, y)){
+			if(target.compType == 'container') {
+				for(const item of (<Container>target).children) {
+					const c = this.tryFindComp(x, y, item);
+					if(c != null)
+						return c;
+				}	
+			}
+			return target;
+		}
+		return null;
+	}
+	
+	containsPoint(con:Comp, x:number, y:number):boolean{
+		if(con.location){
+			const point = this.locationOnCanvas(con);
+			return x >= point.x && y >= point.y && x <= point.x + con.size.x && y <= point.y + con.size.y;	
+		}
+		return false;
+	}
+	
+	findMainPipe(x:number, y:number): any {
+		if(this.template.mainPipes) {
+			for(const mp of this.template.mainPipes){
+				for(const line of mp.locationPath) {
+					var pre: Point = null;
+					for(var point of line) {
+						point = this.toCanvas(point);
+						if(pre != null) {
+							var x1:number;
+							var y1:number;
+							var x2:number;
+							var y2:number;
+							const scope = 3;
+							if(pre.y == point.y) { //水平
+								y1 = pre.y - scope;
+								y2 = pre.y + scope;
+								x1 = Math.min(pre.x, point.x);
+								x2 = Math.max(pre.x, point.x);
+							} else { //垂直
+								x1 = pre.x - scope;
+								x2 = pre.x + scope;
+								y1 = Math.min(pre.y, point.y);
+								y2 = Math.max(pre.y, point.y);
+							}
+							if(x >= x1 && y >= y1 && x <= x2 && y <= y2)
+								return mp;
+						}
+						pre = point;	
+					}	
+				}
+			}
+		}
+		return null;
+	}
+		
+}

+ 281 - 0
adm_comp/src/lib/DiagramModel.ts

@@ -0,0 +1,281 @@
+import {Point} from './UIUtils';
+
+/**
+ * 系统图相关模型
+ * @author zhaoyk
+ */
+export class Template {
+	
+	diagramType:string;
+	
+	id:string;
+	
+	name:string;
+	
+	code:string;
+	
+	frame:Container;
+	
+	mainPipes: Array<MainPipe>;
+	
+	scatteredContainers: Array<Container>;
+	
+}
+
+export class Comp {
+
+	id:string;
+	
+	name:string;
+	
+	location:Point;
+	
+	size:Point;
+	
+	position: any;
+	
+	compType: string;
+	
+	parent: Container;
+				
+}
+
+export class Container extends Comp {
+	
+	remark:string;
+	
+	children: Array<Comp>;
+
+	layout: any;
+	
+	equipmentTypes: Array<string>;
+
+	dataFilter: DataFilter;
+	
+	equipPack: EquipPack;
+	
+	dynGroup: DynGroup;
+	
+	static getEquipBoxes(con: Container, arr?: Array<Container>){
+		if(!arr)
+			arr = new Array<Container>();
+		if(con.equipmentTypes != null && con.equipmentTypes.length > 0)
+			arr.push(con);
+		else if(con.children && con.children.length > 0) {
+			for(const item of con.children) {
+				Container.getEquipBoxes(<Container>item, arr);
+			}
+		}
+		return arr;
+	}
+	
+}
+
+export class DataFilter {
+	
+	type: number;
+	
+	object: string;
+	
+	operation: string;
+	
+	value: string;
+	
+	items: Array<DataFilter>;
+	
+	not: boolean;
+	
+}
+
+export class EquipPack {
+	
+	packByType: boolean;
+	
+	legendId: string;
+	
+	packName: string;
+
+}
+
+export class DynGroup {
+
+	dynSource: string;
+	
+	labelPosition: string;
+
+}
+
+export class MainPipe {
+	
+	id:string;
+	
+	name:string;
+	
+	remark:string;
+	
+	type:string;
+	
+	bindEquipment:boolean;
+	
+	path: any[];
+	
+	locationPath: Array<Array<Point>>;
+			
+}
+
+export class Legend {
+
+	id:string;
+	
+	code:string;
+
+	name:string;
+
+	remark:string;
+
+	width:number;
+
+	height:number;
+
+	equipmentTypes: Array<string>;
+
+	diagramTypes: Array<string>;
+
+	anchors: Array<Anchor>;
+	
+	dataFilter: DataFilter;
+	
+	outline: Array<Array<Point>>;
+
+	infos: Array<string>;
+	
+	static getPointsForAnchor(legend: Legend, location: Point): Array<Point4Anchor> {
+		const size = new Point(legend.width, legend.height);
+		
+		const points = new Array<Point4Anchor>();
+		points.push(new Point4Anchor(new Point(location.x, location.y), Point4Anchor.TL));
+		points.push(new Point4Anchor(new Point(location.x + size.x, location.y), Point4Anchor.TR));
+		points.push(new Point4Anchor(new Point(location.x + size.x, location.y + size.y), Point4Anchor.BR));
+		points.push(new Point4Anchor(new Point(location.x, location.y + size.y), Point4Anchor.BL));
+		
+		const part = 6;
+		var len = size.x / part;
+		for (var i = 1; i < part; i++) {
+			points.push(new Point4Anchor(new Point(location.x + len * i, location.y), "T" + i));
+			points.push(new Point4Anchor(new Point(location.x + len * i, location.y + size.y), "B" + i));
+		}
+		len = size.y / part;
+		for (var i = 1; i < part; i++) {
+			points.push(new Point4Anchor(new Point(location.x, location.y + len * i), "L" + i));
+			points.push(new Point4Anchor(new Point(location.x + size.x, location.y + len * i), "R" + i));
+		}
+		
+		if(legend.anchors) {
+			for(const anchor of legend.anchors) {
+				for(const p of points) {
+					if(p.code == anchor.code) {
+						p.anchor = anchor;
+						p.selected = true;
+						break;
+					}
+				}
+			}	
+		}
+		
+		return points;
+	}
+	
+}
+
+export class Anchor {
+	
+	code: string;
+	
+	name: string;
+	
+	type: string;
+	
+	acceptRelations: Array<string>
+
+	attachToOutline: boolean = true;
+	
+	multiple: boolean;
+	
+	shape: string;
+
+
+	xOffset: number;
+	
+	yOffset: number;
+	
+}
+
+export class Point4Anchor {
+	
+	static TL = "TL";
+	static TR = "TR";
+	static BR = "BR";
+	static BL = "BL";
+	
+	point: Point;
+	code: string;
+	selected: boolean;
+	anchor: Anchor;
+	
+	constructor(point: Point, code: string) {
+		this.point = point;
+		this.code = code;
+	}
+	
+}
+
+
+export class Diagram {
+	
+	id:string;
+	
+	name: string;
+	
+	type: string;
+	
+	system: string;
+	
+	templateId: string;
+	
+	nodes: Array<DiagramNode>;
+	
+	template: Template;
+	
+}
+
+export class DiagramNode extends Comp {
+	
+	containerId: string;
+	
+	layoutIndex: number;
+	
+	dataObject: Object;
+	
+}
+
+export class EquipmentNode extends DiagramNode {
+	
+	objId: string;
+	
+	objClassCode: string;
+
+	legendId: string;
+
+	legend: Legend;
+	
+	anchorLocations: Object;
+	
+	label: Label;
+	
+}
+
+export class Label extends Comp {
+	
+	content: string;
+	
+}

+ 106 - 0
adm_comp/src/lib/Editor.ts

@@ -0,0 +1,106 @@
+import {Point, BackgroundPaniter, ViewTool} from './UIUtils';
+
+/**
+ * 编辑器基类
+ * @author zhaoyk
+ */
+export abstract class Editor {
+	
+	protected container: any;
+	
+	protected canvas: HTMLCanvasElement | undefined;
+	
+	protected ctx: CanvasRenderingContext2D | undefined;
+	
+	protected canvasSize: Point;
+	
+	protected initialised = false;
+	
+	protected editorPart: any; //vue编辑界面组件
+	
+	protected bgPainter: BackgroundPaniter;
+
+	protected state = 0;
+
+	protected selection: Selection;
+
+	margin = 20;
+	
+	constructor(container: any) {
+		this.container = container;
+	}
+	
+	setEditorPart(editorPart: any): void {
+		this.editorPart = editorPart;
+	}
+	
+	show(data: any) : void {
+		if(!this.initialised)
+			this.initPanel();
+			
+		this.showData(data);
+	}
+	
+	private initPanel(): void{
+		this.canvasSize = this.initCanvasSize();
+
+		const canvas = document.createElement('canvas');		
+		canvas.width = this.canvasSize.x; // 实际渲染像素
+		canvas.height = this.canvasSize.y; // 实际渲染像素
+		canvas.style.width = this.canvasSize.x + 'px'; // 控制显示大小
+		canvas.style.height = (this.canvasSize.y - 4) + 'px'; // 控制显示大小
+		canvas.style.position = 'relative';
+								
+		this.container.appendChild(canvas);
+		this.canvas = canvas;
+		
+		const ctx = canvas.getContext('2d');
+		if (ctx) {
+			ctx.translate(0.5, 0.5);
+			this.ctx = ctx;
+		} else {
+			throw new Error('Canvas 2D context not found, please check it is running in Browser environment.');
+		}
+				
+		this.bgPainter = new BackgroundPaniter(canvas, ctx);		
+			
+		this.addListeners();
+
+		this.initialised = true;
+	}
+	
+	protected abstract initCanvasSize(): Point;
+	
+	abstract addListeners(): void;
+
+	abstract showData(data: any): void;
+
+	abstract redraw(): void;
+	
+	protected beforeRedraw(): void {
+		this.ctx.clearRect(-1, -1, this.canvas.width + 1, this.canvas.height + 1);
+		this.bgPainter.paint();
+	}
+	
+}
+
+export class Selection {
+	
+	obj: any;
+	
+	objType: string;
+	
+	setSel(obj: any, objType?: string): void{
+		this.obj = obj;
+		this.objType = objType;
+	}
+	
+	getSel() : any {
+		return this.obj;
+	}
+	
+	getSelType(): string{
+		return this.objType;
+	}
+	
+}

+ 324 - 0
adm_comp/src/lib/LegendEditor.ts

@@ -0,0 +1,324 @@
+import {Legend, Anchor, Point4Anchor} from './DiagramModel';
+import {Point, BackgroundPaniter, ViewTool} from './UIUtils';
+import {Editor, Selection} from './Editor';
+
+/**
+ * 图例编辑器
+ * @author zhaoyk
+ */
+export class LegendEditor extends Editor{
+
+	legendSize = 300;
+
+	legendData: Array<Legend>;
+	legendViews: Object;
+
+	private pointsForAnchor: Array<Point4Anchor>;
+	private focusP4A: Point4Anchor;
+	private focusAnchor: Anchor;
+
+	protected initCanvasSize(): Point {
+		return new Point(this.container.clientWidth, this.container.clientHeight);
+	}
+	
+	addListeners(): void {
+		this.canvas.addEventListener('click', event=>{this.onClick(event)});
+		this.container.addEventListener("mousemove", event=>{this.onMouseMove(event)});
+	}
+	
+	showData(legendData?: Array<Legend>): void {
+		this.legendData = legendData;
+		this.legendViews = new Object();
+		
+		this.selection = new Selection();
+		
+		this.setState(this.getState());
+
+		this.redraw();
+	}
+	
+	setState(state: number): void{
+		this.state = state;
+		this.pointsForAnchor = null;
+		
+		if(this.state == 8) { //设置锚点
+			const legend: Legend = this.getSelComp();
+			
+			this.pointsForAnchor = Legend.getPointsForAnchor(legend, this.legendViews[legend.id].legendLocation);
+			
+			this.redraw();
+		} else if (this.state == 9) { //绘制图例 
+			
+		}
+		//TODO
+	}
+
+	redraw():void {
+		this.ctx.clearRect(-1, -1, this.canvas.width + 1, this.canvas.height + 1);  
+		
+		const col = Math.floor(this.canvasSize.x / this.legendSize);
+		const row = Math.floor(this.legendData.length / col) + 1;
+		const h1 = row * this.legendSize + 10;
+		if(h1 > this.canvasSize.y) {
+			this.canvas.height = h1; // 实际渲染像素
+			this.canvas.style.height = h1 + 'px'; // 控制显示大小
+			
+			this.ctx.translate(0.5, 0.5);
+		}
+		
+		this.bgPainter.paint();
+		
+		//layout 
+		for(var r = 0; r < row; r++){
+			for(var c = 0; c < col; c++) {
+				const idx = r * col + c;
+				if(idx >= this.legendData.length)
+					break;
+				
+				const legend = this.legendData[idx];
+				const point = new Point(c * this.legendSize, r * this.legendSize);
+				const lv = new LegendView();
+				lv.viewLocation = point;
+				lv.legendLocation = new Point(point.x + this.margin, point.y + this.margin);;
+				this.legendViews[legend.id] = lv;
+				
+				if(legend.anchors && legend.anchors.length > 0) {
+					const p4As = Legend.getPointsForAnchor(legend, lv.legendLocation);
+					for(const p of p4As) {
+						if(p.anchor)
+							lv.anchorPoints[p.anchor.code] = p.point;
+					}
+				}
+			}
+		}
+		
+		for(const legend of this.legendData){
+			this.drawLegend(legend, this.legendViews[legend.id]);
+		}
+		
+	}
+	
+	private drawLegend(legend: Legend, legendView: LegendView){		
+		this.ctx.setLineDash([]);
+		const selected = this.getSelComp() == legend;
+		if(selected){
+			this.ctx.lineWidth = 2;	
+			this.ctx.strokeStyle = "#5783FA";
+		} else {
+			this.ctx.lineWidth = 1;	
+			this.ctx.strokeStyle = "#9b9ea3";
+		}
+		
+		this.ctx.beginPath();
+		const l1 = legendView.viewLocation;
+		this.ctx.rect(l1.x ,l1.y, this.legendSize, this.legendSize);
+		this.ctx.stroke();
+		
+		this.ctx.beginPath();
+		this.ctx.lineWidth = 1;
+		this.ctx.strokeStyle = "#000000";
+		const l2 = legendView.legendLocation;
+		this.ctx.rect(l2.x, l2.y, legend.width, legend.height);
+		this.ctx.stroke();
+		
+		var info = legend.code;
+		if(legend.name)
+			info += '  ' + legend.name;
+		info += '(' + legend.width + '*'  + legend.height + ')';
+		if(legend.equipmentTypes && legend.equipmentTypes.length > 0)
+			info += '\n[' + legend.equipmentTypes + ']';
+		if(legend.dataFilter)
+			info += ViewTool.filterToStr(legend.dataFilter);
+		if(legend.remark && legend.remark.length > 0)
+			info += '\n' + legend.remark;
+
+		ViewTool.drawText(this.ctx, info, new Point(l2.x, l2.y + legend.height + 5), new Point(this.legendSize - 20, 0));
+		
+		var color;
+		if(this.pointsForAnchor && selected){ //设置锚点
+			for(const p of this.pointsForAnchor) {
+				this.ctx.beginPath();
+				
+				color = "#9b9ea3";
+				var fill = false;
+				if(p.selected) {
+					if(p.anchor && p.anchor.multiple)
+						color = "#00aa7f";
+					else
+						color = "#000000";
+					fill = true;
+				} else if(this.focusP4A && p.code == this.focusP4A.code) {
+					fill = true;
+				} 
+				ViewTool.paintPoint(this.ctx, p.point, color, fill, 3);
+			}
+		} else { //绘制图例的锚点
+			if(legend.anchors && legend.anchors.length > 0) {
+				for(const a of legend.anchors) {
+					const r = this.focusAnchor == a ? 4 : 3;
+					color = a.multiple ? "#00aa7f" : "#000000";
+					this.ctx.beginPath();
+					ViewTool.paintPoint(this.ctx, legendView.anchorPoints[a.code], color, true, r);
+				}
+			}
+		}
+	}
+	
+	private onClick(event): void {
+		if(this.state == 8) { //anchor
+			if(this.focusP4A) {
+				this.focusP4A.selected = !this.focusP4A.selected;
+				this.redraw();
+			}
+		} else {
+			if(this.focusAnchor) { //编辑锚点
+				this.editorPart.editAnchor(this.focusAnchor);
+			} else { //selection
+				var sel:any = this.findLegend(event.layerX, event.layerY);
+					
+				if(sel != this.getSelComp)
+					this.setSelComp(sel);	
+			}
+		}
+	}
+	
+	findLegend(x:number, y:number): Legend {
+		for(const l of this.legendData){
+			const point = this.legendViews[l.id].viewLocation;
+			if(x >= point.x && y >= point.y && x <= point.x + this.legendSize && y <= point.y + this.legendSize)
+				return l;
+		}
+		return null;
+	}
+	
+	setSelComp(comp: any): void{
+		this.selection.setSel(comp);
+		this.redraw();
+		
+		this.editorPart.onSelectionChange();
+	}
+	
+	getSelComp(): any {
+		if(!this.selection)
+			return null;
+		return this.selection.getSel();
+	}
+	
+	buildSelInfo(){
+		var info = null;
+
+		const sel = this.getSelComp();
+		if(sel != null) {
+			//TODO
+		}
+		
+		if(info == null)
+			info = '~';
+		return info;
+	}
+	
+	getState(): number {
+		const sel = this.getSelComp();
+		return sel != null ? 1 : 0;
+	}
+	
+	addLegend(legendCode:string): Legend {
+		const l = new Legend();
+		l.code = legendCode;
+		l.width = 200;
+		l.height = 200;
+		this.legendData.push(l);
+		this.redraw();
+		
+		return l;
+	}
+	
+	storeAnchors(): void{
+		if(this.pointsForAnchor){
+			const arr = new Array<Anchor>();
+			for(const p of this.pointsForAnchor) {
+				if(p.selected){
+					if(p.anchor)
+						arr.push(p.anchor);
+					else {
+						const a = new Anchor();
+						a.code = p.code;
+						arr.push(a);
+					}
+				}
+			}
+			this.getSelComp().anchors = arr;
+			console.log(this.getSelComp());
+		}
+	}
+	
+	private onMouseMove(event): void {
+		const x = event.layerX;
+		const y = event.layerY;
+		if(this.state == 8) { //设置锚点
+			var focus: Point4Anchor = null;
+			for(const p of this.pointsForAnchor) {
+				const p1 = new Point(p.point.x - 3, p.point.y - 3);
+				const p2 = new Point(p.point.x + 3, p.point.y + 3);
+				if(x >= p1.x && y >= p1.y && x <= p2.x && y <= p2.y) {
+					focus = p;
+					break;
+				}
+			}
+			if(focus != this.focusP4A) {
+				this.focusP4A = focus;
+				this.redraw();
+			}
+		} else if(this.state == 9) { //绘图
+			//TODO
+			this.redraw();
+		} else {
+			//高亮显示锚点
+			var fa: Anchor = null;
+			const legend:Legend = this.getSelComp();
+			if(legend && legend.anchors){
+				for(const a of legend.anchors) {
+					const p:Point = this.legendViews[legend.id].anchorPoints[a.code];
+					const p1 = new Point(p.x - 3, p.y - 3);
+					const p2 = new Point(p.x + 3, p.y + 3);
+					if(x >= p1.x && y >= p1.y && x <= p2.x && y <= p2.y) {
+						fa = a;
+						break;
+					}
+				}
+			}
+			if(this.focusAnchor != fa) {
+				this.focusAnchor = fa;
+				this.redraw();
+			}
+			
+			var info = '';
+			if(this.focusAnchor) {
+				info = this.focusAnchor.code;
+				if(this.focusAnchor.name)
+					info += '    ' + this.focusAnchor.name;
+				if(this.focusAnchor.multiple)
+					info += '    动态数量锚点';
+				if(this.focusAnchor.acceptRelations && this.focusAnchor.acceptRelations.length > 0)
+					info += '    ' + this.focusAnchor.acceptRelations;
+			}
+			this.editorPart.dynInfo = info;
+		}
+	}
+	
+	private onKeydown(event): void {
+		//if(event.keyCode)
+		console.log(event.keyCode);
+	}
+
+}
+
+class LegendView {
+	
+	viewLocation:Point;
+	
+	legendLocation:Point;
+	
+	anchorPoints:Object = new Object();
+	
+}

+ 486 - 0
adm_comp/src/lib/TemplateEditor.ts

@@ -0,0 +1,486 @@
+import {Template, Container, MainPipe} from './DiagramModel';
+import {Point, BackgroundPaniter, ViewTool} from './UIUtils';
+import {ContainerRefPoint} from './ContainerRefPoint';
+import {Editor, Selection} from './Editor';
+
+/**
+ * 模板编辑器
+ * @author zhaoyk
+ */
+export class TemplateEditor extends Editor {
+
+	private util: EditUtil;
+	
+	templateData: Template;
+	
+	tmpMainPipe:Array<Array<Point>>;
+	currentTmpMainPipe:Array<Point>;
+	
+	refPoints: any;
+	currentRefPoint: any;
+	
+	protected initCanvasSize(): Point{
+		return new Point(2500, 2500);
+	}
+	
+	addListeners(): void {
+		this.canvas.addEventListener('click', event=>{this.onClick(event)});
+		this.container.addEventListener("mousemove", event=>{this.onMouseMove(event)});
+	}
+	
+	showData(template?: any): void {
+		this.templateData = (<Template>template);
+
+		this.selection = new Selection();
+		
+		this.setState(this.getState());
+
+		this.util = new EditUtil(this);
+		this.refPoints = {};
+		
+		this.redraw();
+	}
+
+	setState(state: number): void{
+		this.state = state;
+		
+		if(this.state == 9) 
+			this.tmpMainPipe = new Array<Array<Point>>();
+		else
+			this.tmpMainPipe = null;
+		
+		this.currentTmpMainPipe = null;	
+		this.currentRefPoint = null;
+	}
+			
+	redraw():void {
+		this.beforeRedraw();
+		
+		const root = this.templateData.frame;
+		this.util.prepareParent(root);
+		if(this.templateData.scatteredContainers)
+			this.templateData.scatteredContainers.forEach(con => this.util.prepareParent(con));
+
+		//绘制排列定位容器		
+		this.drawContainer(root);
+		
+		//绘制自由定位容器
+		if(this.templateData.scatteredContainers) {
+			const p = new Point(root.location.x, root.location.y);
+			p.y += root.size.y;
+			this.templateData.scatteredContainers.forEach(con => {
+				p.y += 20;
+				con.location = new Point(p.x, p.y);
+				this.drawContainer(con, true);
+				p.y += con.size.y;
+			});	
+		}
+		
+		//绘制干管
+		if(this.templateData.mainPipes) {
+			this.templateData.mainPipes.forEach(item => {
+				let color = "#5783FA";
+				if(item.bindEquipment)
+					color = "#000000";
+				this.ctx.strokeStyle = color; //#FA943B
+				this.ctx.setLineDash([]);
+				if(this.getSelComp() == item)
+					this.ctx.lineWidth = 2;
+				else
+					this.ctx.lineWidth = 1;
+					
+				this.ctx.beginPath();
+				for(const arr of item.locationPath){
+					var iter = 0;
+					for(var p of arr) {
+						p = this.util.toCanvas(p);
+						if(iter == 0)
+							this.ctx.moveTo(p.x, p.y);
+						else
+							this.ctx.lineTo(p.x, p.y);	
+						iter++;
+					}	
+				}
+				this.ctx.stroke();
+				this.ctx.closePath();
+			});
+		}
+		
+		//干管绘制过程
+		if(this.tmpMainPipe) {	
+			let color = "#5783FA";
+			for(const line of this.tmpMainPipe) {
+				var pre: Point = null;
+				for(const p of line) {
+					this.ctx.beginPath();
+					
+					//画点
+					ViewTool.paintPoint(this.ctx, p, color, true);
+					//画线
+					if(pre != null) {
+						this.ctx.strokeStyle = color;
+						this.ctx.lineWidth = 1;
+						this.ctx.setLineDash([]);
+						
+						this.ctx.moveTo(pre.x, pre.y);
+						this.ctx.lineTo(p.x, p.y);	
+						this.ctx.stroke();
+					}					
+
+					this.ctx.closePath();
+					
+					pre = p;
+				}
+			}
+		}
+		//干管绘制的定位参考点
+		if(this.currentRefPoint) {
+			this.ctx.beginPath();
+			ViewTool.paintPoint(this.ctx, this.currentRefPoint.location, "#ff5500", true);
+			this.ctx.closePath();
+		}
+	}
+	
+	private drawContainer(con:Container, drawLiberty?:boolean) {
+		if(!drawLiberty && con.position.absolute)
+			return;
+		
+		const liberty = con.position.absolute;
+		const location = !liberty ? this.util.locationOnCanvas(con) : this.util.toCanvas(con.location);
+		
+		if(!liberty && con.parent != null)
+			this.refPoints[con.id] = ContainerRefPoint.getRefPoints(con.id, location, con.size);
+				
+		const equip = con.equipmentTypes && con.equipmentTypes.length > 0;
+		const color = equip ? (!liberty ? "#000000" : "#2a7652") : (!liberty ? "#9b9ea3": "#42B983");
+		const dash = equip ? [] : [3, 2];
+		
+		this.ctx.lineWidth = 1;
+		this.ctx.setLineDash(dash);
+		this.ctx.strokeStyle = color;
+		
+		this.ctx.beginPath();
+		this.ctx.rect(location.x, location.y, con.size.x, con.size.y);	
+		this.ctx.stroke();
+		
+		if(this.getSelComp() == con) {
+			this.ctx.fillStyle = "rgba(0, 127, 255, 0.1)";
+			this.ctx.fill();
+		}
+
+		if(con.dynGroup && con.parent){
+			var mark = "";
+			if(con.parent.layout.layout == 1)
+				mark = "↓";
+			else if(con.parent.layout.layout == 2)
+				mark = "→";
+			if(mark.length > 0)
+				ViewTool.drawText(this.ctx, mark, new Point(location.x, location.y), con.size);	
+		}
+
+		if(equip) {
+			var info = "";
+			
+			if(con.name)
+				info += con.name + "\n";				
+			info += "[ " + con.equipmentTypes + " ]";	
+			if(con.dataFilter)
+				info += ViewTool.filterToStr(con.dataFilter);
+			if(con.equipPack)
+				info += "  <打包>";
+			ViewTool.drawText(this.ctx, info, new Point(location.x + 10, location.y + 10), con.size);
+			
+			var layoutInfo = "";
+			var offset: Point;
+			if(con.layout.layout == 1) {
+				layoutInfo = "---\n---\n---";
+				offset = new Point(-5, -30);
+			} else if(con.layout.layout == 2) {
+				layoutInfo = "|   |   |";	
+				offset = new Point(-10, -10);
+			}
+			ViewTool.drawText(this.ctx, layoutInfo, new Point(location.x + (con.size.x / 2) + offset.x , location.y + (con.size.y / 2) + offset.y), new Point(con.size.x / 2, con.size.y / 2));
+		}
+		
+		if(con.children)
+			con.children.forEach(item => this.drawContainer(<Container>item));
+	}
+			
+	// private redrawOnScroll(_this) {	
+	// 	const top = _this.container.scrollTop;
+	// 	const left = _this.container.scrollLeft;
+	// 	_this.canvas.style.top = top + 'px';
+	// 	_this.canvas.style.left = left + 'px';
+		
+	// 	_this.draw(_this.mapData);
+	// }
+	
+	private onClick(event): void {
+		if(this.state != 9) { //selection
+			var sel:any = this.util.findMainPipe(event.layerX, event.layerY);
+			if(sel == null)
+				sel = this.util.findContainer(event.layerX, event.layerY);
+				
+			if(sel != this.getSelComp)
+				this.setSelComp(sel);
+		} else { //paint main pipe
+			if(this.currentTmpMainPipe == null) {
+				this.currentTmpMainPipe = new Array<Point>();
+				this.tmpMainPipe.push(this.currentTmpMainPipe);
+			}
+			
+			const point = new Point(event.layerX, event.layerY);
+			this.adjustPointForMp(point);
+			this.currentTmpMainPipe.push(point);
+			
+			this.redraw();
+		}
+	}
+	
+	setSelComp(comp: any): void{
+		this.selection.setSel(comp);
+		this.redraw();
+		
+		this.editorPart.onSelectionChange();
+	}
+	
+	getSelComp(): any {
+		if(!this.selection)
+			return null;
+		return this.selection.getSel();
+	}
+	
+	buildSelInfo(){
+		var info = null;
+
+		const sel = this.getSelComp();
+		if(sel != null) {
+			info = 'id: ' + sel.id;	
+			if(sel.name)
+				info += '  |  名称: ' + sel.name;
+			if(sel.layout) {
+				if(sel.layout.layout == 1)
+					info += '  |  布局方式: 行式';
+				else if(sel.layout.layout == 2)
+					info += '  |  布局方式: 列式';
+			}
+			if(sel.equipmentTypes && sel.equipmentTypes.length > 0){
+				info += '  |  设备类型: ' + sel.equipmentTypes;
+			}
+			if(sel.dataFilter) {
+				info += '  |  过滤条件: '
+				info += ViewTool.filterToStr(sel.dataFilter);
+			}
+			if(sel.equipPack)
+				info += '  |  <打包>';
+			if(sel.locationPath){
+				var str = '';
+				const arr:Array<Array<Point>> = new Array<Array<Point>>();
+				for(const line of sel.locationPath) {
+					const ps:Array<Point> = new Array<Point>();
+					arr.push(ps);
+					
+					line.forEach(item => {
+						ps.push(this.util.toCanvas(item));
+					});
+					
+					if(str.length > 0)
+						str += ", ";
+					str += ViewTool.pointsToStr(ps);		
+				}
+				info += '  |  ' + str;
+			}
+		}
+		
+		if(info == null)
+			info = '~';
+		return info;
+	}
+	
+	getState(): number {
+		const sel = this.getSelComp();
+		if(!sel)
+			return 0;
+		if(sel.locationPath)
+			return 2;
+		return 1;
+	}
+	
+	getCompById(id: string): any {
+		if(!id)
+			return null;
+			
+		if(this.templateData.mainPipes) {
+			for(const mp of this.templateData.mainPipes) {
+				if(mp.id == id)
+					return mp;
+			}
+		}
+		
+		return this.getConById(id);
+	}
+	
+	getConById(id:string): Container {
+		return this.findCon(id, this.templateData.frame);
+	}
+
+	private findCon(id:string, con: Container): Container {
+		if(con.id == id)
+			return con;
+		for(const sub of con.children) {
+			const rtn = this.findCon(id, <Container>sub);
+			if(rtn != null)
+				return rtn;
+		}
+		return null;
+	}
+
+	//干管绘制时,自动调整横平竖直
+	private adjustPointForMp(point: Point) {
+		if(this.currentTmpMainPipe && this.currentTmpMainPipe.length > 0) {
+			const pre = this.currentTmpMainPipe[this.currentTmpMainPipe.length - 1];
+			if(Math.abs(pre.x - point.x) < Math.abs(pre.y - point.y))
+				point.x = pre.x;
+			else
+				point.y = pre.y;
+		}
+	}
+	
+	private onMouseMove(event): void {
+		if(this.state == 9) {
+			const point = new Point(event.layerX, event.layerY);
+			this.adjustPointForMp(point);
+			
+			//显示当前坐标位置
+			this.editorPart.dynInfo = ViewTool.pointToStr(point);
+			
+			//计算定位参考点
+			var min: number = Number.MAX_VALUE;
+			var nearRp: any;
+			for(const cId in this.refPoints) {
+				const rps = this.refPoints[cId];
+				if(rps && rps.length) {
+					for(const rp of rps){
+						const p1 = rp.location;
+						const distance = Math.sqrt(Math.pow(p1.x - point.x, 2) + Math.pow(p1.y - point.y, 2));
+						if(distance < min) {
+							min = distance;
+							nearRp = rp;
+						}
+					}
+				}
+			}
+			this.currentRefPoint = nearRp;
+			
+			this.redraw();
+		}
+	}
+
+}
+
+class EditUtil {
+	
+	private editor:TemplateEditor;
+	
+	private template:Template;
+
+	constructor(editor:TemplateEditor) {
+		this.editor = editor;
+		this.template = editor.templateData;
+	}
+	
+	prepareParent(con:Container) {
+		con.children.forEach(item => {
+			item.parent = con;
+			this.prepareParent(<Container>item);
+		});
+	}
+	
+	locationOnDiagram(con:Container): Point {
+		const p:Point = new Point(0, 0);
+		var c:Container = con;
+		while (c != null){
+			p.x += c.location.x;
+			p.y += c.location.y;
+	
+			c = c.parent;
+		}
+		return p;
+	}
+	
+	locationOnCanvas(con:Container): Point {
+		return this.toCanvas(this.locationOnDiagram(con));
+	}
+	
+	toCanvas(point:Point): Point {
+		return new Point(point.x + this.editor.margin, point.y + this.editor.margin); 
+	}
+	
+	findContainer(x:number, y:number): Container {
+		var rtn = this.findCon(x, y, this.template.frame);
+		if(!rtn && this.editor.templateData.scatteredContainers) {
+			for(const con of this.editor.templateData.scatteredContainers){
+				rtn = this.findCon(x, y, con);
+				if(rtn)
+					break;
+			}
+		}
+		return rtn;
+	}
+	
+	private findCon(x:number, y:number, target:Container): Container {
+		if(this.containsPoint(target, x, y)){
+			for(const item of target.children) {
+				const c = this.findCon(x, y, <Container>item);
+				if(c != null)
+					return c;
+			}
+			return target;
+		}
+		return null;
+	}
+	
+	containsPoint(con:Container, x:number, y:number):boolean{
+		if(con.location){
+			const point = con.position.absolute ? this.toCanvas(con.location) : this.locationOnCanvas(con);
+			return x >= point.x && y >= point.y && x <= point.x + con.size.x && y <= point.y + con.size.y;	
+		}
+		return false;
+	}
+	
+	findMainPipe(x:number, y:number): any {
+		if(this.template.mainPipes) {
+			for(const mp of this.template.mainPipes){
+				for(const line of mp.locationPath) {
+					var pre: Point = null;
+					for(var point of line) {
+						point = this.toCanvas(point);
+						if(pre != null) {
+							var x1:number;
+							var y1:number;
+							var x2:number;
+							var y2:number;
+							const scope = 3;
+							if(pre.y == point.y) { //水平
+								y1 = pre.y - scope;
+								y2 = pre.y + scope;
+								x1 = Math.min(pre.x, point.x);
+								x2 = Math.max(pre.x, point.x);
+							} else { //垂直
+								x1 = pre.x - scope;
+								x2 = pre.x + scope;
+								y1 = Math.min(pre.y, point.y);
+								y2 = Math.max(pre.y, point.y);
+							}
+							if(x >= x1 && y >= y1 && x <= x2 && y <= y2)
+								return mp;
+						}
+						pre = point;	
+					}	
+				}
+			}
+		}
+		return null;
+	}
+		
+}

+ 143 - 0
adm_comp/src/lib/UIUtils.ts

@@ -0,0 +1,143 @@
+import {DataFilter} from './DiagramModel';
+/**
+ * ui操作的基础工具
+ * @author zhaoyk
+ */
+
+export class Point {
+	
+	x: number;
+	y: number;
+	
+	constructor(pX: number, pY: number) {
+		this.x = pX;
+		this.y = pY;
+	}
+	
+}
+
+export class BackgroundPaniter {
+		
+	private canvas: HTMLCanvasElement;
+	
+	private ctx: CanvasRenderingContext2D;
+	
+	private unit = 10;
+	
+	constructor(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
+		this.canvas = canvas;
+		this.ctx = ctx;
+	}
+	
+	paint(){
+		var xCount = 0;
+		var yCount = 0;
+		this.ctx.lineWidth = 1;
+		for(var x = this.unit; ; x += this.unit){
+			if(x > this.canvas.width)
+				break;
+				
+			xCount++;
+			if(xCount % 5 == 0)
+				this.ctx.strokeStyle = "#f1f1f1";
+			else
+				this.ctx.strokeStyle = "#f7f7f7";
+								
+			this.ctx.beginPath();
+			this.ctx.moveTo(x, 0);
+			this.ctx.lineTo(x, this.canvas.height);
+			this.ctx.stroke();
+		}
+		for(var y = this.unit; ; y += this.unit){
+			if(y > this.canvas.height)
+				break;
+				
+			yCount++;
+			if(yCount % 5 == 0)
+				this.ctx.strokeStyle = "#f4f4f4";
+			else
+				this.ctx.strokeStyle = "#fafafa";
+				
+			this.ctx.beginPath();
+			this.ctx.moveTo(0, y);
+			this.ctx.lineTo(this.canvas.width, y);
+			this.ctx.stroke();
+		}		
+	}
+	
+}
+
+export class ViewTool {
+	
+	static paintPoint(ctx: CanvasRenderingContext2D, point:Point, color:string, fill: boolean, r?: number): void {
+		if(!r)
+			r = 2;
+		if(fill) {
+			ctx.fillStyle = color;
+			ctx.arc(point.x, point.y, r, 0, Math.PI * 2);			
+			ctx.fill();
+		} else {
+			ctx.strokeStyle = color;
+			ctx.arc(point.x, point.y, r, 0, Math.PI * 2);
+			ctx.stroke();
+		}
+	}
+	
+	static drawText(ctx: CanvasRenderingContext2D, content: string, location: Point, size: Point):void{
+		const fontSize = 10;
+		ctx.font = "lighter " + fontSize + "px Microsoft YaHei";
+		ctx.textBaseline = "bottom";
+		
+		var rows = [];
+		var tmp = "";
+		for(var i = 0; i < content.length; i++) {
+			var nl = content[i] == '\n';
+			if(!nl){
+				tmp += content[i];
+				nl = ctx.measureText(tmp).width >= size.x - 20;
+			}
+			if(nl) {
+				rows.push(tmp);
+				tmp = "";
+			}
+		}
+		if(tmp.length > 0)
+			rows.push(tmp);
+		
+		for(var i = 0; i < rows.length; i++) {
+			ctx.strokeText(rows[i], location.x, location.y + ((i + 1) * (fontSize + 4)));
+		}
+	}
+	
+	static pointToStr(point: Point): string {
+		return '(x: ' + point.x + ', y: ' + point.y + ')';
+	}
+	
+	static pointsToStr(points: Array<Point>): string {
+		var str = '';
+		if(points && points.length){
+			points.forEach(item => {
+				if(str.length > 0)
+					str += ' , ';
+				str += ViewTool.pointToStr(item);
+			});
+		}
+		return '[' + str + ']';
+	}
+	
+	static filterToStr(filter: DataFilter) {
+		var info = '';
+		if(filter) {
+			info += ' :'
+			if(filter.not)
+				info += '!';
+			if(filter.type == 1)
+				info += filter.value;
+			else {
+				info += '组合过滤';
+			}
+		}
+		return info;
+	}
+	
+}

+ 22 - 0
adm_comp/src/main.ts

@@ -0,0 +1,22 @@
+import Vue from "vue";
+import App from "./App.vue";
+import router from "./router";
+import store from "./store";
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+Vue.config.productionTip = false;
+
+Vue.use(ElementUI);
+
+import common from '@/common/Common';
+Vue.prototype.$common = common;
+
+import axios from 'axios';
+Vue.prototype.$axios = axios;
+
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount("#app");

+ 20 - 0
adm_comp/src/polyfills.ts

@@ -0,0 +1,20 @@
+
+import 'reflect-metadata';
+
+/**
+*  polyfill location
+*/
+(global => {
+	if(!global.location.origin){
+		const location:any = global.location;
+		const {protocol, hostname, port} = location;
+		location.origin = `${protocol}//${hostname}${port? ':'+port : ''}`;
+	}
+})(window);
+
+if(process.env.NODE_ENV!=='production'){
+	Object.assign(window,{
+		_PREFIX_ : 'tui',
+		_DEV_ : true
+	});
+}

+ 55 - 0
adm_comp/src/router/index.ts

@@ -0,0 +1,55 @@
+import Vue from "vue";
+import VueRouter, { RouteConfig } from "vue-router";
+
+Vue.use(VueRouter);
+
+const routes: Array<RouteConfig> = [
+	{
+		  path: "/jobs",
+		  name: "jobs",
+		  component: () =>import("../views/Jobs.vue")
+	},
+	{
+		  path: "/diagramView/:data/:templateId",
+		  name: "diagramView",
+		  component: () =>import("../views/DiagramView.vue")
+	},
+	{
+		  path: "/diagramTemplate",
+		  name: "diagramTemplate",
+		  component: () =>import("../views/DiagramTemplate.vue")
+	},
+	{
+		  path: "/diagramLegend",
+		  name: "diagramLegend",
+		  component: () =>import("../views/DiagramLegend.vue")
+	},
+	{
+		  path: "/legendIcon",
+		  name: "legendIcon",
+		  component: () =>import("../views/LegendIcon.vue")
+	},
+	{
+		  path: "/diagram",
+		  name: "diagram",
+		  component: () =>import("../views/Diagram.vue")
+	},	
+	{
+		  path: "/map",
+		  name: "map",
+		  component: () =>import("../views/Map.vue")
+	},
+	{
+		  path: "/map0",
+		  name: "map0",
+		  component: () =>import("../views/Map0.vue")
+	}
+];
+
+const router = new VueRouter({
+  mode: "hash",
+  base: process.env.BASE_URL,
+  routes
+});
+
+export default router;

+ 13 - 0
adm_comp/src/shims-tsx.d.ts

@@ -0,0 +1,13 @@
+import Vue, { VNode } from "vue";
+
+declare global {
+  namespace JSX {
+    // tslint:disable no-empty-interface
+    interface Element extends VNode {}
+    // tslint:disable no-empty-interface
+    interface ElementClass extends Vue {}
+    interface IntrinsicElements {
+      [elem: string]: any;
+    }
+  }
+}

+ 4 - 0
adm_comp/src/shims-vue.d.ts

@@ -0,0 +1,4 @@
+declare module "*.vue" {
+  import Vue from "vue";
+  export default Vue;
+}

+ 11 - 0
adm_comp/src/store/index.ts

@@ -0,0 +1,11 @@
+import Vue from "vue";
+import Vuex from "vuex";
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {}
+});

+ 1 - 0
adm_comp/src/styles/var.scss

@@ -0,0 +1 @@
+ $green : #42b983

+ 413 - 0
adm_comp/src/views/Diagram.vue

@@ -0,0 +1,413 @@
+<template>
+	<div>
+		<el-row>
+		  <!-- 模板树 -->
+		  <el-col :span="6" >
+			<div style="height: 100vh;overflow: scroll;">
+				<el-tree :data="diagrams" :props="{children: 'children', label: nodeLabel, value: 'id'}"
+					@node-click="open"
+					default-expand-all
+					:expand-on-click-node="false">
+					<span class="custom-tree-node" slot-scope="{ node, data }">
+						<span>{{ node.label}}</span>
+						<span style="padding-left: 10px;">
+							<el-button type="text" size="mini" icon="el-icon-plus" @click="() => newDiagram(node)"
+								v-if="data.objType == 'folder' && data.type == 'type'">
+								创建系统图
+							</el-button>
+							<el-popconfirm title="确定删除?" v-if="data.objType != 'folder'" @onConfirm="() => delDiagram(node)">
+							  <el-button slot="reference" type="text" size="mini" icon="el-icon-remove">删除</el-button>
+							</el-popconfirm>
+						</span>
+					</span>
+				</el-tree>
+			</div>
+		  </el-col>
+		  
+		  <!-- 编辑区 -->
+		  <el-col :span="18" style="border-left: 1px solid #f4f4f4">
+			<!-- 工具条 -->
+			<div ref="header" style="padding: 2px;border-bottom: 1px solid #f4f4f4">
+				<span class="editor-title">{{currentDiagram.name}}</span>
+				
+				<el-button-group>
+					<el-button size="mini" @click="() => prjEnv()">项目环境</el-button>
+				</el-button-group>
+				
+				<el-button-group>
+					<el-button size="mini" @click="() => chooseTemplate()" :disabled="state == -1">选择模板</el-button>
+					<el-button size="mini" @click="() => loadData()" :disabled="state == -1">加载数据</el-button>
+				</el-button-group>
+				
+				<el-button-group>
+					<el-button size="mini" @click="() => {}" :disabled="state != 1">上移/左移</el-button>
+					<el-button size="mini" @click="() => {}" :disabled="state != 1">下移/右移</el-button>
+				</el-button-group>
+				
+				<!--选中信息-->
+				<div class="info">
+					<span>{{selectionInfo}}</span>
+					<span style="margin-left: 30px;color: #a3a3a3;">{{dynInfo}}</span>
+				</div>
+			</div>
+			
+			 <!-- 编辑界面 -->
+			<div ref="editor" :style="editorStyle">
+				
+			</div>
+		  </el-col>
+		</el-row>
+
+		<!-- 编辑项目环境对话框 -->
+		<el-dialog title="项目环境" :visible.sync="dlgEnvVisible">
+			<el-form :model="propsData" label-width="100px">
+				<el-form-item label="项目Id">
+					<el-autocomplete
+					  class="inline-input"
+					  v-model="propsData.projectId"
+					  :fetch-suggestions="getPrjIds"></el-autocomplete>
+				</el-form-item>				
+				<el-form-item label="系统实例Id">
+					<el-input v-model="propsData.systemId"></el-input>
+				</el-form-item>
+				<el-form-item label="集团编码">
+					<el-input v-model="propsData.groupCode"></el-input>
+				</el-form-item>
+			</el-form>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgEnvVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyEnv'>确 定</el-button>
+			</div>
+		</el-dialog>
+		
+		<!-- 选择模板对话框 -->
+		<el-dialog title="系统图模板" :visible.sync="dlgTemplateVisible">
+			<el-form :model="propsData" label-width="100px">
+				<el-form-item label="选择模板">
+					<el-select
+					  v-model="propsData.templateId"
+					  style="width: 600px;"
+					  >
+					  <el-option
+						v-for="item in templateOptions"
+						:key="item.id"
+						:label="item.name"
+						:value="item.id">
+					  </el-option>
+					</el-select>
+				</el-form-item>
+			</el-form>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgTemplateVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyTemplate'>确 定</el-button>
+			</div>
+		</el-dialog>
+	
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	import {DiagramEditor} from '@/lib/DiagramEditor';
+
+	@Component({})
+	export default class DiagramTemplate extends Vue {
+		projectId = 'Pj1101080665';
+		systemId = '';
+		groupCode = '';
+		
+		editorStyle = {
+			width: "100%",
+			height: "",
+			"overflow": "scroll"
+		};
+		
+		diagrams = [];
+		currentDiagram:any = {};
+		
+		editor:DiagramEditor = null;
+		state = -1; //编辑状态(-1:关闭; 0:打开; 1:容器编辑中; 2:绘制干管)
+
+		dlgEnvVisible = false;
+		dlgTemplateVisible = false;
+		
+		propsData: any = {};
+		templateOptions:any[] = [];
+				
+		selectionInfo = '~';
+		dynInfo = '';		
+				
+		mounted() {
+			this.onWindowresize();
+			this.editor = new DiagramEditor(this.$refs.editor);
+			this.editor.setEditorPart(this);
+			
+			this.loadDiagrams();
+		}
+		
+		loadDiagrams(){
+			this['$axios'].get(this['$common'].url + "diagram0/getDiagrams" + this.buildUrlParams())
+				.then(res => {
+					this.diagrams = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+		}
+		
+		buildUrlParams(){
+			var str = "?projectId=" + this.projectId;
+			if(this.systemId)
+				str += '&systemId=' + this.systemId;
+			if(this.groupCode)
+				str += '&groupCode=' + this.groupCode;
+			return str;
+		}
+		
+		//系统图模板树的节点显示
+		nodeLabel(data, node) {
+			if(data.objType == 'folder') //文件夹
+				return data.name;
+			else //图
+				return '[系统图] ' + data.name;
+		}
+
+		prjEnv() {
+			this.propsData = {projectId: this.projectId, systemId: this.systemId, groupCode: this.groupCode};
+			this.dlgEnvVisible = true;
+		}
+		
+		getPrjIds(queryString, cb){
+			const list = [
+				{ "value": "Pj1101080665", "code": "JDKJ" },
+				{ "value": "Pj1101050036", "code": "JM" },
+				{ "value": "Pj1101050031", "code": "YT" }
+			];
+			cb(list);
+		}
+		
+		modifyEnv() {
+			const reload = this.projectId != this.propsData.projectId;
+			
+			this.projectId = this.propsData.projectId;
+			this.systemId = this.propsData.systemId;
+			this.groupCode = this.propsData.groupCode;
+			this.dlgEnvVisible = false;
+			
+			if(reload) {
+				this.loadDiagrams();	
+				this.open(new Object());
+			}
+		}
+				
+		//新建模板文件
+		newDiagram(node){
+			this.$prompt('请输入系统图名称', '提示', {
+						confirmButtonText: '确定',
+						cancelButtonText: '取消',
+						type: 'info',
+						inputValidator: val => {return val != null}
+			       }).then(instance => {
+						const params:any = {name: instance['value'], type: node.data.id};
+						this['$axios'].post(this['$common'].url + "diagram0/newDiagram" + this.buildUrlParams(), params)
+							.then(res => {
+								const root = res.data;
+								if(root.message == 'success') {
+									const newObj = root.data;
+									node.data.children.push(newObj);
+									
+									this.$message({
+										type: 'success',
+										message: '操作成功!'
+									});
+									
+									this.open(newObj);
+								}
+							})
+							.catch(err => {
+								console.log(err);
+							});	
+			       }).catch(() => {
+								
+			       });
+		}
+		
+		//删除模板
+		delDiagram(node){
+			const params:any = {id: node.data.id};
+			this['$axios'].post(this['$common'].url + "diagram0/delDiagram", params)
+				.then(res => {
+					const root = res.data;
+					if(root.data == true) {
+						const parent = node.parent;
+						const children = parent.data.children || parent.data;
+						const index = children.findIndex(d => d.id === node.data.id);
+						children.splice(index, 1);
+						
+						this.$message({
+							type: 'success',
+							message: '操作成功!'
+						});
+						this.open({});
+					}
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+		}
+		
+		//在编辑器中打开模板文件
+		open(data){
+			if(data.objType == 'folder') //文件夹
+				return;
+				
+			if(this.currentDiagram != data) {
+				this.currentDiagram = data;
+				this.selectionInfo = '~';
+				this.dynInfo = '';
+				
+				this.editor.show(this.currentDiagram);
+				
+				if(!this.currentDiagram.id) //编辑对象为空
+					this.state = -1;
+				else {
+					this.onSelectionChange();
+				}
+			}
+		}
+
+		chooseTemplate() {
+			this.templateOptions = [];
+			this['$axios'].get(this['$common'].url + "template/getTemplatesByType?diagramType=" + this.currentDiagram.type)
+				.then(res => {
+					this.templateOptions = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+			
+			const props:any = new Object();
+			props.templateId = this.currentDiagram.templateId;
+			
+			this.propsData = props;
+			
+			this.dlgTemplateVisible = true;
+		}
+		
+		modifyTemplate() {
+			const tId = this.propsData.templateId;
+			this.currentDiagram.templateId = tId;
+
+			this.dlgTemplateVisible = false;
+			this.editor.redraw();
+			
+			const params:any = {diagramId: this.currentDiagram.id, templateId: this.propsData.templateId};
+			//this.propsData = {id: params.newCompId}; //方便后续选中操作
+			this.callAction('setTemplate', params);
+		}
+		
+		loadData() {
+			const params:any = {diagramId: this.currentDiagram.id};
+			//this.propsData = {id: params.newCompId}; //方便后续选中操作
+			this.callAction('loadData', params);
+		}
+		
+		created(){
+			//window.addEventListener('resize', this.onWindowresize);
+		}
+		
+		onWindowresize(){
+			this.editorStyle.height = (window.innerHeight - this.$refs.header['offsetHeight']) + 'px';
+		}
+		
+		//选中组件的事件触发
+		onSelectionChange(){
+			this.state = this.editor.getState();
+			
+			if(this.editor)
+				this.selectionInfo = this.editor.buildSelInfo();
+		}
+		
+		//调用服务端编辑操作,返回新的系统图对象,然后刷新
+		callAction(action, params) {
+			this['$axios'].post(this['$common'].url + 'diagram0/' + action, params)
+				.then(res => {
+					const root = res.data;
+					if(root.message == 'success') {
+						//记录之前的选中对象id
+						// var selId = this.propsData.id;
+						// if(!selId) {
+						// 	const sel:any = this.editor.getSelComp();	
+						// 	if(sel)
+						// 		selId = sel.id;
+						// }
+						
+						const newObj = root.data;
+						if(newObj == null)
+							return;
+						
+						for(const i in newObj) {
+							this.currentDiagram[i] = newObj[i];
+						}
+						
+						// this.$message({
+						// 	type: 'success',
+						// 	message: '操作成功!'
+						// });
+						
+						if(this.dlgTemplateVisible)
+							this.dlgTemplateVisible = false;
+
+						this.propsData = {};
+						
+						this.editor.show(this.currentDiagram);
+						//this.editor.setSelComp(this.editor.getCompById(selId));
+					}
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+		}
+
+		//切换编辑状态
+		changeState(state) {
+			this.state = state;
+			this.editor.setState(state);
+		}
+		
+	}
+</script>
+
+<style>
+	.panel {
+		padding: 30px;
+	}
+	.custom-tree-node {
+		/* 	flex: 1;
+		display: flex; */
+		align-items: center;
+		justify-content: space-between;
+		font-size: 14px;
+		padding-right: 20px;
+	}
+	.editor-title {
+		font-size: 14px;
+		font-weight: bold;
+		padding:0 10px;
+	}
+	.info {
+		font-size: 12px;
+		padding:0 10px;
+	}
+	.el-dialog-div{
+		height: 60vh;
+		overflow: auto;
+	}
+</style>

+ 583 - 0
adm_comp/src/views/DiagramLegend.vue

@@ -0,0 +1,583 @@
+<template>
+	<div>
+		<div class="top-panel" ref="top">
+			<span>选择系统-专业: </span>
+			<el-cascader v-model="sysVal" :options="majSys" @change="sysChange"
+				:props="{ expandTrigger: 'hover', value:'code', label:'name' }" style="width:400px">
+			</el-cascader>
+		</div>
+		
+		<div>
+			<!-- 工具条 -->
+			<div ref="header" style="padding: 2px;border-bottom: 1px solid #f4f4f4">
+				<span class="editor-title">{{currentSys}}</span>
+				
+				<el-button-group>
+					<el-button size="mini" @click="() => addLegend()" :disabled="!(state == 0 || state == 1)">添加图例</el-button>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => setProps('legend')" :disabled="state != 1">属性设置</el-button>
+					<el-popconfirm title="确定删除选中的图例?" @onConfirm="() => delLegend()">
+						<el-button slot="reference" size="mini" :disabled="state != 1">删除</el-button>
+					</el-popconfirm>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => editFilter()" :disabled="!(state == 1 && state != 9)">数据匹配条件</el-button>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => beginSetAnchors()" :disabled="!(state == 1 && state != 9)">选择锚点</el-button>
+					<el-button size="mini" @click="() => cancelSetAnchors()" :disabled="state != 8">取消</el-button>
+					<el-button size="mini" @click="() => finishSetAnchors()" :disabled="state != 8">完成</el-button>
+				</el-button-group>
+				&nbsp;
+				<!-- <el-button-group>
+					<el-button size="mini" @click="() => beginMainLine()" :disabled="state == -1 || state == 9">绘制图形</el-button>
+					<el-button size="mini" @click="() => nextLine()" :disabled="state != 9">下一条线</el-button>
+					<el-button size="mini" @click="() => cancelMainLine()" :disabled="state != 9">取消绘制</el-button>
+					<el-button size="mini" @click="() => finishMainLine()" :disabled="state != 9">完成绘制</el-button>
+				</el-button-group> -->
+				
+				<!--选中信息-->
+				<div class="info">
+					<span>{{selectionInfo}}</span>
+					<span style="margin-left: 30px;color: #a3a3a3;">{{dynInfo}}</span>
+				</div>
+			</div>
+			
+			 <!-- 编辑界面 -->
+			<div ref="editor" :style="editorStyle">
+				
+			</div>
+		</div>
+
+		<!-- 编辑图例属性对话框 -->
+		<el-dialog title="图例属性" :visible.sync="dlgLegendVisible">
+			<div class="el-dialog-div">
+				<el-form :model="propsData" label-width="140px">
+					<el-form-item label="图例代码">
+						<el-input v-model="propsData.code"></el-input>
+					</el-form-item>
+					<el-form-item label="图例名称">
+						<el-input v-model="propsData.name"></el-input>
+					</el-form-item>
+					<el-form-item label="备注">
+						<el-input
+						  type="textarea"
+						  :rows="2"
+						  v-model="propsData.remark">
+						</el-input>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="宽度">
+						<el-input-number v-model="propsData.width" :min="50" :max="250" :step="10"></el-input-number>
+					</el-form-item>
+					<el-form-item label="高度">
+						<el-input-number v-model="propsData.height" :min="50" :max="250" :step="10"></el-input-number>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="支持的设备类型">
+						<el-select
+						  v-model="propsData.equipTypes"
+						  multiple 
+						  placeholder="设备类型,输入名称或代码搜索"
+						  style="width: 600px;"
+						  filterable
+						  remote
+						  :remote-method="queryEquipTypes"
+						  >
+						  <el-option
+							v-for="item in equipOptions"
+							:key="item.id"
+							:label="item.name + ' - ' + item.code"
+							:value="item.code">
+						  </el-option>
+						</el-select>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="适配的系统图类型">
+						<el-select
+						  v-model="propsData.diagramTypes"
+						  multiple
+						  style="width: 600px;"
+						  clearable
+						  >
+						  <el-option
+							v-for="item in diagramTypes"
+							:key="item.code"
+							:label="item.name + ' - ' + item.code"
+							:value="item.code">
+						  </el-option>
+						</el-select>
+					</el-form-item>
+				</el-form>
+			</div>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgLegendVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyLegend'>确 定</el-button>
+			</div>
+		</el-dialog>
+
+		<!-- 编辑锚点对话框 -->
+		<el-dialog title="锚点属性" :visible.sync="dlgAnchorVisible">
+			<div class="el-dialog-div">
+				<el-form :model="propsData" label-width="180px">
+					<el-form-item label="锚点编码">
+						<el-input v-model="propsData.code" :disabled="true"></el-input>
+					</el-form-item>
+					<el-form-item label="锚点名称">
+						<el-input v-model="propsData.name"></el-input>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="动态">
+						<el-checkbox v-model="propsData.multiple">动态数量锚点</el-checkbox>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="类型">
+						<el-radio-group v-model="propsData.type">
+							<el-radio label="non">无</el-radio>
+							<el-radio label="in">进</el-radio>
+							<el-radio label="out">出</el-radio>
+						</el-radio-group>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="可连接的关系类型">
+						<el-cascader
+						  placeholder="搜索"
+						  :options="dicRels" :props="{expandTrigger: 'hover', multiple: true, label: 'name', value: 'code'}"
+						  v-model="propsData.acceptRelations" filterable collapse-tags clearable 
+						  ref="relCascader" style="width: 400px;"></el-cascader>
+					</el-form-item>
+					<el-form-item label="连线另一端的设备类型">
+						<el-select
+						  v-model="propsData.toEquipmentTypes"
+						  multiple 
+						  placeholder="设备类型,输入名称或代码搜索"
+						  style="width: 600px;"
+						  filterable
+						  remote
+						  :remote-method="queryEquipTypes"
+						  >
+						  <el-option
+							v-for="item in equipOptions"
+							:key="item.id"
+							:label="item.name + ' - ' + item.code"
+							:value="item.code">
+						  </el-option>
+						</el-select>
+					</el-form-item>
+				</el-form>
+			</div>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgAnchorVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyAnchor'>确 定</el-button>
+			</div>
+		</el-dialog>
+
+		<!-- 设备过滤对话框 -->
+		<el-dialog title="设备过滤" :visible.sync="dlgFilterVisible">
+			<FilterPanel ref="filterPanel" :dataFilter="dataFilter"></FilterPanel>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgFilterVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyFilter'>确 定</el-button>
+			</div>
+		</el-dialog>
+
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	import FilterPanel from '@/components/FilterPanel.vue';
+	import {LegendEditor} from '@/lib/LegendEditor';
+
+	@Component({components:{FilterPanel}})
+	export default class DiagramLegend extends Vue {
+		majSys = [];
+		sysVal = [];
+		currentSys = '';
+		currentData: any[] = [];
+		
+		editorStyle = {
+			width: "100%",
+			height: "",
+			"overflow": "scroll"
+		};
+				
+		editor:LegendEditor = null;
+		state = -1; //编辑状态(-1:关闭; 0:打开; 1:容器编辑中; 2:绘制干管)
+
+		dlgLegendVisible = false;
+		dlgAnchorVisible = false;
+		dlgFilterVisible = false;
+		
+		propsData: any = {};
+		dataFilter: any = {};
+		selectionInfo = '~';
+		dynInfo = '';
+		
+		equipTypes:any[] = null; //编辑时,系统对应的设备类型
+		equipOptions:any[] = []; //属性设定时,下拉框中的设备类型列表(动态)
+		dicRels:any[] = [];
+		dicEquips:any[] = [];
+		diagramTypes:any[] = [];
+		
+		currentAnchor: any;
+		
+		sysChange(){
+			this.currentSys = this.sysVal[this.sysVal.length - 1];
+			
+			const params:any = {sys: this.currentSys};
+			this['$axios'].get(this['$common'].url + "legend/getLegends?sys=" + this.currentSys)
+				.then(res => {
+					this.currentData = res.data.data;
+					this.openEditor(this.currentData);
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+		}
+		
+		mounted() {
+			this.onWindowresize();
+			this.editor = new LegendEditor(this.$refs.editor);
+			this.editor.setEditorPart(this);
+								
+			this['$axios'].get(this['$common'].url + "majSys")
+				.then(res => {
+					this.majSys = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+			
+			this['$axios'].get(this['$common'].url + "relTypes")
+				.then(res => {
+					this.dicRels = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+				
+			this['$axios'].post(this['$common'].url + "equipTypes")
+				.then(res => {
+					const root = res.data;
+					this.dicEquips = root.data;
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+			this['$axios'].post(this['$common'].url + "diagramTypes")
+				.then(res => {
+					const root = res.data;
+					this.diagramTypes = root.data;
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+			
+			//test 自动初始化
+			// this.sysVal = ['SE', 'SETD'];
+			// this.sysChange();
+		}
+				
+		//在编辑器中打开模板文件
+		openEditor(data){
+			this.currentData = data;
+			this.selectionInfo = '~';
+			this.dynInfo = '';
+			
+			this.editor.show(this.currentData);
+			
+			if(!this.currentSys) //编辑对象为空
+				this.state = -1;
+			else {
+				this.onSelectionChange();
+				
+				const arr = [];
+				this.dicEquips.forEach(item => {
+					if(item.systemCode == this.currentSys)
+						arr.push(item);
+				});
+				this.equipTypes = arr;
+			}
+		}
+		
+		created(){
+			//window.addEventListener('resize', this.onWindowresize);
+		}
+		
+		onWindowresize(){
+			this.editorStyle.height = (window.innerHeight - this.$refs.header['offsetHeight'] - this.$refs.top['offsetHeight']) + 'px';
+		}
+		
+		//选中组件的事件触发
+		onSelectionChange(){
+			this.state = this.editor.getState();
+			
+			if(this.editor)
+				this.selectionInfo = this.editor.buildSelInfo();
+		}
+		
+		saveLegend(){
+			const params:any = {legend: this.editor.getSelComp(), sys:this.currentSys};
+			this.callAction('saveLegend', params);
+		}
+		
+		//添加图例
+		addLegend(){
+			this.$prompt('请输入图例编码', '提示', {
+						confirmButtonText: '确定',
+						cancelButtonText: '取消',
+						type: 'info',
+						inputValidator: val => {return val != null && this.checkId(val)}
+			       }).then(instance => {
+						const legend = this.editor.addLegend(instance['value']);
+						this.editor.setSelComp(legend);
+						
+						this.saveLegend();
+			       }).catch(() => {
+								
+			       });
+		}
+		
+		//检测id是否冲突
+		checkId(id:string):boolean {
+			for(const item of this.currentData) {
+				if(item.code == id)
+					return false;
+			}
+			return true;
+		}
+		
+		//删除图例
+		delLegend(){
+			const legend = this.editor.getSelComp();
+			const idx = this.currentData.indexOf(legend);
+			if(idx >= 0) {
+				this.currentData.splice(idx, 1);
+				this.editor.redraw();
+				
+				const params:any = {currentCompId: legend.id, sys:this.currentSys};
+				this.callAction('delLegend', params);
+			}
+		}
+		
+		//打开属性编辑界面
+		setProps(type: string) {
+			this.equipOptions = this.equipTypes;
+			if(type == 'legend') {
+				const sel = this.editor.getSelComp();
+				
+				const props:any = {code: sel.code, name: sel.name, remark: sel.remark};
+				props.width = sel.width;
+				props.height = sel.height;
+				props.equipTypes = sel.equipmentTypes;
+				props.diagramTypes = sel.diagramTypes;
+				
+				this.propsData = props;
+				this.dlgLegendVisible = true;
+			} else if(type == 'anchor') {
+				const sel = this.currentAnchor;
+				
+				const props:any = {code: sel.code, name: sel.name};
+				props.type = sel.type;
+				props.multiple = sel.multiple;
+				if(sel.acceptRelations){
+					props.acceptRelations = [];
+					for(const r of sel.acceptRelations) {
+						props.acceptRelations.push(r.split("/"));
+					}
+				}
+				props.toEquipmentTypes = sel.toEquipmentTypes;
+				
+				this.propsData = props;
+				this.dlgAnchorVisible = true;
+			}
+		}
+		
+		//设备容器下拉框搜索
+		queryEquipTypes(query){
+			if (query !== '') 
+				this.equipOptions = this.dicEquips.filter(data => this.filterMatch(data.name, query) || this.filterMatch(data.code, query));
+			else
+				this.equipOptions = this.equipTypes;
+		}
+		
+		filterMatch(str, filter){
+			return str.toLowerCase().indexOf(filter.toLowerCase()) !== -1;
+		}
+		
+		//修改容器属性		
+		modifyLegend(){
+			const legend = this.editor.getSelComp();
+
+			if(this.propsData.code != legend.code && !this.checkId(this.propsData.code)) {
+				this.$message({
+					type: 'warning',
+					message: '图例编码冲突!'
+				});
+				return;
+			}
+			
+			legend.code = this.propsData.code;
+			legend.name = this.propsData.name;
+			legend.remark = this.propsData.remark;
+			legend.width = this.propsData.width;
+			legend.height = this.propsData.height;
+			legend.equipmentTypes = this.propsData.equipTypes;
+			legend.diagramTypes = this.propsData.diagramTypes;
+			
+			this.dlgLegendVisible = false;
+			this.editor.redraw();
+			
+			this.saveLegend();
+		}
+		
+		//编辑数据匹配过滤条件
+		editFilter(){
+			const sel = this.editor.getSelComp();
+			this.dataFilter = sel.dataFilter != null ? sel.dataFilter : {}; //为null时赋一个新对象,触发子组件watch
+			
+			this.dlgFilterVisible = true;
+		}
+		
+		//修改数据匹配过滤条件
+		modifyFilter(){
+			var filter = (this.$refs.filterPanel as FilterPanel).buildFilter();
+			if(filter && filter.errMsg){
+				this.$message({
+					type: 'warning',
+					message: filter.errMsg
+				});
+				return;
+			}
+			
+			const legend = this.editor.getSelComp();
+			legend.dataFilter = filter;
+			
+			this.dlgFilterVisible = false;
+			this.editor.redraw();
+			
+			this.saveLegend();
+		}
+		
+		//编辑锚点信息
+		editAnchor(anchor: any){
+			this.currentAnchor = anchor;
+			this.setProps('anchor');
+		}
+		
+		//修改锚点信息
+		modifyAnchor(){
+			this.currentAnchor.name = this.propsData.name;
+			this.currentAnchor.type = this.propsData.type;
+			this.currentAnchor.multiple = this.propsData.multiple;
+
+			if(this.propsData.acceptRelations) {
+				const rels = [];
+				for(const arr of this.propsData.acceptRelations) {
+					rels.push(arr.join("/"));
+				}
+				this.currentAnchor.acceptRelations = rels;
+			}
+			this.currentAnchor.toEquipmentTypes = this.propsData.toEquipmentTypes;
+			
+			this.dlgAnchorVisible = false;
+			this.editor.redraw();
+			
+			this.saveLegend();
+		}
+		
+		//调用服务端编辑操作,返回新的模板对象,然后刷新
+		callAction(action, params) {
+			this['$axios'].post(this['$common'].url + 'legend/' + action, params)
+				.then(res => {
+					const root = res.data;
+					if(root.message == 'success') {
+						const legend = this.editor.getSelComp();
+						if(legend && !legend.id) {
+							legend.id = root.data.id;
+						}
+					}
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+		}
+
+		//开始设置锚点
+		beginSetAnchors(){
+			this.changeState(8);
+		}
+	
+		
+		//取消锚点设置操作操作
+		cancelSetAnchors(){
+			this.dynInfo = '';
+			
+			this.changeState(this.editor.getState());
+			this.editor.redraw();
+		}
+		
+		//完成锚点设置
+		finishSetAnchors(){
+			this.editor.storeAnchors();
+			
+			this.changeState(this.editor.getState());
+			this.editor.redraw();
+			
+			this.saveLegend();
+		}
+		
+		//切换编辑状态
+		changeState(state) {
+			this.state = state;
+			this.editor.setState(state);
+		}
+		
+	}
+</script>
+
+<style>
+	.top-panel {
+		padding: 10px 30px;
+	}
+	.custom-tree-node {
+		/* 	flex: 1;
+		display: flex; */
+		align-items: center;
+		justify-content: space-between;
+		font-size: 14px;
+		padding-right: 20px;
+	}
+	.editor-title {
+		font-size: 14px;
+		font-weight: bold;
+		padding:0 10px;
+	}
+	.info {
+		font-size: 12px;
+		padding:0 10px;
+	}
+	.el-dialog-div{
+		height: 60vh;
+		overflow: auto;
+	}
+</style>

+ 909 - 0
adm_comp/src/views/DiagramTemplate.vue

@@ -0,0 +1,909 @@
+<template>
+	<div>
+		<el-row>
+		  <!-- 模板树 -->
+		  <el-col :span="6" >
+			<div style="height: 100vh;overflow: scroll;">
+				<el-tree :data="templates" :props="{children: 'children', label: nodeLabel, value: 'id'}"
+					@node-click="openTemplate"
+					default-expand-all
+					:expand-on-click-node="false">
+					<span class="custom-tree-node" slot-scope="{ node, data }">
+						<span>{{ node.label}}</span>
+						<span style="padding-left: 10px;">
+							<el-button type="text" size="mini" icon="el-icon-plus" @click="() => newTemplate(node)" v-if="data.type == 'type'">
+								新建模板
+							</el-button>
+							<el-popconfirm title="确定删除模板?" v-if="data.diagramType" @onConfirm="() => delTemplate(node)">
+							  <el-button slot="reference" type="text" size="mini" icon="el-icon-remove">删除</el-button>
+							</el-popconfirm>
+						</span>
+					</span>
+				</el-tree>
+			</div>
+		  </el-col>
+		  
+		  <!-- 编辑区 -->
+		  <el-col :span="18" style="border-left: 1px solid #f4f4f4">
+			<!-- 工具条 -->
+			<div ref="header" style="padding: 2px;border-bottom: 1px solid #f4f4f4">
+				<span class="editor-title">{{currentTemplate.name}}</span>
+				
+				<el-button-group>
+					<el-button size="mini" @click="() => addCon()" :disabled="!(state == 1 && !isEquipCompSel())">添加容器</el-button>
+				</el-button-group>
+				<el-button-group>
+					<el-button size="mini" @click="() => moveCon(-1)" :disabled="state != 1">上移/左移</el-button>
+					<el-button size="mini" @click="() => moveCon(1)" :disabled="state != 1">下移/右移</el-button>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => setProps()" :disabled="!(state == 1 || state == 2)">属性设置</el-button>
+					<el-popconfirm title="确定删除选中对象?" @onConfirm="() => delCon()">
+						<el-button slot="reference" size="mini" :disabled="!(state == 1 || state == 2)">删除</el-button>
+					</el-popconfirm>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => editFilter()" :disabled="!isEquipCompSel()">设备过滤</el-button>
+					<el-button size="mini" @click="() => editPack()" :disabled="!isEquipCompSel() || state != 1">设备打包</el-button>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => editDynGroup()" :disabled="state != 1 || isEquipCompSel()">动态组</el-button>
+				</el-button-group>
+				&nbsp;
+				<el-button-group>
+					<el-button size="mini" @click="() => beginMainLine()" :disabled="state == -1 || state == 9">绘制干管/线</el-button>
+					<el-button size="mini" @click="() => nextLine()" :disabled="state != 9">下一条线</el-button>
+					<el-button size="mini" @click="() => cancelMainLine()" :disabled="state != 9">取消绘制</el-button>
+					<el-button size="mini" @click="() => finishMainLine()" :disabled="state != 9">完成绘制</el-button>
+				</el-button-group>
+				
+				<!--选中信息-->
+				<div class="info">
+					<span>{{selectionInfo}}</span>
+					<span style="margin-left: 30px;color: #a3a3a3;">{{dynInfo}}</span>
+				</div>
+			</div>
+			
+			 <!-- 编辑界面 -->
+			<div ref="editor" :style="editorStyle">
+				
+			</div>
+		  </el-col>
+		</el-row>
+
+		<!-- 编辑容器对话框 -->
+		<el-dialog title="容器属性" :visible.sync="dlgConVisible">
+			<div class="el-dialog-div">
+				<el-form :model="propsData" label-width="130px">
+					<el-form-item label="容器Id">
+						<el-input v-model="propsData.id" :disabled="propsData.root"></el-input>
+					</el-form-item>
+					<el-form-item label="容器名称">
+						<el-input v-model="propsData.name" :disabled="propsData.root"></el-input>
+					</el-form-item>
+					<el-form-item label="备注">
+						<el-input
+						  type="textarea"
+						  :rows="2"
+						  v-model="propsData.remark">
+						</el-input>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="布局方式">
+						<el-radio-group v-model="propsData.layout">
+							<el-radio :label="1">行式</el-radio>
+							<el-radio :label="2">列式</el-radio>
+						</el-radio-group>
+					</el-form-item>
+					<el-form-item label="边距">
+						<el-input-number v-model="propsData.margin" :min="2" :max="200"></el-input-number>
+					</el-form-item>
+					<el-form-item label="间距">
+						<el-input-number v-model="propsData.space" :min="2" :max="200"></el-input-number>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="预设高度">
+						<el-input-number v-model="propsData.minH" :min="100" :max="1000" :disabled="propsData.root"></el-input-number>
+					</el-form-item>
+					<el-form-item label="预设宽度">
+						<el-input-number v-model="propsData.minW" :min="100" :max="1000" :disabled="propsData.root"></el-input-number>
+					</el-form-item>
+					<el-form-item label="定位">
+						<el-checkbox v-model="propsData.absolute" :disabled="propsData.root">自由定位</el-checkbox>
+					</el-form-item>
+					
+					<br>
+					<el-form-item label="设备容器">
+						<el-select
+						  v-model="propsData.equipTypes"
+						  multiple 
+						  placeholder="设备类型,输入名称或代码搜索"
+						  style="width: 600px;"
+						  :disabled="propsData.root || propsData.hasChildren"
+						  filterable
+						  remote
+						  :remote-method="queryEquipTypes"
+						  >
+						  <el-option
+							v-for="item in equipOptions"
+							:key="item.id"
+							:label="item.name + ' - ' + item.code"
+							:value="item.code">
+						  </el-option>
+						</el-select>
+					</el-form-item>
+					
+					<el-form-item label="无设备时自动隐藏" v-if="propsData.equipTypes && propsData.equipTypes.length > 0">
+						<el-radio-group v-model="propsData.auto_hidden">
+							<el-radio :label="true">是</el-radio>
+							<el-radio :label="false">否</el-radio>
+						</el-radio-group>
+					</el-form-item>
+					
+				</el-form>
+			</div>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgConVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyCon'>确 定</el-button>
+			</div>
+		</el-dialog>
+		
+		<!-- 编辑干管对话框 -->
+		<el-dialog title="干管属性" :visible.sync="dlgLineVisible">
+			<el-form :model="propsData" label-width="200px">
+				<el-form-item label="Id">
+					<el-input v-model="propsData.id"></el-input>
+				</el-form-item>
+				<el-form-item label="名称">
+					<el-input v-model="propsData.name"></el-input>
+				</el-form-item>
+				<el-form-item label="备注">
+					<el-input
+					  type="textarea"
+					  :rows="2"
+					  v-model="propsData.remark">
+					</el-input>
+				</el-form-item>
+				<el-form-item label="类型">
+					<el-radio-group v-model="propsData.pipeType">
+						<el-radio :label="null">无</el-radio>
+						<el-radio label="supply">供</el-radio>
+						<el-radio label="return">回</el-radio>
+					</el-radio-group>
+				</el-form-item>
+				
+				<br>
+				<el-form-item label="应用模式">
+					<el-radio-group v-model="propsData.bindEquipment">
+						<el-radio :label="false">承载连接关系</el-radio>
+						<el-radio :label="true">绑定管道设备</el-radio>
+					</el-radio-group>
+				</el-form-item>
+				
+				<br>
+				<el-form-item label="可放置在干管上的设备">
+					<el-select
+					  v-model="propsData.equipTypes"
+					  multiple 
+					  placeholder="设备类型,输入名称或代码搜索"
+					  style="width: 600px;"
+					  :disabled="propsData.root"
+					  filterable
+					  remote
+					  :remote-method="queryEquipTypes"
+					  >
+					  <el-option
+						v-for="item in equipOptions"
+						:key="item.id"
+						:label="item.name + ' - ' + item.code"
+						:value="item.code">
+					  </el-option>
+					</el-select>
+				</el-form-item>
+				
+				<br>
+				<el-form-item label="干管承载的关系类型" v-if="!propsData.bindEquipment">
+					<el-cascader
+					  placeholder="搜索"
+					  :options="dicRels" :props="{expandTrigger: 'hover', multiple: true, label: 'name', value: 'code'}"
+					  v-model="propsData.passbyRels" filterable collapse-tags clearable 
+					  ref="relCascader" style="width: 400px;"></el-cascader>
+				</el-form-item>
+				<el-form-item label="直连干管的设备类型">
+					<el-select
+					  v-model="propsData.connectEquips"
+					  multiple 
+					  placeholder="设备类型,输入名称或代码搜索"
+					  style="width: 600px;"
+					  filterable
+					  remote
+					  :remote-method="queryEquipTypes"
+					  >
+					  <el-option
+						v-for="item in equipOptions"
+						:key="item.id"
+						:label="item.name + ' - ' + item.code"
+						:value="item.code">
+					  </el-option>
+					</el-select>
+				</el-form-item>
+				
+			</el-form>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgLineVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyMainPipe'>确 定</el-button>
+			</div>
+		</el-dialog>
+		
+		<!-- 设备过滤对话框 -->
+		<el-dialog title="设备过滤" :visible.sync="dlgFilterVisible">
+			<FilterPanel ref="filterPanel" :dataFilter="dataFilter"></FilterPanel>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgFilterVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyFilter'>确 定</el-button>
+			</div>
+		</el-dialog>
+		
+		<!-- 设备打包对话框 -->
+		<el-dialog title="设备打包" :visible.sync="dlgPackVisible">
+			<div>
+				<el-form :model="propsData" label-width="100px">
+					<el-form-item label="">
+						<el-checkbox v-model="propsData.enablePack">启用打包</el-checkbox>
+					</el-form-item>
+					<el-form-item label="打包选项">
+						<el-radio-group v-model="propsData.packByType" :disabled="!propsData.enablePack">
+							<el-radio :label="true">按设备类型打包</el-radio>
+							<el-radio :label="false">所有类型打包成一个</el-radio>
+						</el-radio-group>
+					</el-form-item>
+					<el-form-item label="打包图例">
+						<el-input
+						  v-model="propsData.legendId"
+						  clearable
+						  :disabled="!propsData.enablePack || propsData.packByType"
+						  >
+						</el-input>
+					</el-form-item>
+					<el-form-item label="打包名称">
+						<el-input
+						  v-model="propsData.packName"
+						  clearable
+						  :disabled="!propsData.enablePack || propsData.packByType"
+						  >
+						</el-input>
+					</el-form-item>
+				</el-form>
+			</div>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgPackVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyPack'>确 定</el-button>
+			</div>
+		</el-dialog>
+
+		<!-- 容器动态组对话框 -->
+		<el-dialog title="动态组" :visible.sync="dlgDynGroupVisible">
+			<div>
+				<el-form :model="propsData" label-width="100px">
+					<el-form-item label="">
+						<el-checkbox v-model="propsData.enableDynGroup">启用动态组</el-checkbox>
+					</el-form-item>
+					<el-form-item label="数据分组依据">
+						<el-select v-model="propsData.dynSource" :disabled="!propsData.enableDynGroup">
+							<el-option
+								v-for="item in dynSourceOptions"
+								:key="item.value"
+								:label="item.label"
+								:value="item.value">
+							</el-option>
+						</el-select>
+					</el-form-item>
+					<el-form-item label="分组标签位置">
+						<el-radio-group v-model="propsData.labelPosition" :disabled="!propsData.enableDynGroup">
+							<el-radio label="left">左</el-radio>
+							<el-radio label="bottom">下</el-radio>
+							<el-radio label="right">右</el-radio>
+							<el-radio label="top">上</el-radio>
+							<el-radio :label="null">无</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-form>
+			</div>
+			
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dlgDynGroupVisible = false">取 消</el-button>
+				<el-button type="primary" @click='modifyDynGroup'>确 定</el-button>
+			</div>
+		</el-dialog>
+		
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	import FilterPanel from '@/components/FilterPanel.vue';
+	import {TemplateEditor} from '@/lib/TemplateEditor';
+	import {Container} from '@/lib/DiagramModel';
+
+	@Component({components:{FilterPanel}})
+	export default class DiagramTemplate extends Vue {
+		editorStyle = {
+			width: "100%",
+			height: "",
+			"overflow": "scroll"
+		};
+		
+		templates = [];
+		currentTemplate:any = {};
+		
+		editor:TemplateEditor = null;
+		state = -1; //编辑状态(-1:关闭; 0:打开; 1:选中容器; 2:选中干管; 9:绘制干管)
+
+		dlgConVisible = false;
+		dlgLineVisible = false;
+		dlgFilterVisible = false;
+		dlgPackVisible = false;
+		dlgDynGroupVisible = false;
+		
+		propsData: any = {};
+		dataFilter: any = {};
+		selectionInfo = '~';
+		dynInfo = '';
+		
+		equipTypes:any[] = null; //编辑时,系统对应的设备类型
+		equipOptions:any[] = []; //属性设定时,下拉框中的设备类型列表(动态)
+		dicRels:any[] = [];
+		dicEquips:any[] = [];
+		dynSourceOptions:any[] = [];
+				
+		mounted() {
+			this.onWindowresize();
+			this.editor = new TemplateEditor(this.$refs.editor);
+			this.editor.setEditorPart(this);
+								
+			this['$axios'].get(this['$common'].url + "template/getTemplates")
+				.then(res => {
+					this.templates = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+			
+			this['$axios'].get(this['$common'].url + "relTypes")
+				.then(res => {
+					this.dicRels = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+				
+			this['$axios'].post(this['$common'].url + "equipTypes")
+				.then(res => {
+					const root = res.data;
+					this.dicEquips = root.data;
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+		}
+		
+		//系统图模板树的节点显示
+		nodeLabel(data, node) {
+			if(data.type) //文件夹
+				return data.name;
+			else if(data.diagramType)//模板
+				return '[模板] ' + data.name;
+		}
+		
+		//新建模板文件
+		newTemplate(node){
+			this.$prompt('请输入模板名称', '提示', {
+						confirmButtonText: '确定',
+						cancelButtonText: '取消',
+						type: 'info',
+						inputValidator: val => {return val != null}
+			       }).then(instance => {
+						const params:any = {name: instance['value'], type: node.data.id};
+						this['$axios'].post(this['$common'].url + "template/newTemplate", params)
+							.then(res => {
+								const root = res.data;
+								if(root.message == 'success') {
+									const newObj = root.data;
+									node.data.children.push(newObj);
+									
+									this.$message({
+										type: 'success',
+										message: '操作成功!'
+									});
+									
+									this.openTemplate(newObj);
+								}
+							})
+							.catch(err => {
+								console.log(err);
+							});	
+			       }).catch(() => {
+								
+			       });
+		}
+		
+		//删除模板
+		delTemplate(node){
+			const params:any = {id: node.data.id};
+			this['$axios'].post(this['$common'].url + "template/delTemplate", params)
+				.then(res => {
+					const root = res.data;
+					if(root.data == true) {
+						const parent = node.parent;
+						const children = parent.data.children || parent.data;
+						const index = children.findIndex(d => d.id === node.data.id);
+						children.splice(index, 1);
+						
+						this.$message({
+							type: 'success',
+							message: '操作成功!'
+						});
+						this.openTemplate({});
+					}
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+		}
+		
+		//在编辑器中打开模板文件
+		openTemplate(data){
+			if(data.type) //文件夹
+				return;
+				
+			if(this.currentTemplate != data) {
+				this.currentTemplate = data;
+				this.selectionInfo = '~';
+				this.dynInfo = '';
+				
+				this.editor.show(this.currentTemplate);
+				
+				if(!this.currentTemplate.id) //编辑对象为空
+					this.state = -1;
+				else {
+					this.onSelectionChange();
+					
+					const sys = this.currentTemplate.diagramType.substr(0, 4);
+					const arr = [];
+					this.dicEquips.forEach(item => {
+						if(item.systemCode == sys)
+							arr.push(item);
+					});
+					this.equipTypes = arr;
+				}
+			}
+		}
+		
+		created(){
+			//window.addEventListener('resize', this.onWindowresize);
+		}
+		
+		onWindowresize(){
+			this.editorStyle.height = (window.innerHeight - this.$refs.header['offsetHeight']) + 'px';
+		}
+		
+		//选中组件的事件触发
+		onSelectionChange(){
+			this.state = this.editor.getState();
+			
+			if(this.editor)
+				this.selectionInfo = this.editor.buildSelInfo();
+		}
+		
+		isEquipCompSel(){
+			if(this.state == 1 || this.state == 2) {
+				const sel = this.editor.getSelComp();
+				return sel.equipmentTypes != null && sel.equipmentTypes.length > 0;
+			}
+			return false;
+		}
+		
+		//添加容器
+		addCon(){
+			this.$prompt('请输入容器id', '提示', {
+						confirmButtonText: '确定',
+						cancelButtonText: '取消',
+						type: 'info',
+						inputValidator: val => {return val != null && this.checkId(val)}
+			       }).then(instance => {
+						const params:any = {newCompId: instance['value'], currentCompId:this.editor.getSelComp().id, templateId: this.currentTemplate.id};
+						this.propsData = {id: params.newCompId}; //方便后续选中操作
+						this.callAction('addCon', params);
+			       }).catch(() => {
+								
+			       });
+		}
+		
+		//检测id是否冲突
+		checkId(id:string):boolean {
+			return this.editor.getCompById(id) == null;
+		}
+		
+		//删除容器
+		delCon(){
+			const sel = this.editor.getSelComp();
+			var type;
+			if(sel.children) { //容器
+				if(sel.id == 'frame') {
+					this.$message({
+						type: 'info',
+						message: '不能删除根容器!'
+					});
+					return;
+				}
+				type = "container";
+			} else { //干管
+				type = "mainPipe";
+			}
+			
+			const params:any = {currentCompId: sel.id, templateId: this.currentTemplate.id};
+			this.callAction(type == 'container' ? 'delCon' : 'delLine', params);
+		}
+		
+		//移动容器
+		moveCon(offset) {
+			if(this.editor.getSelComp().parent == null)
+				return;
+
+			const params:any = {currentCompId:this.editor.getSelComp().id, templateId: this.currentTemplate.id, moveOffset: offset};
+			this.callAction('moveCon', params);
+		}
+
+		//打开属性编辑界面
+		setProps() {
+			this.equipOptions = this.equipTypes;
+			
+			const sel = this.editor.getSelComp();
+			
+			const props:any = {id0: sel.id, id: sel.id, name: sel.name, remark: sel.remark};
+			if(sel.children) { //容器
+				if(sel.id == 'frame') 
+					props.root = true;
+				if(sel.children.length > 0)
+					props.hasChildren = true;
+				
+				props.layout = sel.layout.layout;
+				props.margin = sel.layout.marginTop; //暂时取上方值
+				props.space = sel.layout.spaceH;
+				
+				props.minH = sel.position.minH;
+				props.minW = sel.position.minW;
+				props.absolute = sel.position.absolute;
+				
+				props.equipTypes = sel.equipmentTypes;
+				
+				if(sel.props) {
+					props.auto_hidden = sel.props['auto_hidden'];
+				}
+				
+				this.propsData = props;
+				this.dlgConVisible = true;
+			} else { //干管
+				props.pipeType = sel.pipeType;
+				props.equipTypes = sel.equipmentTypes;
+				props.bindEquipment = sel.bindEquipment;
+				if(sel.passbyRels){
+					props.passbyRels = [];
+					for(const r of sel.passbyRels) {
+						props.passbyRels.push(r.split("/"));
+					}
+				}
+				
+				props.connectEquips = sel.connectEquips;
+				
+				this.propsData = props;
+				this.dlgLineVisible = true;
+			}
+		}
+		
+		//设备容器下拉框搜索
+		queryEquipTypes(query){
+			if (query !== '') 
+				this.equipOptions = this.dicEquips.filter(data => this.filterMatch(data.name, query) || this.filterMatch(data.code, query));
+			else
+				this.equipOptions = this.equipTypes;
+		}
+		
+		filterMatch(str, filter){
+			return str.toLowerCase().indexOf(filter.toLowerCase()) !== -1;
+		}
+		
+		//修改容器属性		
+		modifyCon(){
+			if(this.propsData.id != this.propsData.id0 && !this.checkId(this.propsData.id)) {
+				this.$message({
+					type: 'warning',
+					message: 'id冲突!'
+				});
+				return;
+			}
+			
+			if(this.propsData.auto_hidden !== null){
+				this.propsData.props = {auto_hidden: this.propsData.auto_hidden};
+			}
+			
+			const params:any = {templatePropsData: this.propsData, currentCompId: this.propsData.id0, templateId: this.currentTemplate.id};
+			this.callAction('modifyCon', params);
+		}
+		
+		//修改干管属性
+		modifyMainPipe(){
+			if(this.propsData.id != this.propsData.id0 && !this.checkId(this.propsData.id)) {
+				this.$message({
+					type: 'warning',
+					message: 'id冲突!'
+				});
+				return;
+			}
+			
+			if(this.propsData.passbyRels) {
+				const rels = [];
+				for(const arr of this.propsData.passbyRels) {
+					rels.push(arr.join("/"));
+				}
+				this.propsData.passbyRels = rels;
+			}
+			const params:any = {templatePropsData: this.propsData, currentCompId: this.propsData.id0, templateId: this.currentTemplate.id};
+			this.callAction('modifyMainPipe', params);
+		}
+
+		//编辑设备过滤条件
+		editFilter(){
+			const sel = this.editor.getSelComp();
+			this.dataFilter = sel.dataFilter != null ? sel.dataFilter : {};//为null时赋一个新对象,触发子组件watch
+			
+			this.dlgFilterVisible = true;
+		}
+		
+		//修改设备过滤条件
+		modifyFilter(){
+			var filter = (this.$refs.filterPanel as FilterPanel).buildFilter();
+			if(filter && filter.errMsg){
+				this.$message({
+					type: 'warning',
+					message: filter.errMsg
+				});
+				return;
+			}
+			
+			const params:any = {currentCompId: this.editor.getSelComp().id, templateId: this.currentTemplate.id, dataFilter: filter};
+			this.callAction('modifyFilter', params);
+		}
+		
+		//编辑设备打包配置
+		editPack(){
+			const sel = this.editor.getSelComp();
+			
+			const props:any = {};
+			if(sel.equipPack) {
+				props.enablePack = true;
+				props.packByType = sel.equipPack.packByType;
+				props.legendId = sel.equipPack.legendId;
+				props.packName = sel.equipPack.packName;
+			} else {
+				props.enablePack = false;
+				props.packByType = true;
+			}
+			this.propsData = props;
+			
+			this.dlgPackVisible = true;
+		}
+		
+		//修改设备打包配置
+		modifyPack(){
+			var pack: any;
+			if(!this.propsData.enablePack)
+				pack = null;
+			else
+				pack = this.propsData;
+			const params:any = {currentCompId: this.editor.getSelComp().id, templateId: this.currentTemplate.id, equipPack: pack};
+			this.callAction('modifyPack', params);
+		}
+		
+		//编辑容器的动态组配置
+		editDynGroup(){
+			const sel = this.editor.getSelComp();
+			
+			if(sel.id == 'frame') {
+				this.$message({
+					type: 'info',
+					message: '根容器不支持动态组!'
+				});
+				return;
+			}
+			
+			const props:any = {};
+			if(sel.dynGroup) {
+				props.enableDynGroup = true;
+				props.dynSource = sel.dynGroup.dynSource;
+				props.labelPosition = sel.dynGroup.labelPosition;
+			} else {
+				props.enableDynGroup = false;
+			}
+			this.propsData = props;
+			
+			this.dynSourceOptions = [{value:'floor', label: '楼层'}, {value:'building', label: '建筑'}];
+			if(this.currentTemplate.mainPipes){
+				for(const mp of this.currentTemplate.mainPipes) {
+					if(mp.bindEquipment){
+						this.dynSourceOptions.push({value: 'mainPipe:' + mp.id, label: '干管:' + mp.name});
+					}
+				}
+			}
+			const equipBoxes: any[] = Container.getEquipBoxes(sel);
+			for(const box of equipBoxes) {
+				this.dynSourceOptions.push({value: 'containerElement:' + box.id, label: '容器中设备:' + (box.name != null ? box.name : box.id)});
+			}
+			
+			this.dlgDynGroupVisible = true;
+		}
+		
+		//修改容器的动态组配置
+		modifyDynGroup(){
+			if(!this.propsData.dynSource) {
+				this.$message({
+					type: 'warning',
+					message: '缺少必要的信息!'
+				});
+				return;
+			}
+			
+			var dynGroup: any;
+			if(!this.propsData.enableDynGroup)
+				dynGroup = null;
+			else
+				dynGroup = this.propsData;
+			const params:any = {currentCompId: this.editor.getSelComp().id, templateId: this.currentTemplate.id, dynGroup: dynGroup};
+			this.callAction('modifyDynGroup', params);
+		}
+		
+		//调用服务端编辑操作,返回新的模板对象,然后刷新
+		callAction(action, params) {
+			this['$axios'].post(this['$common'].url + 'template/' + action, params)
+				.then(res => {
+					const root = res.data;
+					if(root.message == 'success') {
+						//记录之前的选中对象id
+						var selId = this.propsData.id;
+						if(!selId) {
+							const sel:any = this.editor.getSelComp();	
+							if(sel)
+								selId = sel.id;
+						}
+						
+						const newObj = root.data;
+						if(newObj == null)
+							return;
+							
+						this.currentTemplate.frame = newObj.frame;
+						this.currentTemplate.mainPipes = newObj.mainPipes;
+						this.currentTemplate.scatteredContainers = newObj.scatteredContainers;
+						
+						// this.$message({
+						// 	type: 'success',
+						// 	message: '操作成功!'
+						// });
+						
+						if(this.dlgConVisible)
+							this.dlgConVisible = false;
+						if(this.dlgLineVisible)
+							this.dlgLineVisible = false;
+						if(this.dlgFilterVisible)
+							this.dlgFilterVisible = false;
+						if(this.dlgPackVisible)
+							this.dlgPackVisible = false;
+						if(this.dlgDynGroupVisible)
+							this.dlgDynGroupVisible = false;
+						this.propsData = {};
+						
+						this.editor.show(this.currentTemplate);
+						this.editor.setSelComp(this.editor.getCompById(selId));
+					}
+				})
+				.catch(err => {
+					console.log(err);
+				});	
+		}
+
+		//开始绘制干管
+		beginMainLine(){
+			this.changeState(9);
+		}
+		
+		//下一条线
+		nextLine() {
+			this.editor.currentTmpMainPipe = null;
+		}
+		
+		//取消干管绘制操作
+		cancelMainLine(){
+			this.dynInfo = '';
+			
+			this.changeState(this.editor.getState());
+			this.editor.redraw();
+		}
+		
+		//完成干管绘制
+		finishMainLine(){
+			this.dynInfo = '';
+			
+			for(var i = 0; i < this.editor.tmpMainPipe.length; i++) {
+				const line = this.editor.tmpMainPipe[i];
+				if(line.length < 2) {
+					this.editor.tmpMainPipe.splice(i, 1);
+					i--;
+				}
+			}
+			if(this.editor.tmpMainPipe.length == 0) {
+				this.cancelMainLine();
+				return;
+			}
+				
+			const lines:any[] = [];
+			this.editor.tmpMainPipe.forEach(item => {
+				const line:any[] = [];
+				lines.push(line);
+				
+				for(const p of item){
+					line.push({x:p.x - this.editor.margin, y:p.y - this.editor.margin})
+				}
+			});
+
+			const params:any = {lines: lines, templateId: this.currentTemplate.id};
+			this.callAction('addMainPipe', params);
+		}
+		
+		//切换编辑状态
+		changeState(state) {
+			this.state = state;
+			this.editor.setState(state);
+		}
+		
+	}
+</script>
+
+<style>
+	.panel {
+		padding: 30px;
+	}
+	.custom-tree-node {
+		/* 	flex: 1;
+		display: flex; */
+		align-items: center;
+		justify-content: space-between;
+		font-size: 14px;
+		padding-right: 20px;
+	}
+	.editor-title {
+		font-size: 14px;
+		font-weight: bold;
+		padding:0 10px;
+	}
+	.info {
+		font-size: 12px;
+		padding:0 10px;
+	}
+	.el-dialog-div{
+		height: 60vh;
+		overflow: auto;
+	}
+</style>

+ 44 - 0
adm_comp/src/views/DiagramView.vue

@@ -0,0 +1,44 @@
+<template>
+	<div ref="diagram_view" style="width: 2000px;height:2000px;">
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	//import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
+	import {Diagram} from '@/common/Diagram';
+
+	@Component({})
+	export default class DiagramView extends Vue {
+
+		url = "http://192.168.0.11:8084/"; //this['$common'].url
+
+		mounted() {			
+			var pData:string = this.$route.params.data;
+			if(!pData || pData == 'null')
+				pData = '';
+			var pTemplateId = this.$route.params.templateId;
+			if(!pTemplateId || pTemplateId == 'null')
+				pTemplateId = '';
+					
+			this['$axios'].get(this.url + "diagram?data=" + pData + "&templateId=" + pTemplateId)
+				.then(res => {
+					const view = this.$refs.diagram_view;
+					const diagram = new Diagram(view);
+					diagram.show(res.data, true);
+				})
+				.catch(err => {
+					//console.log(err);
+				});		
+		}
+
+	}
+</script>
+
+<style>
+</style>

+ 259 - 0
adm_comp/src/views/Jobs.vue

@@ -0,0 +1,259 @@
+<template>
+	<div>
+		<!-- <el-breadcrumb separator="/" class="stat_breadcrumb">
+			<el-breadcrumb-item>job</el-breadcrumb-item>
+			<el-breadcrumb-item>test</el-breadcrumb-item>
+		</el-breadcrumb> -->
+
+		<div class="custom-tree-container">
+			<div class="block">
+				<el-tree :data="jobDatas" :props="treeProps" node-key="path" default-expand-all
+					:expand-on-click-node="false">
+					<span class="custom-tree-node" slot-scope="{ node, data }">
+						<span :class="getNodeStyle(data)">{{ node.label}}</span>
+						<span style="padding-left: 10px;">
+							<el-button type="text" size="mini" icon="el-icon-edit" @click="() => edit(node)">状态</el-button>
+							<el-button type="text" size="mini" icon="el-icon-plus" @click="() => append(node)"
+								v-if="data.hasDynItems && data.state>0">任务项
+							</el-button>
+						</span>
+					</span>
+				</el-tree>
+			</div>
+		</div>
+
+		<el-dialog title="新建任务项" :visible.sync="dialogFormVisible">
+			<el-form :model="form" label-width="100px">
+				<el-form-item v-for="(item, index) in newItems" :label="'任务项' + (index + 1)" :key="item.key">
+					<el-input v-model="item.name" style="width: 300px;"></el-input>
+					<el-button @click.prevent="removeItem(item)">删除</el-button>
+				</el-form-item>
+				<el-form-item>
+					<el-button @click="addItem">添加任务项</el-button>
+				</el-form-item>
+			</el-form>
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="dialogFormVisible = false">取 消</el-button>
+				<el-button type="primary" @click="addNewItems">确 定</el-button>
+			</div>
+		</el-dialog>
+
+		<el-dialog title="修改状态" :visible.sync="stateDialogVisible">
+			<el-radio-group v-model="targetState">
+				<el-radio :label="-20">不可用</el-radio>
+				<el-radio :label="-10">取消</el-radio>
+				<el-radio :label="10">处理中</el-radio>
+				<el-radio :label="20">完成</el-radio>
+			</el-radio-group>
+			<div slot="footer" class="dialog-footer">
+				<el-button @click="stateDialogVisible = false">取 消</el-button>
+				<el-button type="primary" @click="changeState">确 定</el-button>
+			</div>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	//import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
+
+	@Component({})
+	export default class Jobs extends Vue {
+
+		jobDatas: any = [];
+		treeProps: any = {
+			label: "name",
+			children: "children"
+		};
+		
+		form: any = {};
+		dialogFormVisible = false;
+		newItems: any[] = [];
+		
+		stateDialogVisible = false;
+		targetState = 0;
+
+		instanceId = "test001";
+
+		mounted() {
+			const query = this.$route.query;
+			if (query.instanceId)
+				this.instanceId = query.instanceId as string;
+				
+			this.loadJobs();
+		}
+		
+		loadJobs(){
+			this['$axios'].get(this['$common'].url + "jobs?instanceId=" + this.instanceId)
+				.then(res => {
+					const root = res.data.root;
+					this.fixData(root);
+					this.jobDatas = [root];
+				})
+				.catch(err => {
+					//console.log(err);
+				});			
+		}
+
+		fixData(nodeData) {
+			if (nodeData['dynJobItems'])
+				nodeData['children'] = nodeData['dynJobItems'];
+			else if(nodeData.hasDynItems)
+				nodeData.children = [];
+				
+			if (nodeData['children']) {
+				for (const item of nodeData['children']) {
+					this.fixData(item);
+				}
+			}
+		}
+
+		currentNode: any = {};
+		append(node) {
+			this.currentNode = node;
+			this.newItems = [{
+				name: "",
+				key: Date.now()
+			}];
+			this.dialogFormVisible = true;
+		}
+		edit(node) {
+			this.currentNode = node;
+			this.targetState = node.data.state;
+			this.stateDialogVisible = true;
+		}
+		changeState(){
+			const path = this.getPath(this.currentNode);
+			const params = {
+				instanceId: this.instanceId,
+				path: path,
+				state: this.targetState
+			};
+
+			this['$axios'].post(this['$clmt'].url + "changeState", params)
+				.then(res => {
+					const rtn = res.data;
+					console.log(rtn);
+					//this.currentNode.data.state = parseInt(rtn);
+					this.loadJobs();
+				})
+				.catch(err => {
+					console.log(err);
+				})
+				.finally((fin) => {
+					this.stateDialogVisible = false;
+				});			
+		}
+
+		removeItem(item) {
+			const index = this.newItems.indexOf(item)
+			if (index !== -1) {
+				this.newItems.splice(index, 1)
+			}
+		}
+		addItem() {
+			this.newItems.push({
+				value: '',
+				key: Date.now()
+			});
+		}
+
+		addNewItems() {
+			const path = this.getPath(this.currentNode);
+			const params = {
+				instanceId: this.instanceId,
+				path: path,
+				newNames: []
+			};
+			for (const item of this.newItems) {
+				params.newNames.push(item.name);
+			}
+
+			this['$axios'].post(this['$clmt'].url + "addNewItems", params)
+				.then(res => {
+					const rtn = res.data;
+
+					const nodeData = this.currentNode.data;
+					if (!nodeData.dynJobItems)
+						nodeData.dynJobItems = [];
+					for (const item of rtn) {
+						nodeData.dynJobItems.push(item);
+					}
+					
+					this.fixData(nodeData);
+				})
+				.catch(err => {
+					console.log(err);
+				})
+				.finally((fin) => {
+					this.dialogFormVisible = false;
+				});
+		}
+		getPath(node) {
+			return node.data.path;
+			// let str = "";
+			// while (node && node.data && node.data.id) {
+			// 	if (str.length == 0)
+			// 		str = node.data.id;
+			// 	else
+			// 		str = node.data.id + "/" + str;
+			// 	node = node.parent;
+			// }
+			// return str;
+		}
+
+		remove(node, data) {
+			const parent = node.parent;
+			const children = parent.data.children || parent.data;
+			const index = children.findIndex(d => d.id === data.id);
+			children.splice(index, 1);
+		}
+
+		getNodeStyle(data) {
+			if (data.state == -20)
+				return 'disabled';
+			if (data.state == -10)
+				return 'canceled';
+			if (data.state == 10)
+				return 'processing';
+			if (data.state == 20)
+				return 'finished';
+			return '';
+		}
+
+	}
+</script>
+
+<style>
+	.custom-tree-container{
+		padding: 10px;
+	}
+	.custom-tree-node {
+		/* 	flex: 1;
+		display: flex; */
+		align-items: center;
+		justify-content: space-between;
+		font-size: 14px;
+		padding-right: 20px;
+	}
+
+	.finished {
+		color: #42B983;
+	}
+
+	.processing {
+		color: #5783FA;
+	}
+
+	.disabled {
+		color: #D7DAE2;
+	}
+	.canceled {
+		color: coral;
+	}
+</style>

+ 204 - 0
adm_comp/src/views/LegendIcon.vue

@@ -0,0 +1,204 @@
+<template>
+	<div>
+		<div class="top-panel" ref="top">
+			<span>选择系统-专业: </span>
+			<el-cascader v-model="sysVal" :options="majSys" @change="sysChange"
+				:props="{ expandTrigger: 'hover', value:'code', label:'name' }" style="width:400px">
+			</el-cascader>
+		</div>
+		
+		<div>
+			<!-- 工具条 -->
+			<div ref="header" style="padding: 2px;border-bottom: 1px solid #f4f4f4">
+				<span class="editor-title">{{currentSys}}</span>
+				
+				<el-button-group>
+					<el-button size="mini" @click="() => manageIcon()" :disabled="state != 1">管理图标</el-button>
+				</el-button-group>
+				
+				<!--选中信息-->
+				<div class="info">
+					<span>{{selectionInfo}}</span>
+					<span style="margin-left: 30px;color: #a3a3a3;">{{dynInfo}}</span>
+				</div>
+			</div>
+			
+			 <!-- 编辑界面 -->
+			<div ref="editor" :style="editorStyle">
+				
+			</div>
+		</div>
+
+		<el-dialog title="图例属性" :visible.sync="dlgIconVisible">
+			<div>
+				<el-form>
+					<el-upload
+					  class="avatar-uploader"
+					  action="https://jsonplaceholder.typicode.com/posts/"
+					  :show-file-list="false"
+					  :on-success="handleAvatarSuccess"
+					  :before-upload="beforeAvatarUpload"
+					  accept="svg">
+					  <img v-if="imageUrl" :src="imageUrl" class="avatar">
+					  <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+					</el-upload>
+				</el-form>
+			</div>
+		</el-dialog>			
+
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	import FilterPanel from '@/components/FilterPanel.vue';
+	import {LegendEditor} from '@/lib/LegendEditor';
+
+	@Component({components:{FilterPanel}})
+	export default class DiagramLegend extends Vue {
+		majSys = [];
+		sysVal = [];
+		currentSys = '';
+		currentData: any[] = [];
+		
+		editorStyle = {
+			width: "100%",
+			height: "",
+			"overflow": "scroll"
+		};
+				
+		editor:LegendEditor = null;
+		state = -1; //编辑状态(-1:关闭; 0:打开; 1:容器编辑中; 2:绘制干管)
+
+		dlgIconVisible = false;
+		
+		propsData: any = {};
+		selectionInfo = '~';
+		dynInfo = '';
+		
+		currentAnchor: any;
+		
+		sysChange(){
+			this.currentSys = this.sysVal[this.sysVal.length - 1];
+			
+			const params:any = {sys: this.currentSys};
+			this['$axios'].get(this['$common'].url + "legend/getLegends?sys=" + this.currentSys)
+				.then(res => {
+					this.currentData = res.data.data;
+					this.openEditor(this.currentData);
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+		}
+		
+		mounted() {
+			this.onWindowresize();
+			this.editor = new LegendEditor(this.$refs.editor);
+			this.editor.setEditorPart(this);
+								
+			this['$axios'].get(this['$common'].url + "majSys")
+				.then(res => {
+					this.majSys = res.data.data;
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+		}
+				
+		//在编辑器中打开模板文件
+		openEditor(data){
+			this.currentData = data;
+			this.selectionInfo = '~';
+			this.dynInfo = '';
+			
+			this.editor.show(this.currentData);
+			
+			if(!this.currentSys) //编辑对象为空
+				this.state = -1;
+			else
+				this.onSelectionChange();
+		}
+		
+		created(){
+			//window.addEventListener('resize', this.onWindowresize);
+		}
+		
+		onWindowresize(){
+			this.editorStyle.height = (window.innerHeight - this.$refs.header['offsetHeight'] - this.$refs.top['offsetHeight']) + 'px';
+		}
+		
+		//选中组件的事件触发
+		onSelectionChange(){
+			this.state = this.editor.getState();
+			
+			if(this.editor)
+				this.selectionInfo = this.editor.buildSelInfo();
+		}
+		
+		//不做操作
+		editAnchor(anchor: any){
+			this.currentAnchor = anchor;
+		}
+		
+		manageIcon(){
+			this.dlgIconVisible = true;
+		}
+		
+	}
+</script>
+
+<style>
+	.top-panel {
+		padding: 10px 30px;
+	}
+	.custom-tree-node {
+		/* 	flex: 1;
+		display: flex; */
+		align-items: center;
+		justify-content: space-between;
+		font-size: 14px;
+		padding-right: 20px;
+	}
+	.editor-title {
+		font-size: 14px;
+		font-weight: bold;
+		padding:0 10px;
+	}
+	.info {
+		font-size: 12px;
+		padding:0 10px;
+	}
+	.el-dialog-div{
+		height: 60vh;
+		overflow: auto;
+	}
+	  .avatar-uploader .el-upload {
+	    border: 1px dashed #d9d9d9;
+	    border-radius: 6px;
+	    cursor: pointer;
+	    position: relative;
+	    overflow: hidden;
+	  }
+	  .avatar-uploader .el-upload:hover {
+	    border-color: #409EFF;
+	  }
+	  .avatar-uploader-icon {
+	    font-size: 28px;
+	    color: #8c939d;
+	    width: 178px;
+	    height: 178px;
+	    line-height: 178px;
+	    text-align: center;
+	  }
+	  .avatar {
+	    width: 178px;
+	    height: 178px;
+	    display: block;
+	  }
+</style>

+ 117 - 0
adm_comp/src/views/Map.vue

@@ -0,0 +1,117 @@
+<template>
+	<div>
+		<div ref="header" style="padding: 2px;border-bottom: 1px solid #f4f4f4">
+			<el-radio-group v-model="size" size="mini" @change="scale">
+				<el-radio-button label="0">自适应</el-radio-button>
+				<el-radio-button label="1">100%</el-radio-button>
+				<el-radio-button label="0.5">50%</el-radio-button>
+				<el-radio-button label="0.3">30%</el-radio-button>
+				<el-radio-button label="0.1">10%</el-radio-button>
+				<el-radio-button label="0.06">6%</el-radio-button>
+				<el-radio-button label="0.04">4%</el-radio-button>
+				<el-radio-button label="0.02">2%</el-radio-button>
+			</el-radio-group>
+			
+			<el-button size="mini" @click="() => divide()" style="margin-left: 20px;">划分</el-button>
+			<el-button size="mini" @click="() => reset()">重置</el-button>
+		</div>
+		<div ref="map_view" :style="mapViewStyle">
+		</div>
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	//import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
+	import {Map} from '@/common/Map';
+
+	@Component({})
+	export default class MapView extends Vue {
+		
+		url = "http://192.168.0.11:8084/"; //this['$common'].url
+		
+		mapViewStyle = {
+			width: "100%",
+			height: "",
+			"overflow": "scroll"
+		};
+		
+		size = 0;
+		map:Map = null;
+		
+		created(){
+			//window.addEventListener('resize', this.onWindowresize);
+		}
+		
+		onWindowresize(){
+			this.mapViewStyle.height = (window.innerHeight - this.$refs.header['offsetHeight']) + 'px';
+		}
+		
+		scale(){
+			this.map.scaleTo(this.size);
+		}
+		
+		divide(){
+			this['$axios'].get(this.url + "divideMap")
+				.then(res => {
+					this.map.update(res.data);
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+		}
+		
+		reset(){
+			this['$axios'].get(this.url + "resetMap")
+				.then(res => {
+					this.map.update(res.data);
+				})
+				.catch(err => {
+					//console.log(err);
+				});
+		}
+		
+		mounted() {
+			this.onWindowresize();
+			this.map = new Map(this.$refs.map_view);
+			this.map.addClickEvent(this);
+			
+			// var pData:string = this.$route.params.data;
+			// if(!pData || pData == 'null')
+			// 	pData = '';
+			// var pTemplateId = this.$route.params.templateId;
+			// if(!pTemplateId || pTemplateId == 'null')
+			// 	pTemplateId = '';
+					
+			this['$axios'].get(this.url + "map")
+				.then(res => {
+					this.map.show(res.data);
+				})
+				.catch(err => {
+					//console.log(err);
+				});		
+		}
+		
+		onClick(x, y) {
+			this['$axios'].get(this.url + "getSpace?x=" + x + "&y=" + y)
+				.then(res => {
+					this.map.selectSpace(res.data);
+				})
+				.catch(err => {
+					//console.log(err);
+				});		
+		}
+
+	}
+</script>
+
+<style>
+	.map_view{
+		color: #D7DAE2;
+	}
+</style>

+ 60 - 0
adm_comp/src/views/Map0.vue

@@ -0,0 +1,60 @@
+<template>
+	<div ref="map_view" :style="mapViewStyle">
+	</div>
+</template>
+
+<script lang="ts">
+	import {
+		Component,
+		Prop,
+		Vue,
+		Watch
+	} from "vue-property-decorator";
+	//import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
+	import {Map} from '@/common/Map0';
+
+	@Component({})
+	export default class MapView extends Vue {
+		
+		mapViewStyle = {
+			width: "100%",
+			height: "",
+			"overflow": "scroll"
+		};
+		
+		created(){
+			//window.addEventListener('resize', this.resize);
+			this.resize();
+		}
+		
+		resize(){
+			this.mapViewStyle.height = window.innerHeight + 'px';
+		}
+		
+		mounted() {			
+			// var pData:string = this.$route.params.data;
+			// if(!pData || pData == 'null')
+			// 	pData = '';
+			// var pTemplateId = this.$route.params.templateId;
+			// if(!pTemplateId || pTemplateId == 'null')
+			// 	pTemplateId = '';
+					
+			this['$axios'].get(this['$common'].url + "map0")
+				.then(res => {
+					const view = this.$refs.map_view;
+					const map = new Map(view);
+					map.show(res.data);
+				})
+				.catch(err => {
+					//console.log(err);
+				});		
+		}
+
+	}
+</script>
+
+<style>
+	.map_view{
+		color: #D7DAE2;
+	}
+</style>

+ 40 - 0
adm_comp/tsconfig.json

@@ -0,0 +1,40 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "strict": false,
+    "jsx": "preserve",
+    "importHelpers": true,
+    "moduleResolution": "node",
+    "experimentalDecorators": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "types": [
+      "webpack-env"
+    ],
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "tests/**/*.ts",
+    "tests/**/*.tsx"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}

+ 243 - 0
adm_comp/vue.config.js

@@ -0,0 +1,243 @@
+const path = require('path');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const CompressionWebpackPlugin = require('compression-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+// const ThemeColorReplacer = require('webpack-theme-color-replacer');
+// const forElementUI = require('webpack-theme-color-replacer/forElementUI');
+const { chain, set, each } = require('lodash');
+const package = require('./package.json');
+const cdnDependencies = process.env.VUE_APP_CDN === 'on' ? require('./dependencies-cdn') : [];
+
+const isProduction = process.env.NODE_ENV === 'production';
+
+// 拼接路径
+const resolve = dir => path.join(__dirname, dir);
+
+// 版本 & 构建时间
+const version = package.version;
+const nowDate = new Date();
+const buildDate = `${nowDate.getFullYear()}-${nowDate.getMonth() +
+  1}-${nowDate.getDate()} ${nowDate.getHours()}:${nowDate.getMinutes()}`;
+
+// 增加环境变量
+process.env.VUE_APP_VERSION = version;
+process.env.VUE_APP_BUILD_TIME = buildDate;
+process.env.VUE_APP_BUILD_INFO = `版本: ${version}, 构建日期: ${buildDate}`;
+process.env.VUE_APP_TITLE = process.env.VUE_APP_TITLE || package.name;
+
+// 基础路径
+const publicPath = `/adm_comp`; //process.env.VUE_APP_PUBLIC_PATH || '/';
+const assetsDir = `./static`;
+const assetsPath = _path => `${assetsDir}/${_path}`;
+
+// 设置不参与构建的库
+const externals = {};
+cdnDependencies.forEach(pkg => {
+  externals[pkg.name] = pkg.library;
+});
+
+// 引入文件的 cdn 链接
+const cdn = {
+  css: cdnDependencies.map(e => e.css).filter(e => e),
+  js: cdnDependencies.map(e => e.js).filter(e => e)
+};
+
+// 多页配置, 默认未开启,如需要请参考 https://cli.vuejs.org/zh/config/#pages
+const pages = undefined;
+// const pages = {
+//   index: './src/main.js',
+//   subpage: './src/subpage.js'
+// }
+
+module.exports = {
+  publicPath,
+  assetsDir,
+  lintOnSave: true,
+  devServer: {
+    hot: false,
+    publicPath, // 和 publicPath 保持一致
+    disableHostCheck: !isProduction, // 关闭 host check,方便使用 ngrok 之类的内网转发工具
+    port: 4200,
+    proxy: {
+      '': {
+        // 目标 API 地址  需要代理的服务器
+        target: 'http://192.168.58.127:9012',
+        // 如果要代理 websocket
+        ws: true,
+        // 是否允许跨域
+        changeOrigin: true,
+        pathRewrite: {
+          '^/api': ''
+        }
+      }
+    }
+  },
+  css: {
+    extract: true,
+    loaderOptions: {
+      // 设置 scss 公用变量
+      sass: {
+        prependData: "@import './src/styles/var.scss';"
+      }
+    }
+  },
+  // pages,
+  configureWebpack: config => {
+    config.entry.app = ['./src/polyfills.ts', './src/main.ts'];
+    const configNew = {
+      plugins: [
+        // copy custom static assets
+        new CopyWebpackPlugin([
+          {
+            from: resolve(assetsDir),
+            to: assetsDir,
+            ignore: ['.*']
+          }
+        ])
+      ]
+    };
+    if (isProduction) {
+      configNew.externals = externals;
+      configNew.output = {
+        filename: assetsPath('js/[name].[contenthash:8].js'),
+        chunkFilename: assetsPath('js/[id].[contenthash:8].js')
+      };
+      // gzip
+      if (process.env.GZIP === 'on') {
+        configNew.plugins.push(
+          new CompressionWebpackPlugin({
+            filename: '[path].gz[query]',
+            test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),
+            threshold: 10240,
+            minRatio: 0.8,
+            deleteOriginalAssets: false
+          })
+        );
+      }
+    }
+    return configNew;
+  },
+  // 默认设置: https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-service/lib/config/base.js
+  chainWebpack: config => {
+    if (isProduction) {
+      config.plugin('extract-css').use(MiniCssExtractPlugin, [
+        {
+          filename: assetsPath('css/[name].[contenthash:8].css'),
+          chunkFilename: assetsPath('css/[id].[contenthash:8].css')
+        }
+      ]);
+
+      // 多页时优化代码分割
+      if (pages) {
+        config.optimization.splitChunks({
+          chunks: 'all',
+          // 大于30KB才单独分离成chunk
+          minSize: 30000,
+          name: true,
+          cacheGroups: {
+            default: {
+              priority: -20,
+              // minChunks: 2,
+              reuseExistingChunk: true
+            },
+            core: {
+              name: 'core',
+              test: /[\\/]src[\\/](core|shared)[\\/]/,
+              chunks: 'initial',
+              priority: -10,
+              reuseExistingChunk: true
+            },
+            vendors: {
+              name: 'vendors',
+              test: /[\\/]node_modules[\\/]/,
+              priority: 0,
+              chunks: 'initial' // 只打包初始时依赖的第三方
+            }
+          }
+        });
+      }
+    }
+
+    /**
+     * 添加 CDN 参数到 htmlWebpackPlugin 配置中
+     * 已适配多页
+     */
+    const htmlPluginNames = chain(pages)
+      .keys()
+      .map(page => 'html-' + page)
+      .value();
+    const targetHtmlPluginNames = htmlPluginNames.length ? htmlPluginNames : ['html'];
+    each(targetHtmlPluginNames, name => {
+      config.plugin(name).tap(options => {
+        set(options, '[0].cdn', isProduction ? cdn : []);
+        return options;
+      });
+    });
+
+    /**
+     * 删除懒加载模块的 prefetch preload, 降低带宽压力
+     * https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch
+     * https://cli.vuejs.org/zh/guide/html-and-static-assets.html#preload
+     * 而且预渲染时生成的 prefetch 标签是 modern 版本的,低版本浏览器是不需要的
+     */
+    config.plugins.delete('prefetch').delete('preload');
+    // 解决 cli3 热更新失效 https://github.com/vuejs/vue-cli/issues/1559
+    config.resolve.symlinks(true);
+    // config.plugin('theme-color-replacer').use(ThemeColorReplacer, [
+    //   {
+    //     fileName: `${assetsDir}/css/theme-colors.[contenthash:8].css`,
+    //     matchColors: [
+    //       ...forElementUI.getElementUISeries(process.env.VUE_APP_ELEMENT_COLOR) // Element-UI主色系列
+    //     ],
+    //     externalCssFiles: ['./node_modules/element-ui/lib/theme-chalk/index.css'], // optional, String or string array. Set external css files (such as cdn css) to extract colors.
+    //     changeSelector: forElementUI.changeSelector
+    //   }
+    // ]);
+    config
+      // 开发环境 sourcemap 不包含列信息
+      .when(!isProduction, config => config.devtool('cheap-source-map'));
+    // markdown
+    config.module
+      .rule('md')
+      .test(/\.md$/)
+      .use('text-loader')
+      .loader('text-loader')
+      .end();
+    // svg
+    const svgRule = config.module.rule('svg');
+    svgRule.uses.clear();
+    svgRule.include
+      .add(resolve('src/assets/svg/icons'))
+      .end()
+      .use('svg-sprite-loader')
+      .loader('svg-sprite-loader')
+      .options({
+        symbolId: 'tui-icon-[name]'
+      })
+      .end();
+    // image exclude
+    const imagesRule = config.module.rule('images');
+    imagesRule
+      .test(/\.(png|jpe?g|gif|webp|svg)(\?.*)?$/)
+      .exclude.add(resolve('src/assets/svg/icons'))
+      .end();
+
+    // 重新设置 alias
+    const paths = require('./tsconfig.json').compilerOptions.paths;
+    if (!isProduction && process.env.LOCAL_ALIAS) {
+      const localAlias = JSON.parse(process.env.LOCAL_ALIAS);
+      Object.assign(paths, localAlias);
+    }
+    Object.keys(paths).forEach(key => {
+      config.resolve.alias.set(key.replace('/*', ''), resolve(paths[key][0].replace('/*', '')));
+    });
+
+    // 分析工具
+    if (process.env.npm_config_report) {
+      config.plugin('webpack-bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin);
+    }
+  },
+  // 不输出 map 文件
+  productionSourceMap: false,
+  pluginOptions: {}
+};