9 changed files with 675 additions and 16 deletions
			
			
		| @ -0,0 +1,50 @@ | |||
| <template> | |||
|   <ElSelect class='FormSelect' v-bind="$attrs" v-on="$listeners"> | |||
|     <ElOption v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> | |||
|       <slot></slot> | |||
|     </ElOption> | |||
|   </ElSelect> | |||
| </template> | |||
| 
 | |||
| <script> | |||
| export default { | |||
|   name: 'FormSelect', | |||
|   props: { | |||
|     // options: { | |||
|     //   /** | |||
|     //    * { | |||
|     //    *   value: any; | |||
|     //    *   label: any; | |||
|     //    * }[] | |||
|     //    */ | |||
|     //   type: Array, | |||
|     //   default: () => [] | |||
|     // } | |||
|   }, | |||
|   data() { | |||
|     return { | |||
|       options: [{ | |||
|         value: '选项1', | |||
|         label: '黄金糕' | |||
|       }, { | |||
|         value: '选项2', | |||
|         label: '双皮奶' | |||
|       }, { | |||
|         value: '选项3', | |||
|         label: '蚵仔煎' | |||
|       }, { | |||
|         value: '选项4', | |||
|         label: '龙须面' | |||
|       }, { | |||
|         value: '选项5', | |||
|         label: '北京烤鸭' | |||
|       }], | |||
|       value: '' | |||
|     } | |||
|   } | |||
| } | |||
| </script> | |||
| 
 | |||
| <style lang='scss' scoped> | |||
| .FormSelect {} | |||
| </style> | |||
| @ -0,0 +1,47 @@ | |||
| <template> | |||
|   <ElPopover class='Popover' v-bind="getBind" v-on="$listeners"> | |||
|     <slot /> | |||
|     <template #reference> | |||
|       <slot name="reference" /> | |||
|     </template> | |||
|   </ElPopover> | |||
| </template> | |||
| 
 | |||
| <script> | |||
| export default { | |||
|   name: 'Popover', | |||
|   computed: { | |||
|     getBind() { | |||
|       return { | |||
|         trigger: 'hover', | |||
|         placement: "left", | |||
|         ...this.$attrs, | |||
|         popperClass: "Popover-Scope-Screen" | |||
|       } | |||
|     } | |||
|   } | |||
| } | |||
| </script> | |||
| 
 | |||
