|
@@ -0,0 +1,283 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="box">
|
|
|
|
|
+ <!-- preserveAspectRatio=none 拉伸 -->
|
|
|
|
|
+ <svg
|
|
|
|
|
+ :viewBox="`0 0 ${svgConfig.width} ${svgConfig.height}`"
|
|
|
|
|
+ preserveAspectRatio="none"
|
|
|
|
|
+ version="1.1"
|
|
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
+ xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 图片描边 -->
|
|
|
|
|
+ <defs>
|
|
|
|
|
+ <!-- 定义描边滤镜 -->
|
|
|
|
|
+ <filter
|
|
|
|
|
+ id="outline"
|
|
|
|
|
+ x="-10%"
|
|
|
|
|
+ y="-10%"
|
|
|
|
|
+ width="120%"
|
|
|
|
|
+ height="120%"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 扩张图片边缘 -->
|
|
|
|
|
+ <feMorphology
|
|
|
|
|
+ operator="dilate"
|
|
|
|
|
+ radius="3"
|
|
|
|
|
+ in="SourceAlpha"
|
|
|
|
|
+ result="thicker"
|
|
|
|
|
+ />
|
|
|
|
|
+ <!-- 描边颜色 -->
|
|
|
|
|
+ <feFlood
|
|
|
|
|
+ flood-color="#ff0000"
|
|
|
|
|
+ result="color"
|
|
|
|
|
+ />
|
|
|
|
|
+ <!-- 合并描边和原图 -->
|
|
|
|
|
+ <feComposite
|
|
|
|
|
+ in="color"
|
|
|
|
|
+ in2="thicker"
|
|
|
|
|
+ operator="in"
|
|
|
|
|
+ result="outline"
|
|
|
|
|
+ />
|
|
|
|
|
+ <feComposite
|
|
|
|
|
+ in="SourceGraphic"
|
|
|
|
|
+ in2="outline"
|
|
|
|
|
+ operator="over"
|
|
|
|
|
+ />
|
|
|
|
|
+ </filter>
|
|
|
|
|
+ </defs>
|
|
|
|
|
+ <!-- 底色 -->
|
|
|
|
|
+ <image
|
|
|
|
|
+ v-for="(item, index) in svgConfig.tunnel"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ :href="item.img"
|
|
|
|
|
+ :x="item.x"
|
|
|
|
|
+ :y="item.y"
|
|
|
|
|
+ :width="item.w"
|
|
|
|
|
+ :height="item.h"
|
|
|
|
|
+ preserveAspectRatio="none"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 运动对象 -->
|
|
|
|
|
+ <g
|
|
|
|
|
+ v-for="(item, index) in list"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 存运动路径+动画 -->
|
|
|
|
|
+ <g :id="item.pathId" />
|
|
|
|
|
+ <!-- 对象 -->
|
|
|
|
|
+ <image
|
|
|
|
|
+ :id="item.imgId"
|
|
|
|
|
+ :href="item.img"
|
|
|
|
|
+ :x="item.x"
|
|
|
|
|
+ :y="item.y"
|
|
|
|
|
+ :width="item.w"
|
|
|
|
|
+ :height="item.h"
|
|
|
|
|
+ :transform="item.transform"
|
|
|
|
|
+ preserveAspectRatio="none"
|
|
|
|
|
+ :filter="props.dataCheck === index ? 'url(#outline)':''"
|
|
|
|
|
+ />
|
|
|
|
|
+ </g>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ <!-- 图例 -->
|
|
|
|
|
+ <div class="box-leng">
|
|
|
|
|
+ <template
|
|
|
|
|
+ v-for="(item, index) in svgConfig.leng"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="box-leng-item"
|
|
|
|
|
+ :style="`left:${item.x}`"
|
|
|
|
|
+ >
|
|
|
|
|
+ <p>{{ item.name }}</p>
|
|
|
|
|
+ <p>{{ item.stake }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <script setup lang='ts'>
|
|
|
|
|
+ import {
|
|
|
|
|
+ onMounted, reactive, ref, watch
|
|
|
|
|
+ } from 'vue'
|
|
|
|
|
+ import { mileage2string } from '@/utils/JsFn'
|
|
|
|
|
+ import img1 from './img/1.png'
|
|
|
|
|
+ import img2 from './img/2.png'
|
|
|
|
|
+
|
|
|
|
|
+ const props = withDefaults(defineProps<{
|
|
|
|
|
+ data?: { img: string, postions:any[] }[], // 原始数据
|
|
|
|
|
+ dataCheck?:number, // 对象选中
|
|
|
|
|
+ startAnimate?:boolean, // 显示动画
|
|
|
|
|
+ menuItem?:{isIn:boolean, startStake:number, endStake:number, currentStake:number, length:number, stakeType:string},
|
|
|
|
|
+ }>(), {
|
|
|
|
|
+ menuItem: undefined,
|
|
|
|
|
+ data: undefined,
|
|
|
|
|
+ startAnimate: false,
|
|
|
|
|
+ dataCheck: -1
|
|
|
|
|
+ })
|
|
|
|
|
+ const emit = defineEmits<{(evt: 'update:startAnimate', value: boolean): void }>()
|
|
|
|
|
+ // svg 配置
|
|
|
|
|
+ const svgConfig = reactive({
|
|
|
|
|
+ width: 600,
|
|
|
|
|
+ height: 400,
|
|
|
|
|
+ tunnel: [],
|
|
|
|
|
+ leng: []
|
|
|
|
|
+ } as {[key:string]:any})
|
|
|
|
|
+ const list = ref([] as { [key: string]: any }[])
|
|
|
|
|
+ const timer = ref()
|
|
|
|
|
+
|
|
|
|
|
+ // 计算path 路径
|
|
|
|
|
+ function countPath(path: number[]) {
|
|
|
|
|
+ let str = 'M'
|
|
|
|
|
+ const y = Math.floor(Math.random() * (svgConfig.height / 2)) + (svgConfig.height / 4)
|
|
|
|
|
+ for (let k = 0; k < path.length; k++) {
|
|
|
|
|
+ const el = path[k]
|
|
|
|
|
+ str += ` ${el},${y}`
|
|
|
|
|
+ }
|
|
|
|
|
+ return str
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新数据
|
|
|
|
|
+ function setData(data: any[]|undefined) {
|
|
|
|
|
+ if (!data || !data.length || !svgConfig.tunnel.length) {
|
|
|
|
|
+ list.value = []
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const scale = props.menuItem!.length / 1358 // 1358 dom宽度,比例
|
|
|
|
|
+ list.value = data.map((el, index) => {
|
|
|
|
|
+ const x = props.startAnimate ? -el.w / 2 : Math.floor(el.postions[el.postions.length - 1].mileage - props.menuItem!.startStake)
|
|
|
|
|
+ const y = props.startAnimate ? el.h ? -el.h : -el.w / scale : Math.floor(Math.random() * (svgConfig.height / 2)) + (svgConfig.height / 4)
|
|
|
|
|
+ const transform = el.isDevice && !props.menuItem!.isIn ? `translate(${x + (el.w / 2)},${y}) scale(-1, 1) translate(${-(x + (el.w / 2))},${-y})` : ''
|
|
|
|
|
+ const obj = {
|
|
|
|
|
+ ...el,
|
|
|
|
|
+ path: countPath(el.postions.map((es: { mileage: number; }) => Math.floor((es.mileage - props.menuItem!.startStake) * 10) / 10)),
|
|
|
|
|
+ pathId: `PATH${index}`,
|
|
|
|
|
+ imgId: `IMG${index}`,
|
|
|
|
|
+ duration: 8,
|
|
|
|
|
+ w: el.w,
|
|
|
|
|
+ h: el.h ? el.h : el.w / scale,
|
|
|
|
|
+ x,
|
|
|
|
|
+ y, // 随机Y
|
|
|
|
|
+ transform,
|
|
|
|
|
+ callBack() {
|
|
|
|
|
+ const g = document.getElementById(this.pathId)
|
|
|
|
|
+ // 创建path及动画
|
|
|
|
|
+ const pathChild = g?.childNodes
|
|
|
|
|
+ if (pathChild!.length) g?.replaceChildren()
|
|
|
|
|
+ // 移动目标
|
|
|
|
|
+ const img = document.getElementById(this.imgId)
|
|
|
|
|
+ const imgChild = img?.childNodes
|
|
|
|
|
+ if (imgChild?.length) img?.replaceChildren()
|
|
|
|
|
+ if (props.startAnimate) {
|
|
|
|
|
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
|
|
|
+ path.setAttribute('d', this.path)
|
|
|
|
|
+ path.setAttribute('fill', 'none')
|
|
|
|
|
+ path.setAttribute('stroke', '#3498db')
|
|
|
|
|
+ path.setAttribute('stroke-width', '1')
|
|
|
|
|
+ path.setAttribute('stroke-dashoffset', '0')
|
|
|
|
|
+ const pathLength = Math.ceil(path?.getTotalLength())
|
|
|
|
|
+ path.setAttribute('stroke-dasharray', `0, ${pathLength}`)
|
|
|
|
|
+ const pathAnimate = document.createElementNS('http://www.w3.org/2000/svg', 'animate')
|
|
|
|
|
+ pathAnimate.setAttribute('attributeName', 'stroke-dasharray')
|
|
|
|
|
+ pathAnimate.setAttribute('begin', '0s')
|
|
|
|
|
+ pathAnimate.setAttribute('dur', `${this.duration}s`)
|
|
|
|
|
+ pathAnimate.setAttribute('values', `0, ${pathLength}; ${pathLength}, 0`)
|
|
|
|
|
+ pathAnimate.setAttribute('calcMode', 'linear')
|
|
|
|
|
+ pathAnimate.setAttribute('fill', 'freeze')
|
|
|
|
|
+ path.appendChild(pathAnimate)
|
|
|
|
|
+ g?.appendChild(path)
|
|
|
|
|
+
|
|
|
|
|
+ // 给移动目标创建动画
|
|
|
|
|
+ const animateMotion = document.createElementNS('http://www.w3.org/2000/svg', 'animateMotion')
|
|
|
|
|
+ animateMotion.setAttribute('dur', `${this.duration}s`)
|
|
|
|
|
+ animateMotion.setAttribute('fill', 'freeze')
|
|
|
|
|
+ animateMotion.setAttribute('path', this.path)
|
|
|
|
|
+ img?.appendChild(animateMotion)
|
|
|
|
|
+ animateMotion.beginElement()
|
|
|
|
|
+ pathAnimate.beginElement()
|
|
|
|
|
+ emit('update:startAnimate', false)
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ path.style.display = 'none'
|
|
|
|
|
+ }, this.duration * 1000)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return obj
|
|
|
|
|
+ })
|
|
|
|
|
+ console.log(888, svgConfig, list.value)
|
|
|
|
|
+ if (timer.value) clearTimeout(timer.value)
|
|
|
|
|
+ timer.value = setTimeout(() => {
|
|
|
|
|
+ for (let k = 0; k < list.value.length; k++) {
|
|
|
|
|
+ const el = list.value[k]
|
|
|
|
|
+ el.callBack()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 200)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // init
|
|
|
|
|
+ function initSVG() {
|
|
|
|
|
+ if (!props.menuItem || !props.menuItem.currentStake) {
|
|
|
|
|
+ svgConfig.tunnel = []
|
|
|
|
|
+ svgConfig.leng = []
|
|
|
|
|
+ window.$notification.warning({ content: `隧道数据配置错误${JSON.stringify(props.menuItem)}`, duration: 5000 })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ svgConfig.width = props.menuItem!.length
|
|
|
|
|
+ svgConfig.tunnel = [
|
|
|
|
|
+ {
|
|
|
|
|
+ x: 0, y: 0, w: props.menuItem!.currentStake - props.menuItem!.startStake, h: 400, img: props.menuItem!.isIn ? img2 : img1
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ x: props.menuItem!.currentStake - props.menuItem!.startStake, y: 0, w: props.menuItem!.length - (props.menuItem!.currentStake - props.menuItem!.startStake), h: 400, img: props.menuItem!.isIn ? img1 : img2
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ svgConfig.leng = [
|
|
|
|
|
+ { name: '隧道进口', stake: mileage2string(props.menuItem!.startStake, props.menuItem!.stakeType), x: '0%' },
|
|
|
|
|
+ { name: '当前掌子面', stake: mileage2string(props.menuItem!.currentStake, props.menuItem!.stakeType), x: `${(props.menuItem!.currentStake - props.menuItem!.startStake) / props.menuItem!.length * 100 - 2.5}%` },
|
|
|
|
|
+ { name: '隧道出口', stake: mileage2string(props.menuItem!.endStake, props.menuItem!.stakeType), x: `${props.menuItem!.length / props.menuItem!.length * 100 - 5}%` }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ watch(() => props.menuItem, (v) => {
|
|
|
|
|
+ initSVG()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ watch(() => props.data, (v) => {
|
|
|
|
|
+ console.log(777, v)
|
|
|
|
|
+
|
|
|
|
|
+ setData(v)
|
|
|
|
|
+ })
|
|
|
|
|
+ onMounted(() => clearTimeout(timer.value))
|
|
|
|
|
+ </script>
|
|
|
|
|
+
|
|
|
|
|
+ <style lang="less" scoped>
|
|
|
|
|
+ .box {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+
|
|
|
|
|
+ svg {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: calc(100% - 98px);
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &-leng {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 98px;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+
|
|
|
|
|
+ &-item {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 0.14rem;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translate(0, -50%);
|
|
|
|
|
+ &:nth-child(2){
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ </style>
|