13404089107
2025-05-06 e1333cf445616843299228ac760cce28dd7c9813
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/shehong-vehicle-supervision
39个文件已修改
5个文件已添加
2474 ■■■■■ 已修改文件
package.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/header/more.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/alarm.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/car.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/company.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/complaint.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/home.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/order.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/routerIcon/sys.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/severBg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PlayLive/index.vue 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/index.vue 441 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/router.js 381 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/baseurl.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/404.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/components/detailOrderModal.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/detail.vue 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/service.js 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-type/index.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-type/service.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/company/component/detailModal.vue 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/company/index.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/company/service.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/complaint/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/early-warning/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/home/index.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/home/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/login/index.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/order/component/detailModal.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/order/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/playback/index.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/driver/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/role/addEdit.vue 448 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/role/detail.vue 134 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/role/index.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/user/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -3,7 +3,7 @@
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vue-cli-service serve",
    "dev": "vue-cli-service serve --port 8089",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
@@ -49,4 +49,4 @@
    "> 1%",
    "last 2 versions"
  ]
}
}
src/App.vue
@@ -56,6 +56,16 @@
.el-message {
  z-index: 9999 !important;
  position: fixed !important;
  right: 20px !important;
  bottom: 20px !important;
  top: auto !important;
  left: auto !important;
  transform: none !important;
  min-width: 300px;
  padding: 15px 20px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
#app {
src/assets/header/more.png

src/assets/logo.png

src/assets/routerIcon/alarm.png

src/assets/routerIcon/car.png

src/assets/routerIcon/company.png

src/assets/routerIcon/complaint.png

src/assets/routerIcon/home.png

src/assets/routerIcon/order.png

src/assets/routerIcon/sys.png

src/assets/severBg.png
src/assets/title.png
src/components/PlayLive/index.vue
@@ -1,6 +1,16 @@
<template>
    <div style="height: 100%;">
        <video id="video" style="height: 100%;width: 100%;" muted controls></video>
    <div style="height: 100%; position: relative;">
        <div style="width: 100%; height: 100%; border-radius: 9px; background: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column">
            <video id="video" style="width: 100%; height: 100%; border-radius: 9px; display: none" muted controls></video>
            <el-empty description="暂无视频信息" :image-size="80"></el-empty>
        </div>
        <div v-if="showError" class="error-box" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 9999;">
            <div class="error-content" style="text-align: center; color: #fff;">
                <i class="el-icon-warning" style="font-size: 48px; color: #E6A23C; margin-bottom: 16px;"></i>
                <p style="margin: 8px 0; font-size: 16px;">视频播放失败</p>
                <p style="margin: 8px 0; font-size: 16px;">请稍后重试</p>
            </div>
        </div>
    </div>
</template>
@@ -11,7 +21,7 @@
    props: {
        serverIp: {
            type: String,
            required: ''
            required: null
        },
        serverPort: {
            type: Number,
@@ -21,71 +31,151 @@
            type: Number,
            required: null
        },
        urlLink: {
            type: String,
            required: null,
        },
    },
    data() {
        return {
            flvPlayer: null,
            timer: null,
            showError: false
        }
    },
    watch: {
        urlLink: {
            handler(newUrl) {
                if (newUrl) {
                    this.destroyPlayer();
                    this.playDetection();
                }
            },
            immediate: true
        }
    },
    mounted() {
        this.playDetection()
        if (this.urlLink) {
            this.playDetection();
        }
    },
    beforeDestroy() {
        this.destroyPlayer();
    },
    methods: {
        playDetection() {
            if (flvjs.isSupported()) {
                playDetection(this.carId).then(res => {
            this.showError = false;
            if (!flvjs.isSupported()) {
                this.showError = true;
                this.$emit('video-error');
                return;
            }
            playDetection(this.carId).then(res => {
                if (this.flvPlayer) {
                    this.destroyPlayer();
                }
                try {
                    this.flvPlayer = flvjs.createPlayer({
                        type: 'flv',//视频类型
                        isLive: true,//是否为直播
                        cors: true,//是否开启跨域
                        hasAudio: false,//是否开启音频
                        hasVideo: true,//是否开启视频
                        url: `http://${this.serverIp}:${this.serverPort}/live?port=1935&app=flv&stream=${this.carId}`,  // 后端拿到的视频路径
                        type: 'flv', //视频类型
                        isLive: true, //是否为直播
                        cors: true, //是否开启跨域
                        hasAudio: false, //是否开启音频
                        hasVideo: true, //是否开启视频
                        url: this.urlLink, // 后端拿到的视频路径
                        enableWorker: true, //启用 Web Worker 进程来加速视频的解码和处理过程
                        enableStashBuffer: false, // 启用数据缓存机制,提高视频的流畅度和稳定性。
                        stashInitialSize: 1024, // 初始缓存大小。单位:字节。建议针对直播:调整为1024kb
                        stashInitialTime: 0.2, // 缓存初始时间。单位:秒。建议针对直播:调整为200毫秒
                        seekType: 'range', // 建议将其设置为“range”模式,以便更快地加载视频数据,提高视频的实时性。
                        seekType: 'range', // 建议将其设置为"range"模式,以便更快地加载视频数据,提高视频的实时性。
                        lazyLoad: false, //关闭懒加载模式,从而提高视频的实时性。建议针对直播:调整为false
                        lazyLoadMaxDuration: 0.2, // 懒加载的最大时长。单位:秒。建议针对直播:调整为200毫秒
                        deferLoadAfterSourceOpen: false // 不预先加载视频数据,在 MSE(Media Source Extensions)打开后立即加载数据,提高视频的实时性。建议针对直播:调整为false
                    });
                    let video = document.getElementById('video');
                    this.flvPlayer.attachMediaElement(video); // video容器
                    if (!video) {
                        throw new Error('Video element not found');
                    }
                    this.flvPlayer.attachMediaElement(video);
                    this.flvPlayer.load();
                    this.flvPlayer.play().then(res => {
                    this.flvPlayer.play().then(() => {
                        // 显示视频元素
                        video.style.display = 'block';
                        // 隐藏空状态
                        const emptyElement = video.parentElement.querySelector('.el-empty');
                        if (emptyElement) {
                            emptyElement.style.display = 'none';
                        }
                        this.timer = setInterval(() => {
                            playDetection(this.carId)
                        }, 5000)
                            playDetection(this.carId);
                        }, 5000);
                    }).catch(err => {
                        console.error('视频播放失败:', err);
                        this.showError = true;
                        this.destroyPlayer();
                    })
                    // 错误监听
                    this.flvPlayer.on('error', (err) => {
                        this.destroyPlayer();
                        this.$emit('video-error');
                    });
                })
            }
                    this.flvPlayer.on('error', (err) => {
                        console.error('视频播放器错误:', err);
                        this.showError = true;
                        this.destroyPlayer();
                        this.$emit('video-error');
                    });
                } catch (err) {
                    console.error('创建播放器失败:', err);
                    this.showError = true;
                    this.destroyPlayer();
                    this.$emit('video-error');
                }
            }).catch(err => {
                console.error('获取视频流失败:', err);
                this.showError = true;
                this.destroyPlayer();
                this.$emit('video-error');
            });
        },
        destroyPlayer() {
            // 销毁播放器释放资源
            if (this.flvPlayer) {
                if (this.timer) clearInterval(this.timer)
                closeRealVideo(this.carId).then(res => {
                if (this.timer) {
                    clearInterval(this.timer);
                    this.timer = null;
                }
                try {
                    this.flvPlayer.pause();
                    this.flvPlayer.unload();
                    this.flvPlayer.detachMediaElement();
                    this.flvPlayer.destroy();
                } catch (err) {
                    console.error('销毁播放器失败:', err);
                } finally {
                    this.flvPlayer = null;
                })
                }
                closeRealVideo(this.carId).catch(err => {
                    console.error('关闭视频流失败:', err);
                });
                // 恢复空状态的显示
                const video = document.getElementById('video');
                if (video) {
                    video.style.display = 'none';
                    const emptyElement = video.parentElement.querySelector('.el-empty');
                    if (emptyElement) {
                        emptyElement.style.display = 'block';
                    }
                }
            }
        }
    }
}
</script>
<style></style>
<style scoped>
/* 移除之前的样式,使用内联样式确保样式生效 */
</style>
src/layouts/index.vue
@@ -1,229 +1,326 @@
<template>
    <div class="sticky top0 layout">
        <div class="header relative">
            <div @click="$router.push('/home')" class="title">
                <img src="@/assets/logo.png" alt="">
                射洪“两客一危”监管平台
            </div>
            <div></div>
            <div class="flex a-center pr--40">
                <div class="flex a-center mr--72">
                    <img src="@/assets/header/photo.png" class="w--32 h--32 shrink0 mr--10"
                        style="border-radius: 50%;" />
                    <div class="fs-- 18 lh--25 color2">{{ $store.state.userInfo.nickName }}</div>
                </div>
                <div class="dropdown" @mouseenter="toggleDropdown(true)" @mouseleave="toggleDropdown(false)">
                    <img src="@/assets/header/more.png" class="w--16 h--16" />
                    <div v-if="isOpen" class="dropdown-menu">
                        <div @click="clickItem(item)" class="dropdown-item" v-for="item in menuItems" :key="item.text">
                            <i :class="item.icon"></i> {{ item.text }}
                        </div>
                    </div>
                </div>
            </div>
  <div class="sticky top0 layout">
    <div class="header relative" style="align-items: flex-end">
      <div @click="$router.push('/home')" class="title">
        <img src="@/assets/logo.png" alt="" />
        <span> 射洪两客一危监管平台 </span>
      </div>
      <div></div>
      <div class="flex a-center pr--40">
        <div class="flex a-center mr--72">
          <img
            src="@/assets/header/photo.png"
            class="w--32 h--32 shrink0 mr--10"
            style="border-radius: 50%"
          />
          <div class="fs-- 18 lh--25 color2">
            {{ $store.state.userInfo.nickName }}
          </div>
        </div>
        <div class="menu w100 bgColor1">
            <div v-for="(item, index) in routesList" :key="index" class="flex a-center h100">
                <div v-if="!item.meta || !item.meta.title" class="h100">
                    <template v-for="(item2, index2) in item.children">
                        <div :key="index2" @click="pushPath(item2.path)" v-if="!item2.meta.hide"
                            class="flex a-center j-center h100 w--160 menuItemHover pointer"
                            :class="item2.path == $route.path && 'bgColor2'">
                            <img v-if="item2.meta.icon" :src="require(`@/assets/routerIcon/${item2.meta.icon}.png`)"
                                class="w--15 h--15 mr--12 shrink0" />
                            <div class="color1">
                                {{ item2.meta.title }}
                            </div>
                        </div>
                    </template>
                </div>
                <div v-else :class="$route.path.includes('systemManage') && 'bgColor2'"
                    class="h100 w--160 menuItemHover dropdown" @mouseenter="routerDropdown(true)"
                    @mouseleave="routerDropdown(false)">
                    <div class="flex a-center j-center h100">
                        <img :src="require(`@/assets/routerIcon/${item.meta.icon}.png`)"
                            class="w--15 h--15 mr--12 shrink0" />
                        <div class="color1">
                            {{ item.meta.title }}
                        </div>
                    </div>
                    <div v-if="routerIsOpen" class="dropdown-menu positionTwo">
                        <template v-for="(item2, index2) in item.children">
                            <div v-if="!item2.meta.hide" :key="index2" @click="pushPath(item.path + '/' + item2.path)"
                                class="dropdown-item flex a-center">
                                {{ item2.meta.title }}
                            </div>
                        </template>
                    </div>
                </div>
        <div
          class="dropdown"
          @mouseenter="toggleDropdown(true)"
          @mouseleave="toggleDropdown(false)"
        >
          <img src="@/assets/header/more.png" class="w--16 h--16" />
          <div v-if="isOpen" class="dropdown-menu">
            <div
              @click="clickItem(item)"
              class="dropdown-item"
              v-for="item in menuItems"
              :key="item.text"
            >
              <i :class="item.icon"></i> {{ item.text }}
            </div>
          </div>
        </div>
        <div class="main">
            <router-view></router-view>
        </div>
        <ResetPassword v-if="passwordVisible" :row="row" :dialogVisible="passwordVisible"
            @close="passwordVisible = false, row = {}" @confirm="passwordConfirm" />
      </div>
    </div>
    <div class="menu w100 bgColor1">
      <template v-for="(item, index) in routesList">
        <template v-if="!item.meta || !item.meta.title">
          <template v-for="(item2, index2) in item.children">
            <div
              v-if="!item2.meta.hide"
              :key="index2"
              @click="pushPath(item2.path)"
              class="flex a-center j-center h100 br--18 w--160 menuItemHover pointer"
              :class="item2.path == $route.path && 'bgColor2'"
            >
              <img
                v-if="item2.meta.icon"
                :src="require(`@/assets/routerIcon/${item2.meta.icon}.png`)"
                class="w--40 h--40 mr--12 shrink0"
              />
              <div class="color1">
                {{ item2.meta.title }}
              </div>
            </div>
          </template>
        </template>
        <div
          v-else
          :key="index"
          :class="$route.path.includes('systemManage') && 'bgColor2'"
          class="h100 w--160 br--18 menuItemHover dropdown"
          @mouseenter="routerDropdown(true)"
          @mouseleave="routerDropdown(false)"
        >
          <div class="flex a-center j-center h100">
            <img
              :src="require(`@/assets/routerIcon/${item.meta.icon}.png`)"
              class="w--40 h--40 mr--12 shrink0"
            />
            <div class="color1">
              {{ item.meta.title }}
            </div>
          </div>
          <div v-if="routerIsOpen" class="dropdown-menu positionTwo">
            <template v-for="(item2, index2) in item.children">
              <div
                v-if="!item2.meta.hide"
                :key="index2"
                @click="pushPath(item.path + '/' + item2.path)"
                class="dropdown-item flex a-center"
              >
                {{ item2.meta.title }}
              </div>
            </template>
          </div>
        </div>
      </template>
    </div>
    <div class="main">
      <router-view></router-view>
    </div>
    <ResetPassword
      v-if="passwordVisible"
      :row="row"
      :dialogVisible="passwordVisible"
      @close="(passwordVisible = false), (row = {})"
      @confirm="passwordConfirm"
    />
  </div>
