翠屏区交通应急指挥和信息化中心

翠屏区交通应急指挥和信息化中心


项目介绍 Link to 项目介绍

本系统专为翠屏区设计,旨在提升交通管理和应急响应效率。系统包含以下核心模块:

综合交通监测:实时展示交通数据的大屏监控。 Link to 综合交通监测:实时展示交通数据的大屏监控。

道路客运监测 道路客运监测 公路货运监测 公路货运监测 科技治违监测 科技治违监测 交通执法监测 交通执法监测 渡口监测 渡口监测 交通工程监测 交通工程监测 公路养护监测 公路养护监测 安全应急监测 安全应急监测

信息接入中心:负责维护和更新大屏显示的数据。 Link to 信息接入中心:负责维护和更新大屏显示的数据。

信息接入中心

非法营运监测:监控和识别非法营运行为。 Link to 非法营运监测:监控和识别非法营运行为。

交通应急指挥:在紧急情况下协调和指挥交通资源。 Link to 交通应急指挥:在紧急情况下协调和指挥交通资源。

事件登记 事件登记 指挥调度 指挥调度 主题配置 主题配置

视频汇聚中心:集成第三方视频监控系统。 Link to 视频汇聚中心:集成第三方视频监控系统。

交通数据中心:与第三方平台进行数据交互和集成 Link to 交通数据中心:与第三方平台进行数据交互和集成

项目难点 Link to 项目难点

  1. Leaflet地图操作
  • 需求:在地图上实现多图层的绘制与移除,包括点位、路线、箭头和区域;并能够绘制特定范围以检索该区域内的物资。
  • 解决方案:利用Leaflet的图层管理功能,实现动态添加和移除图层;通过地理围栏技术划定区域,结合后端服务进行物资检索。
  1. 视频监控播放
  • 需求:对接1078设备协议,实现第三方视频监控的播放。
  • 解决方案:开发适配1078协议的接口,确保视频流的稳定接收和播放,同时优化视频加载和播放的用户体验。
  1. 门户系统
  • 需求:提供开放接口,实现内部子系统的免登跳转和第三方系统接入。
  • 解决方案
  • PC、H5门户系统免登实现
  1. 门户跳转子系统时,通过地址栏传输token和projectId。
  2. 子系统携带这些信息调用免登校验接口,获取并保存用户信息。
  3. 登录成功后,用户可直接进入系统。
  • H5门户系统在同一窗口中打开子系统(见下方代码示例)
  1. 门户端点击子系统跳转时,跳转到门户系统中的webview页面。

  2. webview页面的URL携带token和projectId,并监听message事件以接收子系统发送的消息。

  3. 子系统接收这些信息,调用免登校验接口获取用户信息并保存。

  4. 登录成功后,子系统自定义顶部导航栏,通过发送消息给webview页面实现返回门户端的功能。

  5. 自定义多选弹窗组件

  • 需求:开发一个基于Vue的自定义多选弹窗组件。
  • 解决方案:利用Vue的组件化特性,设计和实现一个灵活、可复用的多选弹窗组件vue-select-dialog,支持多种选择模式和用户交互。

代码示例 Link to 代码示例

门户端跳转子系统 Link to 门户端跳转子系统

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
export default {
  methods: {
    goNewPage(data) {
      let showTopBar = data.projectType !== 0; // 是否展示顶部标题栏[0内部系统]
      let passData = JSON.stringify({
        sysCode: data.sysCode,
        token: this.vuex_token, // 门户系统登录时会存储的token
        navigateUrl: location.href,
      });
      uni.navigateTo({
        url:
          "/pages/common/webview?url=" +
          `${data.projectUrl}?data=${passData}` +
          "&title=" +
          data.appName +
          "&showTopBar=" +
          showTopBar,
      });
    },
  },
};
</script>

门户端webview封装 Link to 门户端webview封装

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<template>
  <view class="wrap-view">
    <view class="wrap-view-header" v-if="showTopBar">
      <i class="uni-btn-icon icon" @click="goBack"></i>
      <text class="text">{{ title }}</text>
    </view>

    <view
      class="wrap-view-content"
      :style="{
        height: showTopBar ? 'calc(100vh - 88rpx)' : '100vh',
        top: showTopBar ? '88rpx' : '0',
      }"
    >
      <web-view
        :webview-styles="webviewStyles"
        :src="url"
        @message="getMessage"
      ></web-view>
    </view>
  </view>