| <style lang='scss'> | |||
| div.el-popper.Popover-Scope-Screen { | |||
|   background: linear-gradient(180deg, #005C79 0%, #009BCC 100%); | |||
|   border: 0; | |||
|   color: #fff; | |||
|   font-size: 14px; | |||
|   font-family: PingFang SC, PingFang SC; | |||
|   font-weight: 400; | |||
|   color: #FFFFFF; | |||
| 
 | |||
|   div.popper__arrow { | |||
|     border-left-color: #00799f; | |||
| 
 | |||
|     &::after { | |||
|       border-left-color: #00799f; | |||
|     } | |||
|   } | |||
| } | |||
| </style> | |||
| <style lang='scss' scoped> | |||
| .Popover {} | |||
| </style> | |||
| @ -1,20 +1,123 @@ | |||
| <template> | |||
|   <Card class='CrowdnessIndicatorRankings' title="拥挤度指标排名情况"> | |||
|     CrowdnessIndicatorRankings | |||
|   <Card class='CrowdnessIndicatorRankings border' title="拥挤度指标排名情况"> | |||
|     <div class="map"> | |||
|       map | |||
|     </div> | |||
|     <div class="right border"> | |||
|       <div :class="['item', { active: active === item.key }]" v-for="item in operation" :key="item.key" | |||
|         @click="handleClick(item)"> | |||
|         <Popover :disabled="item.key !== 'weather'"> | |||
|           <div v-if="item.key === 'weather'"> | |||
|             天气:晴 能见度:优 风向:西南 风力:1级 | |||
|           </div> | |||
|           <div class="label" slot="reference" | |||
|             :style="{ backgroundImage: `url(${require(`./images/${item.key}${active === item.key ? '-active' : ''}.svg`)})` }" /> | |||
|         </Popover> | |||
|       </div> | |||
|     </div> | |||
|   </Card> | |||
| </template> | |||
| 
 | |||
| <script> | |||
| import Card from "./../../components/Card.vue" | |||
| import Card from "./../../components/Card.vue"; | |||
| 
 | |||
| import Popover from "@screen/components/Popover/index.vue" | |||
| 
 | |||
| export default { | |||
|   name: 'CrowdnessIndicatorRankings', | |||
|   components: { | |||
|     Card | |||
|     Card, | |||
|     Popover | |||
|   }, | |||
|   data() { | |||
|     return { | |||
|       active: "weather", | |||
|       operation: [ | |||
|         { | |||
|           key: 'weather', | |||
|           label: "天气" | |||
|         }, | |||
|         { | |||
|           key: 'camera', | |||
|           label: "摄像机" | |||
|         }, | |||
|         { | |||
|           key: 'bar', | |||
|           label: "柱状图" | |||
|         }, | |||
|         { | |||
|           key: 'level', | |||
|           label: "天气" | |||
|         }, | |||
|         { | |||
|           key: 'car', | |||
|           label: "天气" | |||
|         }, | |||
|         { | |||
|           key: 'pie', | |||
|           label: "天气" | |||
|         }, | |||
|       ] | |||
|     } | |||
|   }, | |||
|   methods: { | |||
|     handleClick(item) { | |||
|       this.active = item.key | |||
|     } | |||
|   } | |||
| } | |||
| </script> | |||
| 
 | |||
| <style lang='scss' scoped> | |||
| .CrowdnessIndicatorRankings {} | |||
| .border { | |||
|   border: 1px solid; | |||
|   border-image: linear-gradient(360deg, rgba(55, 231, 255, .42), rgba(55, 231, 255, 0)) 1 1; | |||
| } | |||
| 
 | |||
| .CrowdnessIndicatorRankings { | |||
|   ::v-deep { | |||
|     .content { | |||
|       display: flex; | |||
|       padding: 0; | |||
|     } | |||
|   } | |||
| 
 | |||
|   .map { | |||
|     flex: 1; | |||
|   } | |||
| 
 | |||
|   .right { | |||
|     right: 0px; | |||
|     height: 100%; | |||
|     top: 0; | |||
|     width: 34px; | |||
|     background: linear-gradient(180deg, rgba(25, 40, 52, .6) 0%, rgba(28, 50, 60, .6) 100%); | |||
|     border-right: 0; | |||
|     border-bottom: 0; | |||
| 
 | |||
|     display: flex; | |||
|     flex-direction: column; | |||
|     gap: 6px; | |||
| 
 | |||
|     .item { | |||
|       width: 100%; | |||
|       height: 34px; | |||
|       padding: 3px; | |||
|       cursor: pointer; | |||
| 
 | |||
|       div { | |||
|         width: 100%; | |||
|         height: 100%; | |||
|         transition: all .15s linear; | |||
|         background-size: 100% 100%; | |||
|         background-repeat: no-repeat; | |||
|         background-position: center; | |||
|       } | |||
|     } | |||
| 
 | |||
|     .active { | |||
|       background: linear-gradient(180deg, #005C79 0%, #009BCC 100%); | |||
|     } | |||
|   } | |||
| } | |||
| </style> | |||
|  | |||
| @ -1,20 +1,234 @@ | |||
| <template> | |||
|   <Card class='DisposalPlan' title="处置预案"> | |||
|     DisposalPlan | |||
|     <canvas ref="FlowCanvasRef" /> | |||
|   </Card> | |||
| </template> | |||
| 
 | |||
| <script> | |||
| import Card from "./../../components/Card.vue" | |||
| import Card from "./../../components/Card.vue"; | |||
| import { CanvasFlow } from "./utils"; | |||
| 
 | |||
| import { merge } from "lodash" | |||
| 
 | |||
| function getDefaultBlockOption() { | |||
|   return { | |||
|     width: 100, | |||
|     height: 30, | |||
|     radius: 9, | |||
|     linearGradient: "linear-gradient(180deg, #005C79 0%, #009BCC 100%)", | |||
|     text: { | |||
|       font: "PingFang SC", | |||
|       fontSize: 14, | |||
|       color: "#FFFFFF" | |||
|     }, | |||
|   } | |||
| } | |||
| 
 | |||
| function getDefaultLegendOption() { | |||
|   return { | |||
|     width: 9, | |||
|     height: 9, | |||
|     y: 0, | |||
|     text: { | |||
|       font: "PingFang SC", | |||
|       fontSize: 9, | |||
|       align: 'left', | |||
|       color: "#FFFFFF" | |||
|     }, | |||
|   } | |||
| } | |||
| 
 | |||
| export default { | |||
|   name: 'DisposalPlan', | |||
|   components: { | |||
|     Card | |||
|   } | |||
|   }, | |||
|   data() { | |||
|     return { | |||
|       // list: [] | |||
|     } | |||
|   }, | |||
|   mounted() { | |||
|     this.canvasFlow = new CanvasFlow(this.$refs.FlowCanvasRef); | |||
| 
 | |||
|     this.draw(); | |||
|   }, | |||
|   methods: { | |||
|     drawBlock({ x, y, width, height, backgroundColor, linearGradient, | |||
|       radius, | |||
|       text: { | |||
|         color, text, fontSize, fontWeight, fontFamily | |||
|       } = {} }) { | |||
| 
 | |||
|       if (linearGradient) this.canvasFlow.setLinearGradient(...this.canvasFlow.transformCssLinearGradient(x, y, width, height, linearGradient)) | |||
| 
 | |||
|       this.canvasFlow.drawRectangle({ x, y, width, height, backgroundColor, radius }); | |||
| 
 | |||
|       if (text) { | |||
|         this.canvasFlow.fillText(text, { x, y, color, fontSize, fontWeight, fontFamily }, { width, height }); | |||
|       } | |||
|     }, | |||
|     drawLegend({ x, y, width, height, backgroundColor, linearGradient, | |||
|       radius, | |||
|       text: { | |||
|         text, ...textOptions | |||
|       } = {} }) { | |||
| 
 | |||
|       if (linearGradient) this.canvasFlow.setLinearGradient(...this.canvasFlow.transformCssLinearGradient(x, y, width, height, linearGradient)) | |||
| 
 | |||
|       this.canvasFlow.drawRectangle({ x, y, width, height, backgroundColor, radius }); | |||
| 
 | |||
|       if (text) { | |||
|         this.canvasFlow.fillText(text, { ...textOptions, x: x + width + 3, y: y + height / 2, }, { height }); | |||
|       } | |||
|     }, | |||
|     drawLine({ x, y, width, height, backgroundColor, linearGradient, }) { | |||
| 
 | |||
|     }, | |||
|     draw() { | |||
|       const { clientWidth } = this.$refs.FlowCanvasRef; | |||
| 
 | |||
|       const defaultBlockOption = getDefaultBlockOption(); | |||
| 
 | |||
|       const halfWidth = defaultBlockOption.width / 2; | |||
| 
 | |||
|       this.canvasFlow.clear(); | |||
| 
 | |||
|       const legends = [ | |||
|         { | |||
|           type: 'drawLegend', | |||
|           options: merge(getDefaultLegendOption(), { | |||
|             x: 150, | |||
|             linearGradient: "linear-gradient(180deg, #004960 0%, #004B62 100%)", | |||
|             text: { | |||
|               text: "未完成" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawLegend', | |||
|           options: merge(getDefaultLegendOption(), { | |||
|             x: 210, | |||
|             linearGradient: "linear-gradient(90deg, #006121 0%, #488000 100%)", | |||
|             text: { | |||
|               text: "进行中" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawLegend', | |||
|           options: merge(getDefaultLegendOption(), { | |||
|             x: 270, | |||
|             linearGradient: "linear-gradient(180deg, #005C79 0%, #009BCC 100%)", | |||
|             text: { | |||
|               text: "已完成" | |||
|             } | |||
|           }) | |||
|         } | |||
|       ] | |||
| 
 | |||
|       const blockList = [ | |||
|         { | |||
|           type: 'drawBlock', | |||
|           key: "DisposalPlan", | |||
|           options: merge(getDefaultBlockOption(), { | |||
|             x: clientWidth / 2 - halfWidth, | |||
|             y: 24, | |||
|             text: { | |||
|               text: "处置预案" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawBlock', | |||
|           key: "AnalyzeConfirm", | |||
|           options: merge(getDefaultBlockOption(), { | |||
|             x: clientWidth / 2 - halfWidth, | |||
|             y: 75, | |||
|             text: { | |||
|               text: "分析确认" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawBlock', | |||
|           key: "InstructionsGiven", | |||
|           options: merge(getDefaultBlockOption(), { | |||
|             x: clientWidth / 7 - halfWidth, | |||
|             y: 123, | |||
|             linearGradient: "linear-gradient(90deg, #006121 0%, #488000 100%)", | |||
|             text: { | |||
|               text: "指令下达" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawBlock', | |||
|           key: "OnSiteConfirmation", | |||
|           options: merge(getDefaultBlockOption(), { | |||
|             x: clientWidth / 7 * 6 - halfWidth, | |||
|             y: 123, | |||
|             linearGradient: "linear-gradient(90deg, #006121 0%, #488000 100%)", | |||
|             text: { | |||
|               text: "现场确认" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawBlock', | |||
|           key: "OnSiteDisposal", | |||
|           options: merge(getDefaultBlockOption(), { | |||
|             x: clientWidth / 2 - halfWidth, | |||
|             y: 170, | |||
|             linearGradient: "linear-gradient(180deg, #004960 0%, #004B62 100%)", | |||
|             text: { | |||
|               text: "现场处置" | |||
|             } | |||
|           }) | |||
|         }, | |||
|         { | |||
|           type: 'drawBlock', | |||
|           key: "FollowUpProcessing", | |||
|           options: merge(getDefaultBlockOption(), { | |||
|             x: clientWidth / 2 - halfWidth, | |||
|             y: 225, | |||
|             linearGradient: "linear-gradient(180deg, #004960 0%, #004B62 100%)", | |||
|             text: { | |||
|               text: "后续处理" | |||
|             } | |||
|           }) | |||
|         }, | |||
|       ]; | |||
| 
 | |||
|       const linePoints = [ | |||
|         { | |||
|           x: clientWidth / 7 - halfWidth, | |||
|           y: 1 | |||
|         } | |||
|       ]; | |||
| 
 | |||
|       [...blockList, ...legends].forEach(item => { | |||
|         this[item.type]?.(item.options) | |||
|       }) | |||
|     } | |||
|   }, | |||
| 
 | |||
| } | |||
| </script> | |||
| 
 | |||
| <style lang='scss' scoped> | |||
| .DisposalPlan {} | |||
| .DisposalPlan { | |||
|   ::v-deep { | |||
|     .content { | |||
|       display: flex; | |||
|       flex-direction: column; | |||
|     } | |||
|   } | |||
| 
 | |||
|   canvas { | |||
|     flex: 1; | |||
|     width: 100%; | |||
|     height: 100%; | |||
|   } | |||
| } | |||
| </style> | |||
|  | |||
| @ -0,0 +1,202 @@ | |||
| export function getQuadrant(angle) { | |||
|   angle %= 360.0; | |||
|   if (angle < 0) angle += 360.0; | |||
|   var quadrant = Math.floor(angle / 90) + 1; | |||
|   return quadrant; | |||
| } | |||
| 
 | |||
| export function calcPoint(x0, y0, width, height, angle) { | |||
|   const parseAngle = parseFloat(angle); | |||
| 
 | |||
|   angle = (Math.PI / 180) * parseFloat(parseAngle); | |||
| 
 | |||
|   x0 += width / 2; | |||
|   y0 += height / 2; | |||
| 
 | |||
|   const hypotenuse = width / 2 > height / 2 ? width / 2 : height / 2; | |||
| 
 | |||
|   // 对边
 | |||
|   let oppositeEdge = Math.abs(hypotenuse * Math.sin(angle)); | |||
| 
 | |||
|   // 邻边
 | |||
|   let adjacentEdge = Math.abs(hypotenuse * Math.cos(angle)); | |||
| 
 | |||
|   let point0 = [], | |||
|     point1 = []; | |||
| 
 | |||
|   switch (parseFloat(parseAngle) % 360.0) { | |||
|     case 180: | |||
|       oppositeEdge = adjacentEdge; | |||
|       adjacentEdge = 0; | |||
|       break; | |||
|     case 90: | |||
|     case 270: | |||
|       adjacentEdge = oppositeEdge; | |||
|       oppositeEdge = 0; | |||
|       break; | |||
|   } | |||
| 
 | |||
|   switch (getQuadrant(parseAngle)) { | |||
|     case 1: | |||
|       point0 = [x0 - adjacentEdge, y0 + oppositeEdge]; | |||
|       point1 = [x0 + adjacentEdge, y0 - oppositeEdge]; | |||
|       break; | |||
|     case 2: | |||
|       point0 = [x0 - adjacentEdge, y0 - oppositeEdge]; | |||
|       point1 = [x0 + adjacentEdge, y0 + oppositeEdge]; | |||
|       break; | |||
|     case 3: | |||
|       point0 = [x0 + adjacentEdge, y0 - oppositeEdge]; | |||
|       point1 = [x0 - adjacentEdge, y0 + oppositeEdge]; | |||
|       break; | |||
|     case 4: | |||
|       point0 = [x0 + adjacentEdge, y0 + oppositeEdge]; | |||
|       point1 = [x0 - adjacentEdge, y0 - oppositeEdge]; | |||
|       break; | |||
|   } | |||
| 
 | |||
|   return angle > 0 ? [point0, point1] : [point1, point0]; | |||
| } | |||
| 
 | |||
| export class CanvasFlow { | |||
|   canvas = null; | |||
|   context = null; | |||
|   zoom = 3; | |||
|   constructor(canvas) { | |||
|     this.canvas = canvas; | |||
|     this.context = canvas.getContext("2d"); | |||
| 
 | |||
|     this.canvas.width = canvas.clientWidth * this.zoom; | |||
|     this.canvas.height = canvas.clientHeight * this.zoom; | |||
|   } | |||
| 
 | |||
|   drawRectangle({ | |||
|     x, | |||
|     y, | |||
|     width, | |||
|     height, | |||
|     borderWidth, | |||
|     borderColor, | |||
|     backgroundColor, | |||
|     radius, | |||
|   }) { | |||
|     if (backgroundColor) this.context.fillStyle = backgroundColor; | |||
| 
 | |||
|     x *= this.zoom; | |||
|     y *= this.zoom; | |||
|     width *= this.zoom; | |||
|     height *= this.zoom; | |||
| 
 | |||
|     if (radius) { | |||
|       this.context.beginPath(); | |||
|       this.context.roundRect(x, y, width, height, radius); | |||
|       this.context.fill(); | |||
|     } else { | |||
|       this.context.fillRect(x, y, width, height); | |||
|     } | |||
| 
 | |||
|     if (typeof borderWidth === "number") { | |||
|       borderWidth *= this.zoom; | |||
| 
 | |||
|       this.context.lineWidth = borderWidth; | |||
|       this.context.strokeStyle = borderColor; | |||
| 
 | |||
|       this.context.stroke(); | |||
|     } | |||
|   } | |||
| 
 | |||
|   transformCssLinearGradient(x, y, width, height, linearGradient) { | |||
|     const [_, deg, ...stopStrings] = linearGradient.match(/[^,()]+/g); | |||
| 
 | |||
|     const stops = stopStrings.map((stop) => { | |||
|       const [color, offset] = stop.trim().split(" "); | |||
| 
 | |||
|       return { | |||
|         color, | |||
|         offset: parseFloat(offset) * 0.01, | |||
|       }; | |||
|     }); | |||
| 
 | |||
|     const [point0, point1] = calcPoint(x, y, width, height, deg); | |||
| 
 | |||
|     return [ | |||
|       { | |||
|         x0: point0[0], | |||
|         y0: point0[1], | |||
|         x1: point1[0], | |||
|         y1: point1[1], | |||
|       }, | |||
|       stops, | |||
|     ]; | |||
|   } | |||
| 
 | |||
|   setLinearGradient({ x0, y0, x1, y1 }, stops) { | |||
|     x0 *= this.zoom; | |||
|     x1 *= this.zoom; | |||
|     y0 *= this.zoom; | |||
|     y1 *= this.zoom; | |||
| 
 | |||
|     const gradient = this.context.createLinearGradient(x0, y0, x1, y1); | |||
| 
 | |||
|     stops.forEach(({ offset, color }) => { | |||
|       gradient.addColorStop(offset, color); | |||
|     }); | |||
| 
 | |||
|     this.context.fillStyle = gradient; | |||
|   } | |||
| 
 | |||
|   drawLine({ startX, endX, color }, vertices) { | |||
|     startX *= this.zoom; | |||
|     endX *= this.zoom; | |||
| 
 | |||
|     ctx.beginPath(); | |||
| 
 | |||
|     ctx.moveTo(startX, endX); | |||
| 
 | |||
|     vertices.forEach(({ x, y }) => { | |||
|       ctx.lineTo(x * this.zoom, y * this.zoom); | |||
|     }); | |||
| 
 | |||
|     this.context.strokeStyle = color; | |||
| 
 | |||
|     ctx.stroke(); | |||
|   } | |||
| 
 | |||
|   fillText( | |||
|     text, | |||
|     { | |||
|       x, | |||
|       y, | |||
|       fontSize = 15, | |||
|       color, | |||
|       align = "center", | |||
|       fontWeight = "normal", | |||
|       fontFamily = "微软雅黑", | |||
|     } = {}, | |||
|     { width: rectWidth = 0, height: rectHeight = 0 } = {} | |||
|   ) { | |||
|     this.context.font = `${fontWeight} ${fontSize * this.zoom}px ${fontFamily}`; | |||
| 
 | |||
|     this.context.textBaseline = "middle"; | |||
| 
 | |||
|     const textWidth = this.context.measureText(text).width; | |||
| 
 | |||
|     x *= this.zoom; | |||
| 
 | |||
|     switch (align) { | |||
|       case "center": | |||
|         x += (rectWidth * this.zoom - textWidth) / 2; | |||
| 
 | |||
|         y += rectHeight / 2; | |||
|         break; | |||
|     } | |||
| 
 | |||
|     this.context.fillStyle = color; | |||
| 
 | |||
|     this.context.fillText(text, x, y * this.zoom); | |||
|   } | |||
| 
 | |||
|   clear() { | |||
|     this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); | |||
|   } | |||
| } | |||
					Loading…
					
					
				
		Reference in new issue