</template>
<script>
import routes from '@/router/router'
import { mapMutations } from 'vuex';
import ResetPassword from '@/view/systemManage/user/components/resetPassWord.vue'
import { updatePwd } from '@/view/systemManage/user/service'
import routes from "@/router/router";
import { mapMutations } from "vuex";
import ResetPassword from "@/view/systemManage/user/components/resetPassWord.vue";
import { updatePwd } from "@/view/systemManage/user/service";
import { getRoleInfo } from "../view/systemManage/role/service";
export default {
    components: {
        ResetPassword,
    },
    data() {
        return {
            routesList: routes,
            isOpen: false,
            routerIsOpen: false,
            menuItems: [
                { text: '密码设置' },
                { text: '退出登录' },
            ],
            passwordVisible: false,
            row: {}
        };
    },
    created() {
    },
    methods: {
        ...mapMutations(['clearToken']),
        passwordConfirm(form) {
            updatePwd(form).then(() => {
                this.row = {}
                this.passwordVisible = false
                this.msgsuccess('修改密码成功')
            })
        },
        clickItem(item) {
            switch (item.text) {
                case '密码设置':
                    this.row = this.$store.state.userInfo
                    this.passwordVisible = true
                    break;
                case '退出登录':
                    this.clearToken()
                    window.location.replace(`/`);
                    break;
                default:
                    break;
            }
        },
        pushPath(path) {
            this.$router.push(path)
            if (this.routerIsOpen) this.routerIsOpen = false
        },
        toggleDropdown(state) {
            this.isOpen = state;
        },
        routerDropdown(state) {
            this.routerIsOpen = state;
        }
  components: {
    ResetPassword,
  },
  data() {
    return {
      routesList: [],
      isOpen: false,
      routerIsOpen: false,
      menuItems: [{ text: "密码设置" }, { text: "退出登录" }],
      passwordVisible: false,
      row: {},
    };
  },
  created() {
    console.log(this.$store.state.permissions,this.$router.options.routes);
    if (localStorage.getItem("userInfo")) {
      this.routesList = this.filterRoutes(this.$router.options.routes)
    } else {
      this.routesList = routes;
    }
}
  },
  methods: {
    ...mapMutations(["clearToken"]),
    // 过滤路由函数
    filterRoutes(routes) {
      const permissions = this.$store.state.permissions || [];
      return routes.filter((route) => {
        // 如果路由有children,递归过滤
        if (route.children && route.children.length > 0) {
          const filteredChildren = this.filterRoutes(route.children);
          route.children = filteredChildren;
          // 如果过滤后children为空,且父路由有menuId需要权限,则过滤掉该路由
          if (
            filteredChildren.length === 0 &&
            route.meta &&
            route.meta.menuId &&
            !permissions.includes(route.meta.menuId)
          ) {
            return false;
          }
          // 如果过滤后children不为空,保留该父路由
          if (filteredChildren.length > 0) {
            return true;
          }
        }
        // 处理没有children的路由
        // 1. 如果路由没有menuId,保留
        if (!route.meta || !route.meta.menuId) {
          return true;
        }
        // 2. 如果路由有menuId,检查权限
        return permissions.includes(route.meta.menuId);
      });
    },
    passwordConfirm(form) {
      updatePwd(form).then(() => {
        this.row = {};
        this.passwordVisible = false;
        this.msgsuccess("修改密码成功");
      });
    },
    clickItem(item) {
      switch (item.text) {
        case "密码设置":
          this.row = this.$store.state.userInfo;
          this.passwordVisible = true;
          break;
        case "退出登录":
          this.clearToken();
          window.location.replace(`/`);
          break;
        default:
          break;
      }
    },
    pushPath(path) {
      this.$router.push(path);
      if (this.routerIsOpen) this.routerIsOpen = false;
    },
    toggleDropdown(state) {
      this.isOpen = state;
    },
    routerDropdown(state) {
      this.routerIsOpen = state;
    },
  },
};
</script>
<style lang="less" scoped>
.layout {
    display: flex;
    flex-direction: column;
    height: 100%;
  display: flex;
  flex-direction: column;
  height: 100%;
    .main {
        flex: 1;
        overflow: auto;
    }
  .main {
    flex: 1;
    overflow: auto;
  }
}
.bgColor1 {
    background-color: #0E6EFD;
  background-color: #3367ce;
}
.bgColor2 {
    background: #0D55B9;
  background: #2b5ab6;
}
.color1 {
    color: #fff;
  color: #fff;
}
.color2 {
    color: rgba(0, 0, 0, .6);
  // color: rgba(0, 0, 0, .6);
  color: #fff;
}
.header {
    height: 80px;
    background: #fff;
  height: 80px;
  background: #3367ce;
  background-image: url("../assets/title.png");
  background-size: 100% 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  .title {
    display: flex;
    justify-content: center;
    font-weight: 600;
    font-size: 30px;
    align-items: center;
    justify-content: space-between;
    position: absolute;
    top: 38%;
    left: 48.5%;
    transform: translate(-50%, -50%);
    // color: rgba(0, 0, 0, .8);
    color: #fff;
    .title {
        display: flex;
        justify-content: center;
        font-weight: 600;
        font-size: 24px;
        align-items: center;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        color: rgba(0, 0, 0, .8);
        img {
            width: 40px;
            height: 40px;
            margin-right: 10px;
        }
    img {
      width: 50px;
      height: 50px;
      margin-right: 25px;
    }
    span {
      letter-spacing: 4px;
      text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); // 新增字体阴影
    }
  }
}
.menu {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 220px;
}
.menuItemHover {
  flex: 1;
  max-width: 160px;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s;
}
.menuItemHover:hover {
    background: #0D55B9;
  border-radius: 18px;
  background: #2b5ab6;
}
.dropdown {
    position: relative;
    display: inline-block;
    cursor: pointer;
  position: relative;
  display: inline-block;
  cursor: pointer;
}
.positionTwo {
    transform: unset !important;
    width: 160px !important;
  transform: unset !important;
  width: 160px !important;
}
.dropdown-menu {
    position: absolute;
    top: 100%;
    left: 0;
    transform: translateX(-50%);
    background: white;
    border: 1px solid #ccc;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    z-index: 1000;
    border-radius: 8px;
  position: absolute;
  top: 100%;
  left: 0;
  transform: translateX(-50%);
  background: white;
  border: 1px solid #ccc;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  border-radius: 8px;
}
.dropdown-item {
    padding: 8px 16px;
    white-space: nowrap;
    /* 防止文本换行 */
  padding: 8px 16px;
  white-space: nowrap;
  /* 防止文本换行 */
}
.dropdown-item:hover {
    background-color: #f0f0f0;
    /* 添加 hover 效果 */
  background-color: #f0f0f0;
  /* 添加 hover 效果 */
}
</style>
src/main.js
@@ -60,6 +60,32 @@
  });
}
Vue.prototype.$checkPermission = function(permissionId) {
  const permissions = store.state.permissions || [];
  if (!permissions.includes(permissionId)) {
    this.$router.push('/404');
    return false;
  }
  return true;
};
Vue.directive('permission', {
  inserted: function (el, binding) {
    const permissions = store.state.permissions || [];
    if (!permissions.includes(binding.value)) {
      el.style.display = 'none';
    }
  },
  update: function (el, binding) {
    const permissions = store.state.permissions || [];
    if (!permissions.includes(binding.value)) {
      el.style.display = 'none';
    } else {
      el.style.display = '';
    }
  }
});
new Vue({
  router,
  store,
src/router/index.js
@@ -16,12 +16,12 @@
  }),
  routes
})
const router = createRouter()
// 路由守卫
router.beforeEach((to, from, next) => {
  const token = store.state.token
  const permissions = store.state.permissions || []
  if (!token && to.path != '/') {
    // 如果没有 token 并且不是去登录页,重定向到登录页
    next('/')
@@ -29,9 +29,26 @@
    // 如果有 token 并且要去登录页,重定向到首页
    next('/home')
  } else {
    // 检查路由权限
    if (to.meta && to.meta.menuId) {
      // 如果路由有menuId,检查是否有权限访问
      if (permissions.length === 0) {
        // 如果权限数组为空,说明是刚登录,还未获取权限,允许访问
        next()
      } else if (permissions.includes(to.meta.menuId)) {
        // 有权限,允许访问
        next()
      } else {
        // 无权限,重定向到404或首页
        next('/404')
      }
    } else {
      // 路由没有menuId,直接放行
      next()
    }
    // 清理 localStorage
    localStorage.removeItem('registerForm')
    next()
  }
})
src/router/router.js
@@ -1,177 +1,204 @@
import Layout from '@/layouts'
/**
 * icon
 * home: 首页
 * alarm: 报警
 * car: 车辆
 * company: 公司
 * complaint: 投诉
 * sys: 系统
 * order: 订单
 *
 * hide: true, // 是否隐藏
 *
 */
export default [
    {
        path: '/',
        component: () => import('@/view/login'),
    },
    {
        path: '/home',
        component: Layout,
        children: [
            {
                path: '/home',
                component: () => import('@/view/home'),
                meta: {
                    title: '首页',
                    icon: 'home'
                }
            },
        ]
    },
    {
        path: '/company',
        component: Layout,
        children: [
            {
                path: '/company',
                component: () => import('@/view/company'),
                meta: {
                    title: '公司管理',
                    icon: 'company'
                }
            }
        ]
    },
    {
        path: '/car',
        component: Layout,
        children: [
            {
                path: '/car-manage',
                component: () => import('@/view/car-manage'),
                meta: {
                    title: '车辆管理',
                    icon: 'car'
                }
            },
            {
                path: '/car-detail',
                component: () => import('@/view/car-manage/detail'),
                meta: {
                    title: '车辆详情',
                    icon: 'car',
                    hide: true
                }
            },
            {
                path: '/car-playback',
                component: () => import('@/view/playback'),
                meta: {
                    title: '车辆回放',
                    hide: true
                }
            }
        ]
    },
    {
        path: '/early-warning',
        component: Layout,
        children: [
            {
                path: '/early-warning',
                component: () => import('@/view/early-warning'),
                meta: {
                    title: '报警记录',
                    icon: 'alarm'
                }
            }
        ]
    },
    {
        path: '/order',
        component: Layout,
        children: [
            {
                path: '/order',
                component: () => import('@/view/order'),
                meta: {
                    title: '订单记录',
                    icon: 'order'
                }
            }
        ]
    },
    {
        path: '/complaint',
        component: Layout,
        children: [
            {
                path: '/complaint',
                component: () => import('@/view/complaint/index'),
                meta: {
                    title: '投诉记录',
                    icon: 'complaint'
                }
            }
        ]
    },
    {
        path: '/systemManage',
        meta: {
            title: "系统管理",
            icon: 'sys',
        },
        component: Layout,
        children: [
            {
                path: 'driver',
                component: () => import('@/view/systemManage/driver'),
                meta: {
                    title: '驾驶员列表',
                }
            },
            {
                path: 'type',
                component: () => import('@/view/car-type'),
                meta: {
                    title: '车辆分类',
                }
            },
            {
                path: 'user',
                component: () => import('@/view/systemManage/user'),
                meta: {
                    title: '用户管理',
                }
            },
            {
                path: 'role',
                component: () => import('@/view/systemManage/role'),
                meta: {
                    title: '角色管理',
                }
            },
            {
                path: 'add-role',
                component: () => import('@/view/systemManage/role/addEdit.vue'),
                meta: {
                    title: '添加角色',
                    hide: true
                }
            },
            {
                path: 'role-detail',
                component: () => import('@/view/systemManage/role/detail'),
                meta: {
                    title: '角色详情',
                    hide: true
                }
            }
        ]
    }
]
import Layout from "@/layouts";
/**
 * icon
 * home: 首页
 * alarm: 报警
 * car: 车辆
 * company: 公司
 * complaint: 投诉
 * sys: 系统
 * order: 订单
 *
 * hide: true, // 是否隐藏
 *
 */