</template>
<script>
export default {
  data() {
    return {
      showTopBar: true,
      url: "",
      title: "",
      webviewStyles: {
        progress: {
          color: "#FF7200",
        },
      },
    };
  },
  onLoad(params) {
    this.initData(params);
  },

  methods: {
    getMessage(e) {
      const data = e.data;
      if (typeof data !== "string") return;
      let dataObj;
      try {
        dataObj = JSON.parse(data);
      } catch (error) {
        console.error("Failed to parse data:", error);
        return;
      }
      if (dataObj.data === "back") {
        this.autoBackIndex();
      } else if (dataObj.data === "logout") {
        this.autoLogOut();
      }
    },

    autoBackIndex() {
      uni.reLaunch({
        url: "/pages/sys/workbench/index",
      });
    },

    autoLogOut() {
      this.$u.api.normal
        .logOut()
        .then((res) => {
          this.$u.toast(res.message);
          if (res.code == "1") {
            setTimeout(() => {
              uni.clearStorage();
            }, 500);
          }
        })
        .finally(() => {
          setTimeout(() => {
            uni.removeStorageSync("lifeData");
            uni.reLaunch({
              url: "/pages/sys/login/index",
            });
          }, 500);
        });
    },

    initData(params) {
      let { url, title, showTopBar, href } = params;
      this.url = url;
      this.title = title;
      this.showTopBar = JSON.parse(showTopBar);
    },

    goBack() {
      uni.reLaunch({
        url: "/pages/sys/workbench/index",
      });
    },

    handleMessage(event) {
      const message = JSON.parse(event.detail.data);
      if (message.type === "navigateBack") {
        uni.reLaunch({
          url: message.url,
        });
      }
    },
  },
  mounted() {
    // #ifdef H5
    // A项目运行在H5时,如果不加这句代码,真机运行的时候,就无法监听到message事件
    window.addEventListener("message", this.getMessage, false);
    // #endif
  },
};
</script>

<style lang="scss">
.wrap-view {
  height: 100vh;
  position: relative;

  &-header {
    height: 44px;
    background-color: #3296fa;
    color: #fff;
    z-index: 9999 !important;
    position: absolute;
    font-size: 32rpx;
    top: 0;
    left: 0;
    width: 100vw;
    text-align: center;
    line-height: 44px;
    font-weight: 700;

    .icon {
      position: absolute;
      left: 10rpx;
      top: 50%;
      transform: translateY(-50%);
      font-size: 54rpx;
      color: #fff;
    }
  }

  &-content {
    position: absolute;
    left: 0;
    width: 100vw;
  }
}
</style>

子系统自定义顶部导航栏 Link to 子系统自定义顶部导航栏

JSON
1
2
3
4
5
6
7
8
9
[
    {
      "path": "pages/sys/workbench/index",
      "style": {
        "navigationBarTitleText": "工作台",
        "navigationStyle": "custom"
      }
    }
]
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let normalMsg = {
  data: "back" // back 返回到主系统;logout 退出登录并返回到主系统登录页
};
export default function (msg = normalMsg) {
  // #ifndef MP-WEIXIN
  // const webview = web.webView
  // if (typeof webview !== "undefined") {
  //   webview.postMessage({ data: "back" });
  // }
  // #endif

  if (window) {
    window.parent.postMessage(JSON.stringify(msg), "*");
  }
}

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<template>
  <view class="wrap-view">
    <view class="wrap-view-header" v-if="showTopBar">
      <i class="uni-btn-icon icon" @click="goBack"></i>
      <text class="text">{{ title }}</text>
    </view>

    <view
      class="wrap-view-content"
      :style="{
        top: showTopBar ? '88rpx' : '0',
        bottom: '100rpx'
      }"
    >
      <slot name="content"></slot>
    </view>
  </view>
</template>
<script>
import webviewBack from "@/common/webviewBack";
export default {
  props: {
    title: {
      type: String,
      default: ""
    },
    showTopBar: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {};
  },
  onLoad(params) {
  },

  methods: {
    goBack() {
      webviewBack({
        data: "back"
      });
    }
  }
};
</script>
<style lang="scss">
.wrap-view {
  position: relative;

  &-header {
    height: 88rpx;
    background-color: #3296fa;
    color: #fff;
    z-index: 9999 !important;
    position: fixed;
    font-size: 32rpx;
    top: 0;
    left: 0;
    width: 100vw;
    text-align: center;
    line-height: 88rpx;
    font-weight: 700;

    .icon {
      position: absolute;
      left: 10rpx;
      top: 50%;
      transform: translateY(-50%);
      font-size: 54rpx;
      color: #fff;
    }
  }

  &-content {
    position: absolute;
    left: 0;
    width: 100vw;
  }
}
</style>

子系统使用自定义导航栏 Link to 子系统使用自定义导航栏

VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
  <ytNavbar title="工作台">
    <template slot="content">
       内容
    </template>
  </ytNavbar>
</template>
<script>
import ytNavbar from "@/components/ytNavbar/ytNavbar.vue";
export default {
  components: {
    ytNavbar
  },
}
</script>