export default [
  {
    path: "/",
    component: () => import("@/view/login"),
  },
  {
    path: "/home",
    component: Layout,
    children: [
      {
        path: "/home",
        component: () => import("@/view/home"),
        meta: {
          title: "首页",
          icon: "home",
          menuId: 1,
        },
      },
    ],
  },
  {
    path: "/404",
    component: Layout,
    children: [
      {
        path: "/404",
        component: () => import("@/view/404.vue"),
        meta: {
          title: "404",
          icon: "home",
          hide: true,
        },
      },
    ],
  },
  {
    path: "/company",
    component: Layout,
    children: [
      {
        path: "/company",
        component: () => import("@/view/company"),
        meta: {
          title: "公司管理",
          icon: "company",
          menuId: 2,
        },
      },
    ],
  },
  {
    path: "/car",
    component: Layout,
    children: [
      {
        path: "/car-manage",
        component: () => import("@/view/car-manage"),
        meta: {
          title: "车辆管理",
          icon: "car",
          menuId: 3,
        },
      },
      {
        path: "/car-detail",
        component: () => import("@/view/car-manage/detail"),
        meta: {
          title: "车辆详情",
          icon: "car",
          hide: true,
          menuId: 31,
        },
      },
      {
        path: "/car-playback",
        component: () => import("@/view/playback"),
        meta: {
          title: "车辆回放",
          hide: true,
        },
      },
    ],
  },
  {
    path: "/early-warning",
    component: Layout,
    children: [
      {
        path: "/early-warning",
        component: () => import("@/view/early-warning"),
        meta: {
          title: "报警记录",
          icon: "alarm",
          menuId: 9,
        },
      },
    ],
  },
  {
    path: "/order",
    component: Layout,
    children: [
      {
        path: "/order",
        component: () => import("@/view/order"),
        meta: {
          title: "订单记录",
          icon: "order",
          menuId: 6,
        },
      },
    ],
  },
  {
    path: "/complaint",
    component: Layout,
    children: [
      {
        path: "/complaint",
        component: () => import("@/view/complaint/index"),
        meta: {
          title: "投诉记录",
          icon: "complaint",
          menuId: 12,
        },
      },
    ],
  },
  {
    path: "/systemManage",
    meta: {
      title: "系统管理",
      icon: "sys",
    },
    component: Layout,
    children: [
      {
        path: "driver",
        component: () => import("@/view/systemManage/driver"),
        meta: {
          title: "驾驶员列表",
          menuId: 4,
        },
      },
      {
        path: "type",
        component: () => import("@/view/car-type"),
        meta: {
          title: "车辆分类",
          menuId: 5,
        },
      },
      {
        path: "user",
        component: () => import("@/view/systemManage/user"),
        meta: {
          title: "用户管理",
          menuId: 15,
        },
      },
      {
        path: "role",
        component: () => import("@/view/systemManage/role"),
        meta: {
          title: "角色管理",
          menuId: 22,
        },
      },
      {
        path: "add-role",
        component: () => import("@/view/systemManage/role/addEdit.vue"),
        meta: {
          title: "添加角色",
          hide: true,
          menuId: 24,
        },
      },
      {
        path: "role-detail",
        component: () => import("@/view/systemManage/role/detail"),
        meta: {
          title: "角色详情",
          hide: true,
          menuId: 27,
        },
      },
    ],
  },
];
src/store/index.js
@@ -1,29 +1,35 @@
import Vue from 'vue';
import Vuex from 'vuex';
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    token: localStorage.getItem('token') || sessionStorage.getItem('token') || '',
    userInfo: JSON.parse(localStorage.getItem('userInfo')) || {}
    permissions: JSON.parse(localStorage.getItem("permissions")) || [],
    token:
      localStorage.getItem("token") || sessionStorage.getItem("token") || "",
    userInfo: JSON.parse(localStorage.getItem("userInfo")) || {},
  },
  mutations: {
    SET_PERMISSON(state, arr) {
      state.permissions = arr;
      localStorage.setItem("permissions", JSON.stringify(arr));
    },
    setToken(state, token) {
      state.token = token;
      localStorage.setItem('token', token);
      localStorage.setItem("token", token);
    },
    clearToken(state) {
      state.token = '';
      state.userInfo = {}
      state.token = "";
      state.userInfo = {};
      localStorage.clear();
      sessionStorage.clear();
    },
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo;
      localStorage.setItem('userInfo', userInfo)
    }
      localStorage.setItem("userInfo", userInfo);
    },
  },
  actions: {},
  modules: {}
});
  modules: {},
});
src/utils/baseurl.js
@@ -1,7 +1,8 @@
const apiConfig = {
    // 开发环境
    development: {
        baseURL: "http://192.168.110.85:9000",
        // baseURL: "http://192.168.110.80:9000",
        baseURL: "http://221.182.45.100:9000",
        mapKey: "67968c82f27c7e2cb9f40c1a9aa3042b",
        secretKey: "37ce61ae86efa5ad82b649a277f5097c",
    },
src/utils/request.js
@@ -146,7 +146,7 @@
      Message({
        message: res.data.msg || '服务器错误',
        type: 'error',
        duration: 2000
        duration: 4000
      })
      return Promise.reject(res.data.data)
    }
src/view/404.vue
New file
@@ -0,0 +1,54 @@
<template>
  <div class="not-found">
    <div class="not-found-content">
      <!-- <img src="@/assets/404.png" alt="404" class="not-found-image"> -->
      <h1 class="not-found-title">404</h1>
      <p class="not-found-text">抱歉,您暂无该页面访问权限,请联系管理员开通</p>
    </div>
  </div>
</template>
<script>
export default {
  name: 'NotFound',
  methods: {
    goHome() {
      this.$router.push('/home');
    }
  }
}
</script>
<style lang="less" scoped>
.not-found {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f7fa;
  &-content {
    text-align: center;
    padding: 20px;
  }
  &-image {
    width: 300px;
    height: 300px;
    margin-bottom: 20px;
  }
  &-title {
    font-size: 72px;
    color: #409EFF;
    margin: 0;
    line-height: 1.2;
  }
  &-text {
    font-size: 20px;
    color: #606266;
    margin: 20px 0;
  }
}
</style>
src/view/car-manage/components/detailOrderModal.vue
@@ -5,7 +5,7 @@
            <el-radio-group v-model="tabPosition" style="margin-bottom: 30px;">
                <el-radio-button label="order">订单信息</el-radio-button>
                <el-radio-button label="track">行程轨迹</el-radio-button>
                <el-radio-button label="monitoring">行程监控</el-radio-button>
                <!-- <el-radio-button label="monitoring">行程监控</el-radio-button> -->
            </el-radio-group>
            <!-- 订单信息 -->
            <div v-show="tabPosition == 'order'">
@@ -32,10 +32,10 @@
                <div class="mapContainer" id="mapContainers"></div>
            </div>
            <!-- 行程监控 -->
            <div v-if="tabPosition == 'monitoring'">
            <!-- <div v-if="tabPosition == 'monitoring'">
                <PlayLive :serverIp="monitoringData.serverIp" :serverPort="monitoringData.serverPort"
                    :carId="orderData.carId" />
            </div>
                    :urlLink="monitoringData.url" :carId="orderData.carId" />
            </div> -->
        </el-dialog>
    </div>
</template>
@@ -66,7 +66,7 @@
    methods: {
        initData(orderData = {}, monitoringData = {}, travelData = []) {
            console.log('////////////////////////');
            this.orderData = orderData
            this.monitoringData = monitoringData
            this.travelData = travelData
src/view/car-manage/detail.vue
@@ -13,8 +13,8 @@
                        <el-descriptions-item label="车辆营运类型">{{ detail.operateType }}</el-descriptions-item>
                        <el-descriptions-item label="所属车主">{{ detail.driverName }}</el-descriptions-item>
                        <el-descriptions-item label="联系电话">{{ detail.driverPhone }}</el-descriptions-item>
                        <el-descriptions-item label="经营区域">{{ detail.operatingArea }}</el-descriptions-item>
                        <el-descriptions-item label="车辆年度审验">{{ detail.annualReviewStatus }}</el-descriptions-item>
                        <!-- <el-descriptions-item label="经营区域">{{ detail.operatingArea }}</el-descriptions-item> -->
                        <!-- <el-descriptions-item label="车辆年度审验">{{ detail.annualReviewStatus }}</el-descriptions-item> -->
                        <el-descriptions-item label="驾驶证" v-if="detail.drivingLicense">
                            <img class="img-size" :src="detail.drivingLicense"></img>
                        </el-descriptions-item>
@@ -22,24 +22,24 @@
                            <img class="img-size" :src="detail.travelLicense"></img>
                        </el-descriptions-item>
                    </el-descriptions>
                    <div class="info-btn" @click="showDetail">
                    <!-- <div class="info-btn" @click="showDetail">
                        车辆详细信息<i class="el-icon-arrow-right"></i>
                    </div>
                    </div> -->
                </div>
            </div>
            <div class="info-right flex2">
            <div class="info-right flex2" style="margin-top: 40px;">
                <div style="position: relative; width: 100%; height: 330px">
                    <div
                        style="width: 100%; height: 330px; border-radius: 9px; background: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column">
                        <video style="width: 100%; height: 330px; border-radius: 9px; display: none" id="monitoringCard"
                            ref="monitoringCard" :controls="false" autoPlay width="620">
                        <video style="width: 100%;border-radius: 9px; display: none" id="monitoringCard"
                            ref="monitoringCard" :controls="false" autoplay>
                        </video>
                        <el-empty description="暂无视频信息" :image-size="80"></el-empty>
                    </div>
                    <canvas id="myCanvas" style="display:none"></canvas>
                    <el-button type="default" style="position: absolute; top: 10px;right: 60px;"
                        @click="goBack()">查看回放</el-button>
                    <div style="position: absolute; right: 11px; top: 10px">
                    <!-- <el-button type="default" style="position: absolute; top: 20px;right: 60px;"
                        @click="goBack()">查看回放</el-button> -->
                    <div style="position: absolute; right: 11px; top: 20px">
                        <div style="display: flex;flex-direction: column;align-items: center;justify-content: center;
              background: #ffffff; padding: 3px 10px; border-radius: 6px;margin-bottom: 10px;" @click="fullScreen()">
                            <img style="width: 20px; height: 20px" :src="require('../../assets//homeImg/full.png')" />
@@ -233,6 +233,7 @@
import DetailOrderModal from "./components/detailOrderModal.vue";
import { getCarDetail, getCarOrder, getCarWarning, getCarTrack, getCarVideo, getDetail, getOrderInfo, getOrderTravel, getOrderMonitoring, playDetection, closeRealVideo } from './service'
import moment from "moment";
import flvjs from "flv.js";
export default {
    name: "detail",
    components: { DetailModal, DetailOrderModal },
@@ -255,11 +256,13 @@
            showWarnDetail: false,
            info: {},
            activeInfo: {},
            map: null
            map: null,
            carId: null
        }
    },
    mounted() {
        if (this.$route.query.id) {
            this.carId = this.$route.query.id
            getCarDetail({ id: this.$route.query.id }).then(res => {
                this.detail = res;
                this.getList(res.vehicleNumber);
@@ -274,7 +277,7 @@
    },
    destroyed() {
    beforeDestroy() {
        this.destroyPlayer();
    },
    methods: {
@@ -299,14 +302,15 @@
            // 检查flv.js是否支持
            if (flvjs.isSupported()) {
                try {
                    playDetection(this.carId).then((res) => {
                    playDetection(this.$route.query.id).then((res) => {
                        this.flvPlayer = flvjs.createPlayer({
                            type: "flv", //视频类型
                            isLive: true, //是否为直播
                            cors: true, //是否开启跨域
                            hasAudio: false, //是否开启音频
                            hasVideo: true, //是否开启视频
                            url: `http://${this.videoObj.serverIp}:${this.videoObj.serverPort}/live?port=1935&app=flv&stream=${this.$route.query.id}`, // 后端拿到的视频路径
                            // url: `http://${this.videoObj.serverIp}:${this.videoObj.serverPort}/live?port=1935&app=flv&stream=${this.$route.query.id}`, // 后端拿到的视频路径
                            url: this.videoObj.url, // 后端拿到的视频路径
                            enableWorker: true, //启用 Web Worker 进程来加速视频的解码和处理过程
                            enableStashBuffer: false, // 启用数据缓存机制,提高视频的流畅度和稳定性。
                            stashInitialSize: 1024, // 初始缓存大小。单位:字节。建议针对直播:调整为1024kb
@@ -331,7 +335,7 @@
                                }
                                this.videoTimer = setInterval(() => {
                                    playDetection(this.carId);
                                    playDetection(this.$route.query.id);
                                }, 5000);
                            })
                            .catch((err) => {
@@ -343,7 +347,7 @@
                        });
                    });
                } catch (error) {
                    console.error("创建播放器失败:", error);
                    console.log("创建播放器失败:", error);
                }
            } else {
                console.error("当前浏览器不支持flv.js");
@@ -430,13 +434,13 @@
        showDetails(row) {
            this.loading = true
            Promise.all([getOrderInfo(row.id), getOrderTravel({ id: row.id })]).then(res => {
                getOrderMonitoring({ id: row.id }).then(resp => {
                    this.$refs.detailOrder.initData(res[0], resp, res[1])
                    this.loading = false
                }).catch(err => {
                    this.$refs.detailOrder.initData(res[0], {}, res[1])
                    this.loading = false
                })
                // getOrderMonitoring({ id: row.id }).then(resp => {
                //     this.$refs.detailOrder.initData(res[0], resp, res[1])
                //     this.loading = false
                // }).catch(err => {
                this.$refs.detailOrder.initData(res[0], {}, res[1])
                this.loading = false
                // })
            }).catch(err => {
                this.loading = false
            })
@@ -519,8 +523,8 @@
                    pageCurr: 1,
                    pageSize: 10,
                    total: 0,
                    startTime: moment(this.searchForm.date[0]).format('YYYY-MM-DD 00:00:00'),
                    endTime: moment(this.searchForm.date[1]).format('YYYY-MM-DD 23:59:59'),
                    startTime: new Date(this.searchForm.date[0]).getTime() / 1000,
                    endTime: new Date(this.searchForm.date[1]).getTime() / 1000,
                }
                getCarTrack({ ...this.searchForm, vehicleNumber: this.detail.vehicleNumber }).then(res => {
                    this.routeList = res;
@@ -661,6 +665,7 @@
.info-content {
    padding: 30px;
    margin-bottom: 30px;
}
::v-deep .el-descriptions-item__container {
src/view/car-manage/index.vue
@@ -32,7 +32,7 @@
                    </el-form-item>
                    <el-form-item label="车辆运营类型:" prop="operateType" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.operateType" placeholder="请选择">
                            <el-option v-for="(item,index) in options" :key="index" :label="item.name" :value="item.id"></el-option>
                            <el-option v-for="(item,index) in options" :key="index" :label="item.name" :value="item.name"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item label="车辆状态:" prop="status" class="unset_m" style="margin-right: 15px;">
@@ -76,7 +76,7 @@
                </el-table-column>
                <el-table-column label="操作" width="100">
                    <template slot-scope="scope">
                        <el-button type="text" @click="handle(scope.$index, scope.row)">详情</el-button>
                        <el-button type="text" v-permission="31" @click="handle(scope.$index, scope.row)">详情</el-button>
                    </template>
                </el-table-column>
            </el-table>
@@ -118,6 +118,8 @@
        };
    },
    created() {
        this.$checkPermission(30)
        getCarType().then(res => {
            this.options = res;
        });
@@ -125,7 +127,7 @@
        const query = this.$route.query;
        if (query && Object.keys(query).length > 0) {
            if(query.id){
                this.searchForm.operateType = Number(query.id);
                this.searchForm.operateType = query.id;
            }
        }
        this.getList();
src/view/car-manage/service.js
@@ -57,8 +57,6 @@
export const getOrderMonitoring = (params) => {
    return axios.get(`/system/order/getOrderMonitoring`, { params })
}
// 通知后端开始获取视频流
export const playDetection = (id) => {
    return axios.get(`/system/car/playDetection/${id}`)
@@ -67,4 +65,4 @@
// 通知后端开始关闭视频流
export const closeRealVideo = (id) => {
    return axios.get(`/system/car/closeRealVideo/${id}`)
}
}
src/view/car-type/index.vue
@@ -3,18 +3,9 @@
        <div class="form flex a-center j-between mt--23">
            <div class="form-left ml--30">
                <el-form :inline="true" :model="searchForm" class="demo-form-inline">
                    <el-form-item label="类型名称:" prop="level" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.level" placeholder="请输入"></el-input>
                    </el-form-item>
                    <!-- <el-form-item label="驾驶员手机号:" prop="name" class="unset_m" style="margin-right: 15px;">
                    <el-form-item label="类型名称:" prop="name" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.name" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="驾驶员姓名:" prop="name" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.name" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="紧急联系人姓名:" prop="date" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.name" placeholder="请输入"></el-input>
                    </el-form-item> -->
                </el-form>
            </div>
            <div class="form-right mr--24 mb--22">
@@ -26,29 +17,19 @@
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height">
                <el-table-column prop="date" label="序号"></el-table-column>
                <el-table-column prop="name" label="图标">
                <el-table-column type="index" label="序号" width="60"></el-table-column>
                <el-table-column prop="icon" label="图标">
                    <template slot-scope="scope">
                        <img :src="scope.row.img" alt="" style="width: 40px;height: 40px;" />
                        <img :src="scope.row.icon" alt="" style="width: 40px;height: 40px;" />
                    </template>
                </el-table-column>
                <el-table-column prop="name" label="类型"></el-table-column>
                <el-table-column prop="name" label="车辆数"></el-table-column>
                <!-- <el-table-column prop="name" label="驾驶员通信地址"></el-table-column>
                <el-table-column prop="name" label="紧急情况联系人"></el-table-column>
                <el-table-column prop="name" label="紧急情况联系人电话"></el-table-column>
                <el-table-column prop="name" label="驾驶员手机号"></el-table-column>
                <el-table-column prop="name" label="驾驶员合同签订公司"></el-table-column>
                <el-table-column prop="name" label="操作">
                    <template slot-scope="scope">
                        <el-button @click="showDetail">详情</el-button>
                    </template>
                </el-table-column> -->
                <el-table-column prop="carNum" label="车辆数"></el-table-column>
            </el-table>
            <div class="relative mt--23 flex j-end">
                <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
                    :current-page="searchForm.page" background layout="total,sizes,prev, pager, next,jumper"
                    :total="searchForm.total">
                    :current-page="currentPage" :page-size="pageSize" background
                    layout="total,sizes,prev, pager, next,jumper" :total="total">
                </el-pagination>
            </div>
        </div>
@@ -56,34 +37,66 @@
</template>
<script>
import { getCarTypeList } from './service'
export default {
  data() {
    return {
      searchForm: {
        total: 0,
        page: 1,
        pageSize: 10,
        name: '',
      },
      tableData: [],
      currentPage: 1,
      pageSize: 10,
      total: 0,
    };
  },
  computed: {
    height() {
      return this.$baseTableHeight();
    },
    }
  },
  created() {
    this.getList();
  },
  methods: {
    getList() {
      const params = {
        name: this.searchForm.name,
        pageCurr: this.currentPage,
        pageSize: this.pageSize,
      };
      getCarTypeList(params).then(res => {
        if (res && res.records) {
          this.tableData = res.records;
          this.total = res.total;
        } else {
          // 如果返回的是完整数组而不是分页对象,直接使用
          this.tableData = res || [];
          this.total = (res || []).length;
        }
      });
    },
    showDetail() {
      this.$refs.detailModal.dialogVisible = true;
    },
    reset() {},
    search() {},
    handleSizeChange(e) {
      this.searchForm.pageSize = e;
    reset() {
      this.searchForm.name = '';
      this.currentPage = 1;
      this.getList();
    },
    handleCurrentChange(e) {
      this.searchForm.page = e;
    search() {
      this.currentPage = 1;
      this.getList();
    },
    handleSizeChange(size) {
      this.pageSize = size;
      this.currentPage = 1;
      this.getList();
    },
    handleCurrentChange(page) {
      this.currentPage = page;
      this.getList();
    },
  },
};
src/view/car-type/service.js
New file
@@ -0,0 +1,5 @@
import axios from '@/utils/request';
// 获取车辆类型列表数据
export const getCarTypeList = (params) => {
    return axios.get('/system/car/getCarTypeList', {params})
}
src/view/company/component/detailModal.vue
@@ -1,35 +1,37 @@
<template>
    <div>
        <el-dialog title="公司信息" :visible.sync="dialogVisible" width="50%" :modal-append-to-body="false">
            <el-descriptions title="" :column="2">
                <el-descriptions-item label="公司名称">射洪洪达出租车有限公司</el-descriptions-item>
                <el-descriptions-item label="统一社会信用代码">91510922769973987B</el-descriptions-item>
                <el-descriptions-item label="注册地址">四川省遂宁市射洪市太和街道万洪二路33、35、37号</el-descriptions-item>
                <el-descriptions-item label="紧急联系电话">14725836902</el-descriptions-item>
        <el-dialog title="公司信息" :visible.sync="dialogVisible" width="50%" :modal-append-to-body="false" @open="open">
            <el-descriptions title="" :column="2" class="right-label-descriptions">
                <el-descriptions-item label="公司名称">{{ companyData.name }}</el-descriptions-item>
                <el-descriptions-item label="统一社会信用代码">{{ companyData.creditIdentifier }}</el-descriptions-item>
                <el-descriptions-item label="注册地址">{{ companyData.mailingAddress }}</el-descriptions-item>
                <el-descriptions-item label="紧急联系电话">{{ companyData.emergencyNumber }}</el-descriptions-item>
            </el-descriptions>
            <el-descriptions title="" :column="1">
                <el-descriptions-item
                    label="经营范围名称">巡游出租汽车经营服务。(依法须经批准的项目,经相关部门批准后方可开展经营活动,具体经营项目以相关部门批准文件或许可证件为准)一般项目:代驾服务;商务代理代办服务;小微型客车租赁经营服务;信息系统集成服务;汽车零配件批发。(除依法须经批准的项目外,凭营业执照依法自主开展经营活动)</el-descriptions-item>
                <el-descriptions-item label="通讯地址">四川省遂宁市射洪市太和街道万洪二路33、35、37号</el-descriptions-item>
            <el-descriptions title="" :column="1" class="right-label-descriptions">
                <el-descriptions-item label="经营范围名称">{{ companyData.businessScope }}</el-descriptions-item>
                <el-descriptions-item label="通讯地址">{{ companyData.mailingAddress }}</el-descriptions-item>
            </el-descriptions>
            <el-descriptions title="" :column="2">
                <el-descriptions-item label="经营业户经济类型">有限责任公司(非自然人投资或控股的法人独资)</el-descriptions-item>
                <el-descriptions-item label="注册资本">180万(元)</el-descriptions-item>
                <el-descriptions-item label="法人代表姓名">王强</el-descriptions-item>
                <el-descriptions-item label="法人代表电话">14725836905</el-descriptions-item>
                <el-descriptions-item label="状态">有效</el-descriptions-item>
            <el-descriptions title="" :column="2" class="right-label-descriptions">
                <el-descriptions-item label="经营业户经济类型">{{ companyData.operationType }}</el-descriptions-item>
                <el-descriptions-item label="注册资本">{{ companyData.registeredCapital }}</el-descriptions-item>
                <el-descriptions-item label="法人代表姓名">{{ companyData.legalRepresentative }}</el-descriptions-item>
                <el-descriptions-item label="法人代表电话">{{ companyData.contactNumber }}</el-descriptions-item>
                <el-descriptions-item label="状态">{{ companyData.status === 1 ? '有效' : '无效' }}</el-descriptions-item>
                <el-descriptions-item label="联系人">{{ companyData.contactPerson }}</el-descriptions-item>
            </el-descriptions>
        </el-dialog>
    </div>
</template>
<script>
// import { getEnterpriseInfo } from '../service'
export default {
    components: {},
    props: {},
    data() {
        return {
            dialogVisible: false
            dialogVisible: false,
            companyData: {}
        };
    },
    computed: {},
@@ -39,6 +41,9 @@
    methods: {
        closeClick() {
            this.dialogVisible = false
        },
        open() {
            console.log(JSON.stringify(this.companyData));
        }
    },
};
@@ -47,4 +52,26 @@
::v-deep .el-descriptions .el-descriptions-item__cell {
    padding-bottom: 25px;
}
::v-deep .el-descriptions-item__label {
    width: 140px !important;
    text-align: right !important;
    justify-content: flex-end !important;
    padding-right: 16px !important;
}
::v-deep .el-descriptions__label {
    width: 140px !important;
    text-align: right !important;
}
::v-deep .el-descriptions__table td.el-descriptions-item__label {
    width: 140px !important;
    text-align: right !important;
}
/* 对单列描述项设置更宽的标签宽度 */
::v-deep .el-descriptions[class*="right-label-descriptions"][column="1"] .el-descriptions-item__label {
    width: 160px !important;
}
</style>
src/view/company/index.vue
@@ -3,23 +3,26 @@
        <div class="form flex a-center j-between mt--23">
            <div class="form-left ml--30">
                <el-form :inline="true" :model="searchForm" class="demo-form-inline">
                    <el-form-item label="公司名称:" prop="level" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.level" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="经营范围:" prop="name" class="unset_m" style="margin-right: 15px;">
                    <el-form-item label="公司名称:" prop="name" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.name" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="通讯地址:" prop="name" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.name" placeholder="请输入"></el-input>
                    <el-form-item label="经营范围:" prop="businessScope" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.businessScope" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="法人代表:" prop="date" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.name" placeholder="请输入"></el-input>
                    <el-form-item label="通讯地址:" prop="mailingAddress" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.mailingAddress" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="车辆运营类型:" prop="type" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.type" placeholder="请选择">
                            <el-option label="公交" value="1"></el-option>
                            <el-option label="出租车" value="2"></el-option>
                            <el-option label="网约车" value="3"></el-option>
                    <el-form-item label="法人代表:" prop="legalRepresentative" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.legalRepresentative" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="车辆运营类型:" prop="operationType" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.operationType" placeholder="请选择">
                            <el-option label="公交" value="公交车"></el-option>
                            <el-option label="出租车" value="出租车"></el-option>
                            <el-option label="网约车" value="网约车"></el-option>
                            <el-option label="客运" value="客运"></el-option>
                            <el-option label="郊游" value="郊游"></el-option>
                            <el-option label="危险品" value="危险品"></el-option>
                        </el-select>
                    </el-form-item>
                </el-form>
@@ -33,26 +36,26 @@
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height">
                <el-table-column prop="date" label="序号"></el-table-column>
                <el-table-column type="index" label="序号"></el-table-column>
                <el-table-column prop="name" label="公司名称"></el-table-column>
                <el-table-column prop="name" label="经营范围"></el-table-column>
                <el-table-column prop="name" label="运营类型"></el-table-column>
                <el-table-column prop="name" label="通讯地址"></el-table-column>
                <el-table-column prop="name" label="注册资本"></el-table-column>
                <el-table-column prop="name" label="法定代表人"></el-table-column>
                <el-table-column prop="name" label="联系人"></el-table-column>
                <el-table-column prop="name" label="联系电话"></el-table-column>
                <el-table-column prop="businessScope" label="经营范围"></el-table-column>
                <el-table-column prop="operationType" label="运营类型"></el-table-column>
                <el-table-column prop="mailingAddress" label="通讯地址"></el-table-column>
                <el-table-column prop="registeredCapital" label="注册资本"></el-table-column>
                <el-table-column prop="legalRepresentative" label="法定代表人"></el-table-column>
                <el-table-column prop="contactPerson" label="联系人"></el-table-column>
                <el-table-column prop="contactNumber" label="联系电话"></el-table-column>
                <el-table-column prop="name" label="操作">
                    <template slot-scope="scope">
                        <el-button @click="showDetail">详情</el-button>
                        <el-button v-permission="29" @click="showDetail(scope.row)" size="small" type="primary">详情</el-button>
                    </template>
                </el-table-column>
            </el-table>
            <div class="relative mt--23 flex j-end">
                <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
                    :current-page="searchForm.page" background layout="total,sizes,prev, pager, next,jumper"
                    :total="searchForm.total">
                    :current-page="currentPage" :page-size="pageSize" background
                    layout="total,sizes,prev, pager, next,jumper" :total="total">
                </el-pagination>
            </div>
        </div>
@@ -62,6 +65,7 @@
<script>
import DetailModal from "./component/detailModal"
import { getEnterpriseList } from './service'
export default {
    components: {
@@ -70,11 +74,16 @@
    data() {
        return {
            searchForm: {
                total: 0,
                page: 1,
                pageSize: 10
                name: '',
                businessScope: '',
                mailingAddress: '',
                legalRepresentative: '',
                operationType: ''
            },
            tableData: [],
            currentPage: 1,
            pageSize: 10,
            total: 0
        };
    },
    computed: {
@@ -82,20 +91,51 @@
            return this.$baseTableHeight()
        },
    },
    created() {
        this.$checkPermission(28)
        this.getList()
    },
    methods: {
        showDetail() {
            this.$refs.detailModal.dialogVisible = true
        getList() {
            const params = {
                ...this.searchForm,
                pageCurr: this.currentPage,
                pageSize: this.pageSize
            };
            getEnterpriseList(params).then(res => {
                if (res && res.records) {
                    this.tableData = res.records;
                    this.total = res.total;
                }
            })
        },
        showDetail(row) {
            this.$refs.detailModal.dialogVisible = true;
            this.$refs.detailModal.companyData = row;
        },
        reset() {
            this.searchForm = {
                name: '',
                businessScope: '',
                mailingAddress: '',
                legalRepresentative: '',
                operationType: ''
            };
            this.currentPage = 1;
            this.getList();
        },
        search() {
            this.currentPage = 1;
            this.getList();
        },
        handleSizeChange(e) {
            this.searchForm.pageSize = e
        handleSizeChange(size) {
            this.pageSize = size;
            this.getList();
        },
        handleCurrentChange(e) {
            this.searchForm.page = e
        handleCurrentChange(page) {
            this.currentPage = page;
            this.getList();
        },
    }
}
src/view/company/service.js
New file
@@ -0,0 +1,9 @@
import axios from '@/utils/request';
// 获取公司列表数据
export const getEnterpriseList = (params) => {
    return axios.get('/system/enterprise/getEnterpriseList', {params})
}
// 获取公司详情
export const getEnterpriseInfo = (id) => {
    return axios.get(`/system/enterprise/getEnterpriseInfo/${id}`)
}
src/view/complaint/index.vue
@@ -32,10 +32,10 @@
        <div class="table-box-btn mt--23 ml--30">
            <el-button class="search-button h--40 w--90 fs--14" icon="el-icon-top" type="primary" size="small"
                @click="exportExcell">导出</el-button>
                @click="exportExcell" v-permission="14">导出</el-button>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height" :element-loading-text="'正在加载...'">
            <el-table :data="tableData" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
                <el-table-column type="index" width="55" label="序号"></el-table-column>
                <el-table-column prop="carName" label="车辆名称"></el-table-column>
                <el-table-column prop="vehicleNumber" label="车牌号码"></el-table-column>
@@ -87,6 +87,8 @@
        },
    },
    created() {
        this.$checkPermission(13)
        this.getTableList()
    },
    methods: {
src/view/early-warning/index.vue
@@ -55,7 +55,7 @@
        </div>
        <div class="table-box-btn mt--23 ml--30">
            <el-button class="search-button h--40 w--90 fs--14" icon="el-icon-top" type="primary" size="small"
            <el-button v-permission="11" class="search-button h--40 w--90 fs--14" icon="el-icon-top" type="primary" size="small"
                @click="exportExc">导出</el-button>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
@@ -66,8 +66,8 @@
                <el-table-column prop="vehicleNumber" label="车牌号码" fixed="left"></el-table-column>
                <!-- 其余列不固定,可滚动 -->
                <el-table-column prop="name" label="近15分钟情况">
                    <template #default="{ row }">
                        <img src="@/assets/homeImg/eye-fill.png" alt="" @click="viewDetail(row)"
                    <template #default="{ row }" >
                        <img v-permission="35" src="@/assets/homeImg/eye-fill.png" alt="" @click="viewDetail(row)"
                            style="width: 30px;cursor: pointer;">
                    </template>
                </el-table-column>
@@ -192,6 +192,7 @@
        };
    },
    created() {
        this.$checkPermission(10)
        this.fetchData()
    },
    methods: {
src/view/home/index.vue
@@ -6,12 +6,8 @@
      <div class="carCount">
        <div class="title">车辆统计</div>
        <div class="fir">
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(0, 3)"
            :key="item.id"
            @click="toCarManage(item.id)"
          >
          <div class="countCard" v-for="(item, index) in carCountData.slice(0, 3)" :key="item.id"
            @click="toCarManage(item.name)">
            <img class="iconImg" :src="imgList[index]" />
            <div>
              <div class="name">{{ item.name || "" }}(辆)</div>
@@ -20,12 +16,8 @@
          </div>
        </div>
        <div class="sec">
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(3, 7)"
            @click="toCarManage(item.id)"
            :key="item.id"
          >
          <div class="countCard" v-for="(item, index) in carCountData.slice(3, 7)" @click="toCarManage(item.name)"
            :key="item.id">
            <img class="iconImg" :src="imgList[index + 3]" />
            <div>
              <div class="name">{{ item.name || "" }}(辆)</div>
@@ -43,16 +35,9 @@
              <div class="name">在线</div>
              <div class="num">{{ carStatusData.online || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.onlinePercent"
              color="rgba(91, 143, 249, 1)"
              define-back-color="rgba(91, 143, 249, 0.25)"
              class="progressCard"
            ></el-progress>
            <el-progress type="circle" :width="20" :show-text="false" stroke-linecap="butt"
              :percentage="carStatusData.onlinePercent" color="rgba(91, 143, 249, 1)"
              define-back-color="rgba(91, 143, 249, 0.25)" class="progressCard"></el-progress>
          </div>
          <div class="statusLine"></div>
          <div class="statusCard">
@@ -60,16 +45,9 @@
              <div class="name">离线</div>
              <div class="num">{{ carStatusData.offline || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.offlinePercent"
              color="rgba(93, 112, 146, 1)"
              define-back-color="rgba(93, 112, 146, 0.25)"
              class="progressCard"
            ></el-progress>
            <el-progress type="circle" :width="20" :show-text="false" stroke-linecap="butt"
              :percentage="carStatusData.offlinePercent" color="rgba(93, 112, 146, 1)"
              define-back-color="rgba(93, 112, 146, 0.25)" class="progressCard"></el-progress>
          </div>
        </div>
        <div class="statusSec">
@@ -78,16 +56,9 @@
              <div class="name">故障</div>
              <div class="num">{{ carStatusData.breakdown || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.breakdownPercent"
              color="rgba(253, 83, 118, 1)"
              define-back-color="rgba(253, 83, 118, 0.25)"
              class="progressCard"
            ></el-progress>
            <el-progress type="circle" :width="20" :show-text="false" stroke-linecap="butt"
              :percentage="carStatusData.breakdownPercent" color="rgba(253, 83, 118, 1)"
              define-back-color="rgba(253, 83, 118, 0.25)" class="progressCard"></el-progress>
          </div>
          <div class="statusLine"></div>
          <div class="statusCard">
@@ -95,16 +66,9 @@
              <div class="name">异常</div>
              <div class="num">{{ carStatusData.abnormal || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.abnormalPercent"
              color="rgba(246, 189, 22, 1)"
              define-back-color="rgba(246, 189, 22, 0.25)"
              class="progressCard"
            ></el-progress>
            <el-progress type="circle" :width="20" :show-text="false" stroke-linecap="butt"
              :percentage="carStatusData.abnormalPercent" color="rgba(246, 189, 22, 1)"
              define-back-color="rgba(246, 189, 22, 0.25)" class="progressCard"></el-progress>
          </div>
        </div>
      </div>
@@ -136,18 +100,12 @@
      <div class="todayWarn">
        <div class="title">今日预警</div>
        <div class="warnList" v-if="warnList.length > 0">
          <div
            class="warnItem"
            v-for="(item, index) in warnList"
            :key="index"
            :class="
              item.warnLevel
                ? ['oneWarn', 'twoWarn', 'threeWarn', 'fourWarn'][
                    item.warnLevel - 1
                  ]
                : 'fiveWarn'
            "
          >
          <div class="warnItem" v-for="(item, index) in warnList" :key="index" :class="item.warnLevel
            ? ['oneWarn', 'twoWarn', 'threeWarn', 'fourWarn'][
            item.warnLevel - 1
            ]
            : 'fiveWarn'
            ">
            <div class="grade">
              {{
                item.warnLevel
@@ -179,10 +137,7 @@
        <div class="rankChart" id="rankChart">
          <div class="rankItem" v-for="(item, index) in rankList" :key="index">
            <div class="left">{{ item.name }}</div>
            <div
              class="rankRight"
              :class="[0, 1, 2].includes(index) ? 'rankColor' : ''"
            >
            <div class="rankRight" :class="[0, 1, 2].includes(index) ? 'rankColor' : ''">
              <div class="rank" :style="{ width: item.percentage + '%' }"></div>
            </div>
          </div>
@@ -200,6 +155,7 @@
import html2canvas from "html2canvas";
import AMapLoader from "@amap/amap-jsapi-loader";
import flvjs from "flv.js";
import moment from 'moment'
import {
  getCarCount,
  getCarStatusCount,
@@ -242,6 +198,7 @@
      serverIp: "", //监控ip
      serverPort: "", //监控端口
      carId: "", //监控车辆
      urlLink: '',//视频地址
    };
  },
  watch: {
@@ -283,6 +240,9 @@
    this.getWarnTop10Data();
    this.initMap();
    if (this.timer) {
      clearInterval(this.timer);
    }
    // 设置定时器,每分钟刷新一次数据
    this.timer = setInterval(() => {
      this.getCarCountData();
@@ -413,7 +373,7 @@
    // 获取预警列表数据
    async getWarnListData() {
      try {
        const res = await getCarWarnList();
        const res = await getCarWarnList({ pageNum: 1, pageSize: 100000,startTime:moment().format('YYYY-MM-DD 00:00:00'),endTime:moment().format('YYYY-MM-DD 23:59:59') });
        this.warnList = res.records;
      } catch (error) {
        this.$message.error("获取预警列表数据失败");
@@ -611,12 +571,13 @@
    },
    // 获取视频地址
    async getVideoUrl(carId) {
      this.carId = carId;
      try {
        const res = await getRealVideo({ id: carId });
        // 将RTSP流转换为FLV流
        this.serverIp = res.serverIp;
        this.serverPort = res.serverPort;
        this.carId = carId;
        this.urlLink = res.url;
      } catch (error) {
        console.error("获取视频地址失败", error);
        return {};
@@ -625,7 +586,7 @@
    // 初始化视频播放器
    initVideoPlayer() {
      console.log('11111',this.serverIp,'2222222222',this.serverPort)
      console.log('11111', this.serverIp, '2222222222', this.serverPort)
      // 先销毁之前的播放器
      if (this.flvPlayer) {
        this.flvPlayer.destroy();
@@ -649,7 +610,8 @@
              cors: true, //是否开启跨域
              hasAudio: false, //是否开启音频
              hasVideo: true, //是否开启视频
              url: `http://${this.serverIp}:${this.serverPort}/live?port=1935&app=flv&stream=${this.carId}`, // 后端拿到的视频路径
              // url: `http://${this.serverIp}:${this.serverPort}/live?port=1935&app=flv&stream=${this.carId}`, // 后端拿到的视频路径
              url: this.urlLink, // 后端拿到的视频路径
              enableWorker: true, //启用 Web Worker 进程来加速视频的解码和处理过程
              enableStashBuffer: false, // 启用数据缓存机制,提高视频的流畅度和稳定性。
              stashInitialSize: 1024, // 初始缓存大小。单位:字节。建议针对直播:调整为1024kb
@@ -672,7 +634,7 @@
                if (emptyElement) {
                  emptyElement.style.display = 'none';
                }
                this.videoTimer = setInterval(() => {
                  playDetection(this.carId);
                }, 5000);
@@ -694,6 +656,7 @@
    },
    destroyPlayer() {
      this.urlLink = ""
      // 销毁播放器释放资源
      if (this.flvPlayer) {
        if (this.videoTimer) clearInterval(this.videoTimer);
@@ -706,7 +669,7 @@
          // 恢复空状态的显示
          const video = document.getElementById("monitoringCard");
          if (video) {
          if (video) {
            video.style.display = 'none';
            const emptyElement = video.parentElement.querySelector('.el-empty');
            if (emptyElement) {
@@ -759,31 +722,24 @@
          </div>
        </div>
        <div style="display: flex;justify-content: space-between;margin-top: 15px;margin-bottom: 12px;">
          <div style="font-weight: 500;font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">车牌号:${
            record.vehicleNumber || ""
          }</div>
          <div style="font-weight: 500; font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">驾驶员:${
            record.driverName || ""
          }</div>
          <div style="font-weight: 500;font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">车牌号:${record.vehicleNumber || ""
        }</div>
          <div style="font-weight: 500; font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">驾驶员:${record.driverName || ""
        }</div>
        </div>
        <div style="display: flex; justify-content: space-between">
          <div style="font-weight: 500; font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;width: 200px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;" title="${
            record.location
          }">位置:${record.location}</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">经纬度:${
            record.longitude + "," + record.latitude
          }</div>
          <div style="font-weight: 500; font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;width: 200px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;" title="${record.location
        }">位置:${record.location}</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">经纬度:${record.longitude + "," + record.latitude
        }</div>
        </div>
        <div style="display: flex; justify-content: space-between">
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">当前时速:${
            record.speed || ""
          }${record.speed && "km/h"}</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">驾驶时长:${
            record.drivingTime
          }</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">当前时速:${record.speed || ""
        }${record.speed && "km/h"}</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">驾驶时长:${record.drivingTime
        }</div>
        </div>
        <div style="margin-top: 14px;display: flex;justify-content: flex-end;align-items: center;cursor: pointer;" onclick="toCarDetail(${
          record.id
        <div style="margin-top: 14px;display: flex;justify-content: flex-end;align-items: center;cursor: pointer;" onclick="toCarDetail(${record.id
        })">
          <div style="font-weight: 400;font-size: 18px; color: rgba(22, 119, 255, 1);line-height: 25px;">车辆详情</div>
          <img style="width:18px;height: 18px;margin-left: 8px;" src="${require("../../assets//homeImg/right.png")}" />
@@ -845,7 +801,7 @@
              overflow: 'truncate', // 截断模式
              ellipsis: true, // 超出显示省略号
              rotate: -40, // 可以根据需要调整角度
              formatter: function(value) {
              formatter: function (value) {
                if (value.length > 4) {
                  return value.substring(0, 4) + '...';
                }
@@ -959,7 +915,7 @@
  .leftMap {
    // width: 100%;
    height: 100%;
    flex: 1;
    flex: 3;
    display: flex;
    position: relative;
@@ -969,6 +925,7 @@
      height: 100%;
    }
  }
  .mapTop {
    z-index: 99;
    position: absolute;
@@ -977,7 +934,7 @@
    right: 513px;
    display: flex;
    justify-content: space-between;
    width: calc(100% - 570px);
    width: calc(100% - 770px);
    .title {
      font-weight: 600;
@@ -1026,11 +983,9 @@
      .countCard {
        flex: 1;
        background: linear-gradient(
          180deg,
          rgba(246, 246, 252, 0) 0%,
          #f3f4f8 100%
        );
        background: linear-gradient(180deg,
            rgba(246, 246, 252, 0) 0%,
            #f3f4f8 100%);
        box-shadow: inset 0px -1px 4px 0px #ffffff;
        border-radius: 10px;
        border: 1px solid #f1f1f1;
@@ -1158,13 +1113,15 @@
  }
  .right {
    width: 493px;
    // width: 493px;
    flex: 1;
    height: calc(100% - 20px);
    margin: 20px 17px 0 20px;
    background: #ffffff;
    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
    border-radius: 10px 10px 0px 0px;
    padding: 20px;
    overflow-y: auto;
    .title {
      margin-top: 30px;
@@ -1175,7 +1132,8 @@
      text-transform: none;
      margin-bottom: 10px;
    }
    .mt-0{
    .mt-0 {
      margin-top: 0 !important;
    }
@@ -1185,7 +1143,7 @@
      .companyCard {
        width: 140px;
        height: 90px;
        // height: 90px;
        background: #f4f4ff;
        border-radius: 0px 10px 10px 0px;
        position: relative;
@@ -1195,7 +1153,7 @@
          left: 0;
          top: 0;
          width: 4px;
          height: 90px;
          // height: 90px;
          background: #0e6efd;
          border-radius: 2px;
        }
@@ -1300,6 +1258,7 @@
            color: rgba(82, 196, 26, 1);
          }
        }
        .fiveWarn {
          background: rgba(214, 219, 228, 0.3);
@@ -1332,7 +1291,7 @@
      position: relative;
      #countChart {
        width: 453px;
        width: 100%;
        height: 180px;
      }
@@ -1353,8 +1312,8 @@
      position: relative;
      .rankChart {
        width: 453px;
        height: 300px;
        width: 100%;
        // height: 300px;
        .rankItem {
          display: flex;
src/view/home/service.js
@@ -13,7 +13,7 @@
}
// 获取车辆预警列表
export const getCarWarnList = (data) => {
    return axios.get('/system/warn/getCarWarnList', data)
    return axios.get('/system/warn/getCarWarnList', {params:data})
}
// 获取车辆预警情况统计
export const getWarnGroupCount = (data) => {
src/view/login/index.vue
@@ -1,54 +1,70 @@
<template>
  <div class="bgImg">
    <div class="login_box">
      <div class="fs--40 fw-bold">
        <h1>
          射洪“两客一危”监管平台
        </h1>
      <div class="logo">
        <img src="@/assets/logo.png" alt="" />
        <div class="fs--25 fw-bold" style="letter-spacing: 4px">
          射洪两客一危监管平台
        </div>
      </div>
      <div class="mt--20 txt-center py--20 px--20">
        <div class="fs--20 fw-bold">登陆</div>
      <div class="txt-center pb--20 px--20">
        <div class="mt--20">
          <el-input class="w100" prefix-icon="el-icon-user" placeholder="账号" v-model="username" />
          <el-input
            class="w100"
            prefix-icon="el-icon-user"
            placeholder="账号"
            v-model="username"
          />
        </div>
        <div class="mt--20">
          <el-input prefix-icon="el-icon-lock" placeholder="密码" v-model="password" show-password />
          <el-input
            prefix-icon="el-icon-lock"
            placeholder="密码"
            v-model="password"
            show-password
          />
        </div>
        <div class="mt--20 flex a-center code_box">
          <div class="flex1">
            <el-input placeholder="请输入验证码" v-model="code" />
          </div>
          <div @click="resetCodeStr" class="fs--18 lh--40 border1 pointer">{{ codeStr }}</div>
          <div @click="resetCodeStr" class="fs--18 lh--40 border1 pointer">
            {{ codeStr }}
          </div>
        </div>
        <el-button :loading="loginLoading" @click="login"
          class="mt--40 w100 br--5 pointer bgcolor1 color1">登录</el-button>
        <el-button
          :loading="loginLoading"
          @click="login"
          class="mt--40 w100 br--5 pointer bgcolor1 color1"
          >登录</el-button
        >
      </div>
    </div>
  </div>
</template>
<script>
import { generateVerificationCode, generateRandomString } from '@/utils/utils';
import { mapMutations } from 'vuex';
import { loginPwd } from './service'
import CryptoJS from 'crypto-js';
import {
  Message
} from 'element-ui'
import { generateVerificationCode, generateRandomString } from "@/utils/utils";
import { mapMutations } from "vuex";
import { loginPwd } from "./service";
import CryptoJS from "crypto-js";
import { getRoleInfo } from "@/view/systemManage/role/service";
import { Message } from "element-ui";
export default {
  components: {},
  props: {},
  data() {
    return {
      loginLoading: false,//登录loading
      username: '',
      password: '',
      code: '',
      codeStr: ''
      loginLoading: false, //登录loading
      username: "",
      password: "",
      code: "",
      codeStr: "",
    };
  },
  created() {
    this.resetCodeStr()
    this.resetCodeStr();
  },
  mounted() {
    document.addEventListener("keydown", this.handleKeyDown);
@@ -57,48 +73,53 @@
    document.removeEventListener("keydown", this.handleKeyDown);
  },
  methods: {
    ...mapMutations(['setToken', 'setUserInfo']),
    ...mapMutations(["setToken", "setUserInfo"]),
    login() {
      if (!this.rulesLogin()) return
      if (!this.rulesLogin()) return;
      this.loginLoading = true;
      loginPwd({
        username: this.username,
        password: CryptoJS.MD5(this.password).toString()
      }).then(res => {
        localStorage.setItem('client', generateRandomString(16));
        this.loginLoading = false;
        this.setToken(res.token.access_token);
        this.setUserInfo(JSON.stringify(res.info.sysUser));
        this.$router.push('/home')
      }).catch(() => {
        this.resetCodeStr()
        this.loginLoading = false;
        password: CryptoJS.MD5(this.password).toString(),
      })
        .then((res) => {
          localStorage.setItem("client", generateRandomString(16));
          this.loginLoading = false;
          this.setToken(res.token.access_token);
          this.setUserInfo(JSON.stringify(res.info.sysUser));
          getRoleInfo({ id: res.info.sysUser.roles[0].roleId }).then((res) => {
            this.$store.commit("SET_PERMISSON", res.menus);
            this.$router.push("/home");
          });
        })
        .catch(() => {
          this.resetCodeStr();
          this.loginLoading = false;
        });
    },
    rulesLogin() {
      if (!this.username) {
        Message({
          message: '请输入账号',
          type: 'warning',
          duration: 2000
        })
        return false
          message: "请输入账号",
          type: "warning",
          duration: 2000,
        });
        return false;
      }
      if (!this.password) {
        Message({
          message: '请输入密码',
          type: 'warning',
          duration: 2000
        })
        return false
          message: "请输入密码",
          type: "warning",
          duration: 2000,
        });
        return false;
      }
      if (!this.code) {
        Message({
          message: '请输入验证码',
          type: 'warning',
          duration: 2000
        })
        return false
          message: "请输入验证码",
          type: "warning",
          duration: 2000,
        });
        return false;
      }
      if (!this.matchCaseInsensitive(this.codeStr, this.code)) {
        Message({
@@ -106,19 +127,19 @@
          type: "warning",
          duration: 1500,
        });
        this.resetCodeStr()
        return false
        this.resetCodeStr();
        return false;
      }
      return true
      return true;
    },
    handleKeyDown(event) {
      if (event.key === "Enter") {
        this.login()
        this.login();
      }
    },
    // 校验验证码
    matchCaseInsensitive(str, pattern) {
      return str.match(new RegExp(pattern, "i"));
      return str.toLowerCase() === pattern.toLowerCase();
    },
    resetCodeStr() {
      this.codeStr = generateVerificationCode();
@@ -128,7 +149,7 @@
</script>
<style scoped lang="less">
.bgImg {
  background-image: url('../../assets/loginBG.png');
  background-image: url("../../assets/loginBG.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
@@ -137,6 +158,7 @@
}
.login_box {
  width: 25%;
  position: absolute;
  top: 50%;
  left: 50%;
@@ -145,11 +167,23 @@
  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
  border-radius: 10px;
  padding: 20px;
  .logo {
    display: flex;
    align-items: center;
    justify-content: center;
    img {
      width: 50px;
      height: 50px;
      margin-right: 15px;
    }
  }
}
.code_box {
  border-radius: 4px;
  border: 1px solid #DCDFE6;
  border: 1px solid #dcdfe6;
  ::v-deep .el-input__inner {
    border: unset !important;
@@ -157,7 +191,7 @@
}
.bgcolor1 {
  background: #0E6EFD;
  background: #0e6efd;
}
.color1 {
@@ -165,7 +199,7 @@
}
.border1 {
  border-left: 1px solid #DCDFE6;
  border-left: 1px solid #dcdfe6;
  min-width: 100px;
}
</style>
src/view/order/component/detailModal.vue
@@ -5,7 +5,7 @@
            <el-radio-group v-model="tabPosition" style="margin-bottom: 30px;">
                <el-radio-button label="order">订单信息</el-radio-button>
                <el-radio-button label="track">行程轨迹</el-radio-button>
                <el-radio-button label="monitoring">行程监控</el-radio-button>
                <!-- <el-radio-button label="monitoring">行程监控</el-radio-button> -->
            </el-radio-group>
            <!-- 订单信息 -->
            <div v-show="tabPosition == 'order'">
@@ -32,10 +32,10 @@
                <div class="mapContainer" id="mapContainer"></div>
            </div>
            <!-- 行程监控 -->
            <div v-if="tabPosition == 'monitoring'">
            <!-- <div v-if="tabPosition == 'monitoring'">
                <PlayLive :serverIp="monitoringData.serverIp" :serverPort="monitoringData.serverPort"
                    :carId="orderData.carId" />
            </div>
                    :urlLink="monitoringData.url" :carId="orderData.carId" />
            </div> -->
        </el-dialog>
    </div>
</template>
@@ -66,7 +66,7 @@
    methods: {
        initData(orderData = {}, monitoringData = {}, travelData = []) {
            this.orderData = orderData
            this.monitoringData = monitoringData
            // this.monitoringData = monitoringData
            this.travelData = travelData
            this.dialogVisible = true
        },
src/view/order/index.vue
@@ -46,11 +46,10 @@
        </div>
        <div class="mt--23 ml--30">
            <el-button class="search-button h--40 w--90 fs--14" icon="el-icon-top" type="primary" size="small"
                @click="exportExcell">导出</el-button>
                @click="exportExcell" v-permission="8">导出</el-button>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height"
                :element-loading-text="'正在加载...'">
            <el-table :data="tableData" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
                <el-table-column type="index" width="55" label="序号" />
                <el-table-column prop="code" label="订单编号" />
                <el-table-column prop="vehicleNumber" label="车牌号" />
@@ -69,7 +68,7 @@
                </el-table-column>
                <el-table-column label="操作">
                    <template #default="{ row }">
                        <el-button @click="showDetail(row)">详情</el-button>
                        <el-button v-permission="34" @click="showDetail(row)">详情</el-button>
                    </template>
                </el-table-column>
            </el-table>
@@ -111,6 +110,7 @@
        },
    },
    created() {
        this.$checkPermission(7)
        this.getTableList()
    },
    methods: {
@@ -159,13 +159,13 @@
        showDetail(row) {
            this.loading = true
            Promise.all([getOrderInfo(row.id), getOrderTravel({ id: row.id })]).then(res => {
                getOrderMonitoring({ id: row.id }).then(resp => {
                    this.$refs.detailModal.initData(res[0], resp, res[1])
                    this.loading = false
                }).catch(err => {
                // getOrderMonitoring({ id: row.id }).then(resp => {
                //     this.$refs.detailModal.initData(res[0], resp, res[1])
                //     this.loading = false
                // }).catch(err => {
                    this.$refs.detailModal.initData(res[0], {}, res[1])
                    this.loading = false
                })
                // })
            }).catch(err => {
                this.loading = false
            })
src/view/playback/index.vue
@@ -57,7 +57,11 @@
            </el-col>
            <el-col :span="20">
                <div class="grid-content" style="">
                   <PlayLive :serverPort="serverPort" :serverIp="serverIp" :carId="$route.query.id"/>
                    <PlayLive v-if="urlLink" :serverPort="serverPort" :serverIp="serverIp" :carId="$route.query.id"
                        :urlLink="urlLink" @video-error="handleVideoError" />
                    <div v-else class="empty-state">
                        <el-empty description="请选择时间范围并点击查询获取视频" :image-size="80"></el-empty>
                    </div>
                </div>
            </el-col>
        </el-row>
@@ -92,7 +96,8 @@
                }
            },
            serverIp: '',
            serverPort: ''
            serverPort: '',
            urlLink: ''
        }
    },
    created() {
@@ -143,6 +148,11 @@
                return;
            }
            // 重置视频链接
            this.urlLink = '';
            this.serverIp = '';
            this.serverPort = '';
            getPlaybackVideo({
                startTime: new Date(this.searchForm.startTime).getTime(),
                endTime: new Date(this.searchForm.endTime).getTime(),
@@ -151,7 +161,8 @@
                if (res && res.serverIp && res.serverPort) {
                    // 构建完整的视频流地址
                    this.serverIp = res.serverIp;
                    this.serverPort = res.serverPort
                    this.serverPort = res.serverPort;
                    this.urlLink = res.url;
                } else {
                    this.$message.error('未获取到视频地址');
                }
@@ -160,6 +171,10 @@
                this.$message.error('获取视频失败,请稍后重试');
            });
        },
        handleVideoError() {
            this.urlLink = '';
            this.$message.error('视频加载失败,请稍后重试');
        },
        resetForm() {
            this.searchForm.startTime = '';
            this.searchForm.endTime = '';
src/view/systemManage/driver/index.vue
@@ -25,7 +25,7 @@
            </div>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height" :element-loading-text="'正在加载...'">
            <el-table :data="tableData" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
                <el-table-column type="index" width="55" label="序号"></el-table-column>
                <el-table-column prop="driverName" label="机动车驾驶员姓名"></el-table-column>
                <el-table-column prop="enterpriseName" label="车辆所属公司"></el-table-column>
@@ -37,7 +37,7 @@
                <el-table-column prop="contractingCompany" label="驾驶员合同签订公司"></el-table-column>
                <el-table-column label="操作">
                    <template #default="{ row }">
                        <el-button @click="showDetail(row)">详情</el-button>
                        <el-button  v-permission="33" @click="showDetail(row)">详情</el-button>
                    </template>
                </el-table-column>
            </el-table>
@@ -76,6 +76,7 @@
        },
    },
    created() {
        this.$checkPermission(32)
        this.getTableList()
    },
    methods: {
src/view/systemManage/role/addEdit.vue
@@ -5,9 +5,19 @@
        <div></div>
        <div>角色信息</div>
      </div>
      <el-form ref="form" :inline="true" :model="form" :rules="rules" label-width="80px" class="demo-form-inline">
      <el-form
        ref="form"
        :inline="true"
        :model="form"
        :rules="rules"
        label-width="80px"
        class="demo-form-inline"
      >
        <el-form-item label="角色名称" prop="roleName">
          <el-input v-model="form.roleName" placeholder="请输入角色名称"></el-input>
          <el-input
            v-model="form.roleName"
            placeholder="请输入角色名称"
          ></el-input>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" placeholder="请输入备注"></el-input>
@@ -17,67 +27,18 @@
        <div></div>
        <div>操作权限</div>
      </div>
      <div>
        <div class="header">
          <div class="w20">模块名称</div>
          <div class="sconed">
            <div class="subpage">
              <div class="title">页面名称</div>
              <div class="btns">权限</div>
            </div>
          </div>
        </div>
        <div v-for="item in menu" :key="item.menuId">
          <div class="row">
            <div class="w20">
              <el-checkbox v-model="item.selected" @change="(e) => {
                setCheckStatus1(item.menuId, e)
              }" :checked="item.selected">
                {{ item.menuName }}
              </el-checkbox>
            </div>
            <div class="sconed">
              <div class="subpage"
                v-if="(item.children.length > 0 && item.children[0].children.length > 0) || item.children[0].children.menuType != 'F'">
                <div v-for="item1 in item.children" :key="item1.menuId" class="two">
                  <div class="left">
                    <el-checkbox v-model="item1.selected" @change="(e) => {
                      setCheckStatus2(item1.menuId, e, item.menuId)
                    }" :checked="item1.selected">
                      {{ item1.menuName }}
                    </el-checkbox>
                  </div>
                  <div class="right">
                    <div v-for="item2 in item1.children" :key="item2.menuId">
                      <el-checkbox v-model="item2.selected" @change="(e) => {
                        setCheckStatus3(item2.menuId, e, item1.menuId, item.menuId)
                      }" :checked="item2.selected">
                        {{ item2.menuName }}
                      </el-checkbox>
                    </div>
                  </div>
                </div>
              </div>
              <div class="subpage" v-else>
                <div class="two">
                  <!-- <div class="left">
                  </div> -->
                  <div class="right">
                    <div v-for="item1 in item.children" :key="item1.menuId">
                      <el-checkbox v-model="item1.selected" @change="(e) => {
                        setCheckStatus2(item1.menuId, e, item.menuId,)
                      }" :checked="item1.selected">
                        {{ item1.menuName }}
                      </el-checkbox>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      <div class="tree-container">
        <el-tree
          ref="permissionTree"
          :data="menu"
          :props="defaultProps"
          show-checkbox
          node-key="menuId"
          default-expand-all
          :check-strictly="true"
          @check="handleCheckChange"
        >
        </el-tree>
      </div>
      <div class="btn_box">
@@ -89,7 +50,7 @@
</template>
<script>
import { roleInfoFromUserId, getRoleInfo, add, edit } from './service.js'
import { roleInfoFromUserId, getRoleInfo, add, edit } from "./service.js";
export default {
  components: {},
  props: {},
@@ -105,28 +66,158 @@
        ],
      },
      menu: [],
      defaultProps: {
        children: "children",
        label: "menuName",
      },
    };
  },
  computed: {},
  watch: {},
  created() {
    roleInfoFromUserId().then(res => {
    roleInfoFromUserId().then((res) => {
      // 确保菜单数据正确处理
      this.menu = this.processMenuData(res);
      if (this.$route.query.roleId) {
        getRoleInfo({ id: this.$route.query.roleId }).then(resp => {
          this.menu = this.setSelectedIds(res.data.data, resp.data.data.menus || []);
        getRoleInfo({ id: this.$route.query.roleId }).then((resp) => {
          // 设置表单数据
          const roleData = resp;
          this.form = {
            roleName: resp.data.data.roleName,
            remark: resp.data.data.remark,
            roleId: resp.data.data.roleId
          }
        })
      } else {
        this.menu = res.data.data
            roleName: roleData.roleName,
            remark: roleData.remark,
            roleId: roleData.roleId,
          };
          // 设置选中的菜单项
          this.$nextTick(() => {
            if (roleData.menus && roleData.menus.length > 0) {
              // 直接使用后端返回的选中节点
              this.$refs.permissionTree.setCheckedKeys(roleData.menus);
            }
          });
        });
      }
    })
    });
  },
  mounted() { },
  mounted() {},
  methods: {
    // 处理菜单数据,为每个节点添加必要的属性
    processMenuData(menuData) {
      // 如果返回的是嵌套在data.data中的数据
      if (menuData.data && menuData.data.data) {
        menuData = menuData.data.data;
      }
      function traverse(item) {
        // 确保有children属性
        if (!item.children) {
          item.children = [];
        }
        // 递归处理子节点
        if (item.children && item.children.length > 0) {
          item.children.forEach(traverse);
        }
      }
      // 克隆一份数据,避免直接修改原数据
      const clonedData = JSON.parse(JSON.stringify(menuData));
      clonedData.forEach(traverse);
      return clonedData;
    },
    // 复选框状态变化事件处理
    handleCheckChange(data, checked) {
      // 获取树实例
      const tree = this.$refs.permissionTree;
      // 如果是父节点被点击
      if (data.children && data.children.length > 0) {
        // 获取当前节点的选中状态
        const isChecked = tree.getCheckedKeys().includes(data.menuId);
        if (isChecked) {
          // 如果父节点被选中,选中所有子节点
          const childIds = this.getAllChildIds(data);
          const currentCheckedKeys = tree.getCheckedKeys();
          const newCheckedKeys = [
            ...new Set([...currentCheckedKeys, ...childIds]),
          ];
          tree.setCheckedKeys(newCheckedKeys);
        } else {
          // 如果父节点被取消选中,取消选中所有子节点
          const childIds = this.getAllChildIds(data);
          const currentCheckedKeys = tree.getCheckedKeys();
          const newCheckedKeys = currentCheckedKeys.filter(
            (key) => !childIds.includes(key)
          );
          tree.setCheckedKeys(newCheckedKeys);
        }
      } else {
        // 如果是子节点被点击,检查父节点状态
        this.updateParentNodeState(data);
      }
    },
    // 获取节点的所有子节点ID
    getAllChildIds(node) {
      let ids = [];
      if (node.children && node.children.length > 0) {
        node.children.forEach((child) => {
          ids.push(child.menuId);
          ids = ids.concat(this.getAllChildIds(child));
        });
      }
      return ids;
    },
    // 更新父节点状态
    updateParentNodeState(node) {
      const tree = this.$refs.permissionTree;
      const parentNode = this.findParentNode(this.menu, node.menuId);
      if (parentNode) {
        const allChildren = this.getAllChildIds(parentNode);
        const checkedChildren = allChildren.filter((id) =>
          tree.getCheckedKeys().includes(id)
        );
        if (checkedChildren.length === 0) {
          // 如果没有子节点被选中,取消选中父节点
          tree.setChecked(parentNode.menuId, false);
        } else if (checkedChildren.length === allChildren.length) {
          // 如果所有子节点都被选中,选中父节点
          tree.setChecked(parentNode.menuId, true);
        } else {
          // 如果部分子节点被选中,设置父节点为半选状态
          tree.setChecked(parentNode.menuId, false);
          const node = tree.getNode(parentNode.menuId);
          if (node) {
            node.indeterminate = true;
          }
        }
        // 递归更新更上层的父节点
        this.updateParentNodeState(parentNode);
      }
    },
    // 查找节点的父节点
    findParentNode(nodes, targetId, parent = null) {
      for (let node of nodes) {
        if (node.menuId === targetId) {
          return parent;
        }
        if (node.children && node.children.length > 0) {
          const found = this.findParentNode(node.children, targetId, node);
          if (found) return found;
        }
      }
      return null;
    },
    setSelectedIds(arr, selectKeyList) {
      function traverse(item) {
        item.selected = selectKeyList.includes(item.menuId);
@@ -137,32 +228,44 @@
      arr.forEach(traverse);
      return arr;
    },
    onSubmit() {
      this.$refs['form'].validate((valid) => {
      this.$refs["form"].validate((valid) => {
        if (valid) {
          if (this.getSelectedIds(this.menu).length == 0) {
            this.$baseMessage('请勾选操作权限', 'warning')
            return
          // 获取选中的节点ID列表
          const checkedKeys = this.$refs.permissionTree.getCheckedKeys();
          const halfCheckedKeys =
            this.$refs.permissionTree.getHalfCheckedKeys();
          // 合并完全选中和半选中的节点
          const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
          if (allCheckedKeys.length === 0) {
            this.$message.warning("请勾选操作权限");
            return;
          }
          let obj = {
            ...this.form,
            menuIds: this.getSelectedIds(this.menu)
          }
            menuIds: allCheckedKeys,
          };
          if (this.$route.query && this.$route.query.roleId) {
            obj.roleId = this.$route.query.roleId
            obj.roleId = this.$route.query.roleId;
            edit(obj).then(() => {
              this.$baseMessage('保存成功', 'success')
              this.$router.go(-1)
            })
              this.$message.success("保存成功");
              this.$router.go(-1);
            });
          } else {
            add(obj).then(() => {
              this.$baseMessage('保存成功', 'success')
              this.$router.go(-1)
            })
              this.$message.success("保存成功");
              this.$router.go(-1);
            });
          }
        }
      })
      });
    },
    getSelectedIds(arr) {
      let result = [];
      function traverse(item) {
@@ -181,75 +284,6 @@
      }
      return result;
    },
    setCheckStatus1(id, status) { //点击第1级
      if (!status) {
        this.menu = this.menu.map(item => {
          if (item.menuId == id) {
            item.selected = status
            if (item.children.length > 0) {
              item.children = item.children.map(item1 => {
                item1.selected = status
                if (item1.children.length > 0) {
                  item1.children = item1.children.map(item2 => {
                    item2.selected = status
                    return { ...item2 }
                  })
                }
                return { ...item1 }
              })
            }
          }
          return { ...item }
        })
      } else {
        this.menu = this.menu.map(item => {
          if (item.menuId == id) {
            item.selected = true
          }
          return { ...item }
        })
      }
    },
    setCheckStatus2(id, status, aId) { //点击第2级
      this.menu = this.menu.map(item => {
        if (item.menuId == aId) {
          item.selected = true
          if (item.children.length > 0) {
            item.children = item.children.map(item1 => {
              if (item1.menuId == id) {
                item1.selected = status
              }
              return { ...item1 }
            })
          }
        }
        return { ...item }
      })
    },
    setCheckStatus3(id, status, bId, aId) {//点击第3级
      this.menu = this.menu.map(item => {
        if (item.menuId == aId) {
          item.selected = true
          if (item.children.length > 0) {
            item.children = item.children.map(item1 => {
              if (item1.menuId == bId) {
                item1.selected = true
                if (item1.children.length > 0) {
                  item1.children = item1.children.map(item2 => {
                    if (item2.menuId == id) {
                      item2.selected = status
                    }
                    return { ...item2 }
                  })
                }
              }
              return { ...item1 }
            })
          }
        }
        return { ...item }
      })
    }
  },
};
</script>
@@ -266,7 +300,7 @@
    div:first-child {
      width: 4px;
      height: 16px;
      background: #598DEC;
      background: #598dec;
      margin-right: 8px;
    }
  }
@@ -277,78 +311,36 @@
  }
}
.el-checkbox {
  display: flex;
  align-items: center;
}
.row,
.header {
  display: flex;
  align-items: center;
.tree-container {
  padding: 20px;
  border: 1px solid #e8e8e8;
  .w20 {
    width: 15%;
    padding: 8px 20px;
  }
  .sconed {
    flex: 1;
    .subpage {
      .title {
        border: 1px solid #e8e8e8;
        border-top: none;
        border-bottom: none;
      }
      .two {
        display: flex;
        align-items: center;
        border: 1px solid #e8e8e8;
        border-top: none;
        border-right: none;
        .left {
          width: 200px;
          padding: 13px 20px;
          border-right: 1px solid #e8e8e8;
        }
        .right {
          display: flex;
          flex: 1;
          div {
            padding: 13px 0 13px 20px;
          }
        }
      }
      .two:last-child {
        border-bottom: none;
      }
      .btns {
        display: flex;
        align-items: center;
        padding: 0 20px;
      }
    }
  }
  border-radius: 4px;
  margin: 10px 0;
  background-color: #fff;
  max-height: 600px;
  overflow-y: auto;
}
.header {
  background-color: #e8e8e8;
/* 树节点样式优化 */
::v-deep .el-tree-node__content {
  height: 40px;
  padding: 0 10px;
}
  .subpage {
    display: flex;
  }
::v-deep .el-tree-node__label {
  font-size: 14px;
}
  .title {
    width: 200px;
    padding: 8px 20px;
  }
::v-deep .el-tree-node.is-current > .el-tree-node__content {
  background-color: #f0f7ff;
}
/* 复选框样式优化 */
::v-deep .el-checkbox__inner {
  border-radius: 2px;
}
::v-deep .el-tree-node:hover > .el-tree-node__content {
  background-color: #f5f7fa;
}
</style>
src/view/systemManage/role/detail.vue
@@ -7,62 +7,23 @@
    <el-card>
      <el-tabs v-model="activeName">
        <el-tab-pane label="操作权限" name="first">
          <div class="header">
            <div class="w20">模块名称</div>
            <div class="sconed">
              <div class="subpage">
                <div class="title">页面名称</div>
                <div class="btns">权限</div>
              </div>
            </div>
          </div>
          <div v-for="item in menu" :key="item.menuId">
            <div class="row">
              <div class="w20">
                <el-checkbox disabled :checked="item.selected">
                  {{ item.menuName }}
                </el-checkbox>
              </div>
              <div class="sconed">
                <div class="subpage"
                  v-if="(item.children.length > 0 && item.children[0].children.length > 0) || item.children[0].children.menuType != 'F'">
                  <div v-for="item1 in item.children" :key="item1.menuId" class="two">
                    <div class="left">
                      <el-checkbox disabled :checked="item1.selected">
                        {{ item1.menuName }}
                      </el-checkbox>
                    </div>
                    <div class="right">
                      <div v-for="item2 in item1.children" :key="item2.menuId">
                        <el-checkbox disabled :checked="item2.selected">
                          {{ item2.menuName }}
                        </el-checkbox>
                      </div>
                    </div>
                  </div>
                </div>
                <div class="subpage" v-else>
                  <div class="two">
                    <!-- <div class="left">
                    </div> -->
                    <div class="right">
                      <div v-for="item1 in item.children" :key="item1.menuId">
                        <el-checkbox disabled :checked="item1.selected">
                          {{ item1.menuName }}
                        </el-checkbox>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          <div class="tree-container">
            <el-tree
              ref="permissionTree"
              :data="menu"
              :props="defaultProps"
              show-checkbox
              node-key="menuId"
              default-expand-all
              :check-strictly="true"
              >
            </el-tree>
          </div>
        </el-tab-pane>
        <el-tab-pane label="人员列表" name="second">
          <el-form :inline="true" class="demo-form-inline">
            <div style="display: flex;justify-content: space-between;">
            <div style="display: flex;">
              <el-form-item label="人员搜索">
                <el-input v-model="nickNameOrPhone" placeholder="请输入姓名/联系电话"></el-input>
              </el-form-item>
@@ -137,6 +98,13 @@
      form: {},
      activeName: 'first',
      menu: [],
      defaultProps: {
        children: 'children',
        label: 'menuName',
        disabled: (data,node) => {
          return {...node,disabled:true}
        }
      },
      data: [],
      nickNameOrPhone: '',
      status: '',
@@ -160,15 +128,22 @@
  watch: {},
  created() {
    roleInfoFromUserId().then(res => {
      this.menu = this.processMenuData(res);
      getRoleInfo({ id: this.$route.query.roleId }).then(resp => {
        this.menu = this.setSelectedIds(res.data.data, resp.data.data.menus);
        this.form = {
          roleName: resp.data.data.roleName,
          remark: resp.data.data.remark,
        }
      })
    })
    this.getListData()
          roleName: resp.roleName,
          remark: resp.remark,
        };
        this.$nextTick(() => {
          if (resp.menus && resp.menus.length > 0) {
            this.$refs.permissionTree.setCheckedKeys(resp.menus);
          }
        });
      });
    });
    this.getListData();
  },
  mounted() { },
  methods: {
@@ -187,15 +162,21 @@
        this.listLoading = false
      }, 500)
    },
    setSelectedIds(arr, selectKeyList) {
    processMenuData(menuData) {
      function traverse(item) {
        item.selected = selectKeyList.includes(item.menuId);
        if (!item.children) {
          item.children = [];
        }
        if (item.children && item.children.length > 0) {
          item.children.forEach(traverse);
        }
      }
      arr.forEach(traverse);
      return arr;
      const clonedData = JSON.parse(JSON.stringify(menuData));
      clonedData.forEach(traverse);
      return clonedData;
    },
    reset() {
      this.nickNameOrPhone = ''
@@ -325,8 +306,31 @@
  }
}
/* 当复选框禁用时覆盖默认样式 */
.tree-container {
  padding: 20px;
  border: 1px solid #e8e8e8;
  border-radius: 4px;
  margin: 10px 0;
  background-color: #fff;
  max-height: 600px;
  overflow-y: auto;
}
/* 树节点样式优化 */
::v-deep .el-tree-node__content {
  height: 40px;
  padding: 0 10px;
}
::v-deep .el-tree-node__label {
  font-size: 14px;
}
::v-deep .el-tree-node.is-current > .el-tree-node__content {
  background-color: #f0f7ff;
}
/* 禁用状态下的复选框样式 */
::v-deep .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner {
  background-color: #409eff;
  border-color: #409eff;
@@ -335,4 +339,8 @@
::v-deep .el-checkbox.is-disabled .el-checkbox__inner::after {
  border-color: #fff;
}
::v-deep .el-tree-node__content:hover {
  background-color: #f5f7fa;
}
</style>
src/view/systemManage/role/index.vue
@@ -13,9 +13,9 @@
    </el-card>
    <el-card style="margin-top: 20px;">
      <div class="add_btn">
        <el-button icon="el-icon-plus" @click="add" type="primary">添加角色</el-button>
        <el-button v-permission="24" icon="el-icon-plus" @click="add" type="primary">添加角色</el-button>
      </div>
      <el-table ref="tableSort" v-loading="listLoading" :height="height" stripe :data="data"
      <el-table ref="tableSort" v-loading="listLoading" stripe :data="data"
        :element-loading-text="elementLoadingText">
        <el-table-column type="index" width="55" label="序号"></el-table-column>
        <el-table-column prop="roleName" label="角色名称"></el-table-column>
@@ -24,10 +24,10 @@
        <el-table-column label="操作" width="300">
          <template slot-scope="{row}">
            <div>
              <el-button type="text"
              <el-button v-permission="27" type="text"
                @click="$router.push(`/systemManage/role-detail?roleId=${row.roleId}`)">详情</el-button>
              <el-button type="text" @click="$router.push(`/systemManage/add-role?roleId=${row.roleId}`)">编辑</el-button>
              <el-button type="text" @click="del(row)">删除</el-button>
              <el-button v-if="row.roleId != 1" v-permission="25" type="text" @click="$router.push(`/systemManage/add-role?roleId=${row.roleId}`)">编辑</el-button>
              <el-button v-if="row.roleId != 1" v-permission="26" type="text" @click="del(row)">删除</el-button>
            </div>
          </template>
        </el-table-column>
@@ -74,6 +74,7 @@
  },
  watch: {},
  created() {
    this.$checkPermission(23)
    this.getListData()
  },
  mounted() { },
src/view/systemManage/user/index.vue
@@ -30,13 +30,13 @@
      </div>
    </div>
    <div class="table-box-btn mt--23 ml--30">
      <el-button class="search-button h--40 w--90 fs--14" icon="el-icon-plus" type="primary" size="small"
      <el-button v-permission="17" class="search-button h--40 w--90 fs--14" icon="el-icon-plus" type="primary" size="small"
        @click="dialogVisible = true">添加</el-button>
      <!-- <el-button class="search-button h--40 w--90 fs--14" icon="el-icon-delete" type="danger" size="small"
        @click="dialogVisible = true">删除</el-button> -->
    </div>
    <div class="table-box ml--30 mt--23 mr--30">
      <el-table :data="data" border stripe style="width: 100%" :height="height" :element-loading-text="'正在加载...'">
      <el-table :data="data" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
        <el-table-column prop="nickName" label="姓名"></el-table-column>
        <el-table-column prop="phonenumber" label="联系电话">
        </el-table-column>
@@ -66,11 +66,11 @@
        <el-table-column label="操作" width="300">
          <template #default="{ row }">
            <div v-if="row.userId != 1">
              <el-button type="text" @click="edit(row)">编辑</el-button>
              <el-button v-if="row.status != 0" type="text" @click="updateStatus(row, true)">启用</el-button>
              <el-button v-if="row.status == 0" type="text" @click="updateStatus(row, false)">禁用</el-button>
              <el-button type="text" @click="detail(row)">重置密码</el-button>
              <el-button type="text" @click="del(row)">删除</el-button>
              <el-button v-permission="19" type="text" @click="edit(row)">编辑</el-button>
              <el-button v-permission="20" v-if="row.status != 0" type="text" @click="updateStatus(row, true)">启用</el-button>
              <el-button v-permission="20" v-if="row.status == 0" type="text" @click="updateStatus(row, false)">禁用</el-button>
              <el-button v-permission="21" type="text" @click="detail(row)">重置密码</el-button>
              <el-button v-permission="18" type="text" @click="del(row)">删除</el-button>
            </div>
          </template>
        </el-table-column>
@@ -137,6 +137,8 @@
  },
  watch: {},
  created() {
    this.$checkPermission(16)
    this.getRoleList()
    this.getListData()
  },