Browse Source

更新 cam 相关页面

reghao 2 days ago
parent
commit
6ca1f39c83
6 changed files with 827 additions and 462 deletions
  1. 4 5
      src/api/disk.js
  2. 10 3
      src/router/disk.js
  3. 531 0
      src/views/disk/CamDetail.vue
  4. 151 0
      src/views/disk/CamList.vue
  5. 131 77
      src/views/disk/Disk.vue
  6. 0 377
      src/views/disk/DiskCam.vue

+ 4 - 5
src/api/disk.js

@@ -15,8 +15,7 @@ const diskApi = {
   getAlbumListApi: '/api/disk/album/list',
   getAlbumExcludeFilesApi: '/api/disk/album/exclude',
   getAlbumDetailApi: '/api/disk/album/detail',
-  getCamKeyValueApi: '/api/disk/cam/kv',
-  getCamDetailApi: '/api/disk/cam/detail',
+  getCamApi: '/api/disk/cam',
   createShareApi: '/api/disk/share/create',
   deleteShareApi: '/api/disk/share/delete',
   getShareListApi: '/api/disk/share/list',
@@ -103,12 +102,12 @@ export function getShareToList(shareId) {
   return get(diskApi.getShareToListApi + '?shareId=' + shareId)
 }
 
-export function getCamKeyValue() {
-  return get(diskApi.getCamKeyValueApi)
+export function getCamList() {
+  return get(diskApi.getCamApi + '/list')
 }
 
 export function getCamDetail(query) {
-  return get(diskApi.getCamDetailApi, query)
+  return get(diskApi.getCamApi + '/detail', query)
 }
 
 export function getRecordByMonth(query) {

+ 10 - 3
src/router/disk.js

@@ -5,7 +5,8 @@ const DiskAlbumEdit = () => import('views/disk/DiskAlbumEdit')
 const DiskFile = () => import('views/disk/DiskFile')
 const DiskShare = () => import('views/disk/DiskShare')
 const DiskRecycle = () => import('views/disk/DiskRecycle')
-const DiskCam = () => import('views/disk/DiskCam')
+const CamList = () => import('views/disk/CamList')
+const CamDetail = () => import('views/disk/CamDetail')
 
 export default {
   path: '/disk',
@@ -51,8 +52,14 @@ export default {
     },
     {
       path: '/disk/cam',
-      name: 'DiskCam',
-      component: DiskCam,
+      name: 'CamList',
+      component: CamList,
+      meta: { needAuth: true }
+    },
+    {
+      path: '/disk/cam/detail',
+      name: 'CamDetail',
+      component: CamDetail,
       meta: { needAuth: true }
     }
   ]

+ 531 - 0
src/views/disk/CamDetail.vue

@@ -0,0 +1,531 @@
+<template>
+  <div class="cam-detail-container">
+    <el-row :gutter="16">
+      <el-col :md="17" :sm="24">
+        <el-card class="video-main-card" shadow="never">
+          <div slot="header" class="card-header">
+            <div class="title-info">
+              <i class="el-icon-video-camera"></i>
+              <span class="cam-name">{{ camDetail ? camDetail.camName : '摄像头监控' }}</span>
+              <el-tag v-if="camDetail" :type="camDetail.onLive ? 'danger' : 'info'" size="mini" effect="dark" class="live-tag">
+                {{ camDetail.onLive ? '• LIVE 直播' : '回放中' }}
+              </el-tag>
+            </div>
+            <div class="header-actions">
+              <el-button icon="el-icon-share" type="primary" size="mini" round @click="onShareCam">分享</el-button>
+            </div>
+          </div>
+
+          <div class="video-content-wrapper">
+            <div class="video-container">
+              <video
+                id="videoElement"
+                controls
+                autoplay
+                muted
+                playsinline
+                webkit-playsinline
+                class="video-element"
+              />
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :md="7" :sm="24">
+        <el-card class="record-sidebar-card" shadow="never">
+          <div slot="header" class="sidebar-header">
+            <div class="sidebar-title">
+              <i class="el-icon-collection"></i>
+              <span>历史录像</span>
+            </div>
+            <el-button type="primary" icon="el-icon-date" size="mini" plain @click="onSelectDate">切换日期</el-button>
+          </div>
+
+          <div class="record-list-info">
+            <div class="date-display">
+              <i class="el-icon-time"></i>
+              <span>{{ getYearMonthDay(calendarDate) }}</span>
+              <small>的监控档案</small>
+            </div>
+
+            <el-table
+              :data="dataList"
+              size="small"
+              height="460px"
+              class="custom-table"
+              :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
+            >
+              <el-table-column prop="startTime" label="开始时间" width="90">
+                <template slot-scope="scope">
+                  <span class="time-text">{{ scope.row.startTime }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column prop="duration" label="时长" align="center">
+                <template slot-scope="scope">
+                  <el-tag size="mini" type="info" plain>{{ scope.row.duration }}s</el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="80" align="right">
+                <template slot-scope="scope">
+                  <el-button
+                    type="primary"
+                    circle
+                    size="mini"
+                    icon="el-icon-caret-right"
+                    @click="handlePlay(scope.row.recordId)"
+                  ></el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+
+            <div class="sidebar-footer">
+              <el-button type="warning" icon="el-icon-magic-stick" size="medium" class="full-width-btn" @click="onButtonSubmit">提交</el-button>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-dialog
+      title="选择监控日期"
+      append-to-body
+      :visible.sync="showCalenderDialog"
+      width="600px"
+      custom-class="custom-calendar-dialog"
+    >
+      <div class="calendar-legend">
+        <span class="legend-item"><i class="dot active"></i> 有录像</span>
+        <span class="legend-item"><i class="dot"></i> 无数据</span>
+      </div>
+      <el-calendar v-model="calendarDate">
+        <div slot="dateCell" slot-scope="{ date, data }" class="custom-date-cell" @click="handleCellClick(date, data)">
+          <span :class="{ 'has-record': dealMyDate(data.day) }">
+            {{ data.day.split("-").slice(2).join() }}
+          </span>
+          <div v-if="dealMyDate(data.day)" class="record-dot"></div>
+        </div>
+      </el-calendar>
+    </el-dialog>
+
+    <el-dialog
+      title="分享摄像头权限"
+      :visible.sync="showCreateShareDialog"
+      width="420px"
+      center
+    >
+      <div class="share-dialog-body">
+        <el-form ref="createAlbumForm" :model="createShareForm" label-position="top">
+          <el-form-item label="授权给联系人">
+            <div class="user-check-list">
+              <el-checkbox-group v-model="createShareForm.shareToList">
+                <el-checkbox v-for="user in userContactList" :key="user.userIdStr" :label="user.username" border size="small" class="user-checkbox" />
+              </el-checkbox-group>
+            </div>
+          </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button type="primary" block @click="createShare" style="width: 100%">确认分享</el-button>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import flvjs from 'flv.js'
+
+import { createShare, getCamDetail, getCamList, getRecordByMonth, getRecordUrl, submitActivity } from '@/api/disk'
+import { getUserContact } from '@/api/user'
+
+export default {
+  name: 'CamDetail',
+  data() {
+    return {
+      calendarDate: new Date(),
+      dateMap: new Map(),
+      query: {
+        camId: '',
+        yearMonth: '',
+        yearMonthDay: ''
+      },
+      dataList: [],
+      showCalenderDialog: false,
+      camDetail: null,
+      camRecordDetail: null,
+      // ****************************************************************************************************************
+      showCreateShareDialog: false,
+      userContactList: [],
+      createShareForm: {
+        albumType: 1,
+        albumId: null,
+        shareToList: []
+      },
+      flvPlayer: null
+    }
+  },
+  watch: {
+    $route() {
+      this.$router.go()
+    },
+    calendarDate: {
+      handler(newValue, oldValue) {
+        const oldMonth = this.getYearMonth(oldValue)
+        const newMonth = this.getYearMonth(newValue)
+        if (oldMonth !== newMonth) {
+          this.query.yearMonth = this.getYearMonth(newValue)
+          getRecordByMonth(this.query).then(resp => {
+            if (resp.code === 0) {
+              this.dateMap.clear()
+              for (const item of resp.data) {
+                const date1 = new Date(item)
+                const dayStr = this.getYearMonthDay(date1)
+                this.dateMap.set(dayStr, date1)
+              }
+              // 对 calendarDate 赋值重新渲染日历, 目的是调用 dealMyDate 处理当月的日期
+              this.calendarDate = new Date(newMonth)
+            }
+          }).catch(error => {
+            this.$message.error(error.message)
+          })
+        }
+      }
+    }
+  },
+  created() {
+    document.title = '摄像头监控'
+    this.query.camId = this.$route.query.camId
+    if (this.query.camId) {
+      this.getData()
+    } else {
+      this.$message.error('参数错误')
+      this.goBack()
+    }
+  },
+  methods: {
+    getData() {
+      getCamDetail(this.query).then(resp => {
+        if (resp.code === 0) {
+          this.camDetail = resp.data
+          if (this.camDetail.onLive) {
+            this.initVideoPlayer(this.camDetail.liveUrl, true)
+          } else {
+            this.initVideoPlayer(this.camDetail.url, false)
+          }
+        }
+      })
+    },
+    goBack() {
+      this.$router.go(-1)
+    },
+    handleCellClick(date, data) {
+      this.showCalenderDialog = false
+      this.query.yearMonthDay = this.getYearMonthDay(date)
+      getCamDetail(this.query).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.camDetail = respData
+          this.dataList = respData.dayRecords
+        }
+      })
+    },
+    // 渲染日历时会处理每个日期
+    dealMyDate(val) {
+      return this.dateMap.get(val) !== undefined
+    },
+    getYearMonth(date) {
+      const year = date.getFullYear().toString().padStart(4, '0')
+      const month = (date.getMonth() + 1).toString().padStart(2, '0')
+      // const day = date.getDate().toString().padStart(2, '0')
+      // const hour = date.getHours().toString().padStart(2, '0')
+      // const minute = date.getMinutes().toString().padStart(2, '0')
+      // const second = date.getSeconds().toString().padStart(2, '0')
+      // 2023-02-16 08:25:05
+      // console.log(`${year}-${month}-${day} ${hour}:${minute}:${second}`)
+      return year + '-' + month
+    },
+    getYearMonthDay(date) {
+      const year = date.getFullYear().toString().padStart(4, '0')
+      const month = (date.getMonth() + 1).toString().padStart(2, '0')
+      const day = date.getDate().toString().padStart(2, '0')
+      return year + '-' + month + '-' + day
+    },
+    handlePlay(recordId) {
+      getRecordUrl(recordId).then(resp => {
+        if (resp.code === 0) {
+          this.camRecordDetail = resp.data
+          this.initVideoPlayer(this.camRecordDetail.url, false)
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    initVideoPlayer(videoUrl, live) {
+      var videoElement = document.getElementById('videoElement')
+      if (!live) {
+        videoElement.src = videoUrl
+        return
+      }
+
+      if (!flvjs.isSupported()) {
+        this.$message.error('not support flv')
+        return
+      }
+
+      this.flvPlayer = flvjs.createPlayer({
+        type: 'flv',
+        isLive: true,
+        url: videoUrl,
+        duration: 0,
+        filesize: 0,
+        enableStashBuffer: false,
+        hasAudio: true,
+        hasVideo: true
+      })
+      this.flvPlayer.attachMediaElement(videoElement)
+      this.flvPlayer.load()
+      this.flvPlayer.play()
+
+      videoElement.addEventListener('progress', () => {
+        const end = this.flvPlayer.buffered.end(0) // 获取当前buffered值(缓冲区末尾)
+        const delta = end - this.flvPlayer.currentTime // 获取buffered与当前播放位置的差值
+        // 延迟过大,通过跳帧的方式更新视频
+        if (delta > 10 || delta < 0) {
+          this.flvPlayer.currentTime = this.flvPlayer.buffered.end(0) - 1
+          return
+        }
+
+        // 追帧
+        if (delta > 1) {
+          videoElement.playbackRate = 1.1
+        } else {
+          videoElement.playbackRate = 1
+        }
+      })
+      // 点击播放按钮后,更新视频
+      videoElement.addEventListener('play', () => {
+        if (this.flvPlayer.buffered.length > 0) {
+          const end = this.flvPlayer.buffered.end(0) - 1
+          this.flvPlayer.currentTime = end
+        }
+      })
+      // 网页重新激活后,更新视频
+      window.onfocus = () => {
+        const end = this.flvPlayer.buffered.end(0) - 1
+        this.flvPlayer.currentTime = end
+      }
+    },
+    onSelectDate() {
+      this.showCalenderDialog = true
+    },
+    onButtonSubmit() {
+      this.$confirm('确认提交?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        submitActivity().then(resp => {
+          this.$message.info(resp.msg)
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    },
+    onShareCam() {
+      this.createShareForm.albumId = this.camDetail.camId
+      getUserContact(1).then(resp => {
+        if (resp.code === 0) {
+          this.userContactList = resp.data
+          this.showCreateShareDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    createShare() {
+      createShare(this.createShareForm).then(resp => {
+        this.$message.info(resp.msg)
+      }).finally(() => {
+        this.showCreateShareDialog = false
+        this.createShareForm = {
+          albumType: 1,
+          albumId: null,
+          shareToList: []
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.cam-detail-container {
+  padding: 16px;
+  background-color: #f0f2f5;
+  min-height: 100vh;
+}
+
+/* 播放器卡片美化 */
+.video-main-card {
+  border-radius: 8px;
+  border: none;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.title-info {
+  display: flex;
+  align-items: center;
+}
+.title-info i {
+  font-size: 20px;
+  color: #409eff;
+  margin-right: 8px;
+}
+.cam-name {
+  font-weight: 600;
+  font-size: 16px;
+  margin-right: 12px;
+}
+.live-tag {
+  letter-spacing: 1px;
+}
+
+.video-content-wrapper {
+  background-color: #000;
+  border-radius: 4px;
+  overflow: hidden;
+}
+.video-container {
+  position: relative;
+  width: 100%;
+  padding-top: 56.25%; /* 16:9 Aspect Ratio */
+}
+.video-element {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+/* 侧边栏美化 */
+.record-sidebar-card {
+  border-radius: 8px;
+  border: none;
+  height: 100%;
+}
+.sidebar-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.sidebar-title {
+  display: flex;
+  align-items: center;
+  font-weight: 600;
+  color: #303133;
+}
+.sidebar-title i {
+  margin-right: 6px;
+  color: #909399;
+}
+.date-display {
+  padding: 12px;
+  background: #fdf6ec;
+  border-radius: 4px;
+  margin-bottom: 12px;
+  color: #e6a23c;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.date-display small {
+  color: #909399;
+}
+.time-text {
+  font-family: 'Monaco', 'Courier New', monospace;
+  font-weight: 500;
+}
+.sidebar-footer {
+  margin-top: 16px;
+}
+.full-width-btn {
+  width: 100%;
+}
+
+/* 日历组件美化 */
+.custom-calendar-dialog >>> .el-dialog__body {
+  padding: 0 20px 20px 20px;
+}
+.calendar-legend {
+  text-align: right;
+  font-size: 12px;
+  margin-bottom: 10px;
+}
+.legend-item {
+  margin-left: 15px;
+  color: #909399;
+}
+.dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #dcdfe6;
+}
+.dot.active {
+  background: #f56c6c;
+}
+
+.custom-date-cell {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+.has-record {
+  color: #f56c6c;
+  font-weight: bold;
+}
+.record-dot {
+  width: 4px;
+  height: 4px;
+  background: #f56c6c;
+  border-radius: 50%;
+  margin-top: 2px;
+}
+
+/* 分享列表美化 */
+.user-check-list {
+  max-height: 200px;
+  overflow-y: auto;
+  border: 1px solid #ebeef5;
+  padding: 10px;
+  border-radius: 4px;
+}
+.user-checkbox {
+  margin-bottom: 8px;
+  margin-left: 0 !important;
+  margin-right: 8px;
+}
+
+/* 表格滚动条美化 */
+.custom-table >>> .el-table__body-wrapper::-webkit-scrollbar {
+  width: 6px;
+}
+.custom-table >>> .el-table__body-wrapper::-webkit-scrollbar-thumb {
+  background-color: #ddd;
+  border-radius: 3px;
+}
+</style>

+ 151 - 0
src/views/disk/CamList.vue

@@ -0,0 +1,151 @@
+<template>
+  <div class="cam-list-container">
+    <el-row :gutter="20">
+      <el-col
+        v-for="item in dataList"
+        :key="item.id"
+        :xs="24" :sm="12" :md="8" :lg="6"
+        style="margin-bottom: 20px;"
+      >
+        <el-card :body-style="{ padding: '0px' }" class="cam-card">
+          <div class="cam-preview">
+            <i class="el-icon-video-camera"></i>
+            <el-tag
+              size="mini"
+              :type="item.state ? 'success' : 'danger'"
+              class="state-tag"
+            >
+              {{ item.state ? '在线' : '离线' }}
+            </el-tag>
+          </div>
+
+          <div style="padding: 14px;">
+            <div class="cam-info">
+              <span class="cam-name">{{ item.camName }}</span>
+              <span class="cam-id">ID: {{ item.camId }}</span>
+            </div>
+
+            <div class="bottom clearfix">
+              <time class="time">添加时间: {{ item.addAt }}</time>
+              <el-divider></el-divider>
+              <div class="actions">
+                <el-button type="text" size="small" @click="handlePlay(item)">查看</el-button>
+                <el-button type="text" size="small" style="color: #F56C6C">设置</el-button>
+              </div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-empty v-if="dataList.length === 0" description="暂无摄像头数据"></el-empty>
+  </div>
+</template>
+
+<script>
+import { getCamList } from '@/api/disk'
+
+export default {
+  name: 'CamList',
+  data() {
+    return {
+      dataList: []
+    }
+  },
+  created() {
+    document.title = '摄像头列表'
+    this.getData()
+  },
+  methods: {
+    getData() {
+      getCamList().then(resp => {
+        if (resp.code === 0) {
+          this.dataList = resp.data
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(err => {
+        this.$message.error("获取数据失败")
+      })
+    },
+    handlePlay(item) {
+      this.$router.push({
+        path: '/disk/cam/detail',
+        query: {
+          camId: item.camId
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.cam-list-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
+.cam-card {
+  transition: transform 0.3s;
+  border-radius: 8px;
+}
+
+.cam-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+}
+
+.cam-preview {
+  height: 150px;
+  background-color: #2c3e50;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+}
+
+.cam-preview i {
+  font-size: 48px;
+  color: #909399;
+}
+
+.state-tag {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+}
+
+.cam-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.cam-name {
+  font-size: 16px;
+  font-weight: bold;
+  color: #303133;
+}
+
+.cam-id {
+  font-size: 12px;
+  color: #999;
+}
+
+.time {
+  font-size: 12px;
+  color: #9499a0;
+}
+
+.el-divider--horizontal {
+  margin: 12px 0;
+}
+
+.actions {
+  display: flex;
+  justify-content: space-between;
+}
+</style>

+ 131 - 77
src/views/disk/Disk.vue

@@ -1,73 +1,47 @@
 <template>
-  <el-container>
-    <el-main>
-      <el-row class="el-menu-demo">
-        <el-col :md="2">
-          <ul role="menubar" class="el-menu--horizontal el-menu">
-            <li role="menuitem" class="el-menu-item">
-              <a href="/disk" style="color: #007bff;text-decoration-line: none">
-                <img src="@/assets/img/logo.png" class="logo" alt="img">
-                disk
-              </a>
-            </li>
-          </ul>
+  <el-container class="disk-container">
+    <el-header height="auto" class="header-wrapper">
+      <el-row type="flex" align="middle" class="nav-row">
+        <el-col :xs="6" :sm="4" :md="2">
+          <div class="logo-box">
+            <router-link to="/disk" class="logo-link">
+              <img src="@/assets/img/logo.png" class="logo" alt="logo">
+              <span class="logo-text">Disk</span>
+            </router-link>
+          </div>
         </el-col>
-        <el-col :md="20">
+
+        <el-col :xs="14" :sm="16" :md="20">
           <el-menu
-            class="el-menu--horizontal el-menu"
-            :default-active="this.$route.path"
+            :default-active="$route.path"
             router
             mode="horizontal"
+            class="top-menu"
           >
-            <el-menu-item index="/disk/album">
-              <template slot="title">
-                <span style="color: #007bff">相册</span>
-              </template>
-            </el-menu-item>
-            <el-menu-item index="/disk/share">
-              <template slot="title">
-                <span style="color: #007bff">分享</span>
-              </template>
-            </el-menu-item>
-<!--            <el-menu-item index="/disk/recycle">
-              <template slot="title">
-                <span style="color: #007bff">回收站</span>
-              </template>
-            </el-menu-item>-->
-            <el-menu-item index="/disk/cam">
-              <template slot="title">
-                <span style="color: #007bff">监控</span>
-              </template>
-            </el-menu-item>
+            <el-menu-item index="/disk/album">相册</el-menu-item>
+            <el-menu-item index="/disk/share">分享</el-menu-item>
+            <el-menu-item index="/disk/cam">摄像头</el-menu-item>
           </el-menu>
         </el-col>
-        <el-col :md="2">
-          <ul class="el-menu--horizontal el-menu">
-            <li class="el-menu-item">
-              <el-dropdown v-if="user">
-                <img
-                  :src="user.avatarUrl"
-                  class="el-avatar--circle el-avatar--medium"
-                  style="margin-right: 10px; margin-top: 15px"
-                  alt=""
-                >
-                <el-dropdown-menu slot="dropdown">
-                  <el-dropdown-item
-                    icon="el-icon-error"
-                    class="size"
-                    @click.native="goToLogout"
-                  >登出</el-dropdown-item>
-                </el-dropdown-menu>
-              </el-dropdown>
-              <span
-                v-else
-                style="color: #007bff"
-              >登入</span>
-            </li>
-          </ul>
+
+        <el-col :xs="4" :sm="4" :md="2" class="user-col">
+          <el-dropdown v-if="user" trigger="click">
+            <div class="avatar-wrapper">
+              <img :src="user.avatarUrl" class="user-avatar" alt="avatar">
+            </div>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item icon="el-icon-back" @click.native="goToLogout">登出</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+          <span v-else class="login-btn">登录</span>
         </el-col>
       </el-row>
-      <router-view />
+    </el-header>
+
+    <el-main class="main-content">
+      <transition name="fade-transform" mode="out-in">
+        <router-view :key="$route.fullPath" />
+      </transition>
     </el-main>
   </el-container>
 </template>
@@ -84,29 +58,109 @@ export default {
       user: null
     }
   },
-  watch: {
-    // 地址栏 url 发生变化时重新加载本页面
-    $route() {
-      this.$router.go()
-    }
-  },
+  // 注意:不再建议监听 $route 执行 router.go(),这会强制刷新整个页面。
+  // 通过在 router-view 绑定 :key="$route.fullPath" 即可实现组件更新。
   created() {
-    document.title = '网盘'
+    document.title = '我的云端'
     this.user = getAuthedUser()
-  },
-  methods: {
-    backToHome() {
-    },
-    logout() {
-      this.$message.info('logout')
-    }
   }
 }
 </script>
 
-<style>
+<style scoped>
+.disk-container {
+  min-height: 100vh;
+  background-color: #f5f7fa;
+}
+
+.header-wrapper {
+  background-color: #fff;
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
+  padding: 0 15px;
+  z-index: 100;
+}
+
+.nav-row {
+  height: 60px;
+}
+
+.logo-link {
+  display: flex;
+  align-items: center;
+  text-decoration: none;
+  color: #007bff;
+  font-weight: bold;
+}
+
 .logo {
-  width: 30px;
-  position: relative;
+  width: 28px;
+  height: 28px;
+  margin-right: 8px;
+}
+
+.top-menu {
+  border-bottom: none !important;
+}
+
+/* 兼容移动端菜单横向滑动 */
+.el-menu--horizontal {
+  display: flex;
+  overflow-x: auto;
+  scrollbar-width: none; /* Firefox */
+}
+.el-menu--horizontal::-webkit-scrollbar {
+  display: none; /* Chrome/Safari */
+}
+
+.top-menu .el-menu-item {
+  font-weight: 500;
+  padding: 0 15px;
+}
+
+.user-col {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.avatar-wrapper {
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+}
+
+.user-avatar {
+  width: 35px;
+  height: 35px;
+  border-radius: 50%;
+  border: 1px solid #ebeef5;
+}
+
+.main-content {
+  padding: 10px; /* 移动端减少边距 */
+}
+
+/* 页面切换动画 */
+.fade-transform-enter-active,
+.fade-transform-leave-active {
+  transition: all 0.3s;
+}
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-10px);
+}
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(10px);
+}
+
+@media screen and (max-width: 768px) {
+  .logo-text {
+    display: none; /* 手机端隐藏 Disk 文字,只留图标 */
+  }
+  .el-menu-item {
+    padding: 0 10px !important;
+    font-size: 13px;
+  }
 }
 </style>

+ 0 - 377
src/views/disk/DiskCam.vue

@@ -1,377 +0,0 @@
-<template>
-  <div>
-    <el-row style="padding: 5px">
-      <el-col :md="16" style="padding: 5px">
-        <el-card class="box-card">
-          <div v-if="camDetail" slot="header" class="clearfix">
-            <span>{{ camDetail.camName }}</span>
-            <el-button style="float: right; padding: 3px 0" type="text" @click="onShareCam">分享</el-button>
-          </div>
-          <div class="text item">
-            <video
-              id="videoElement"
-              controls
-              autoplay
-              class="video"
-              width="100%"
-              height="480px"
-            />
-          </div>
-        </el-card>
-        <!--        <el-card v-else class="box-card">
-          <div slot="header" class="clearfix">
-            <span>选择摄像头查看监控</span>
-          </div>
-        </el-card>-->
-      </el-col>
-      <el-col :md="8" style="padding: 5px">
-        <el-card class="box-card">
-          <div slot="header" class="clearfix">
-            <el-select
-              v-model="selectedOption"
-              style="margin: 5px;"
-              clearable
-              placeholder="选择摄像头"
-              @change="onSelectChange"
-            >
-              <el-option v-for="item in selectOptions" :key="item.value" :label="item.label" :value="item.value" />
-            </el-select>
-            <el-button type="plain" icon="el-icon-date" style="margin: 5px" @click="onSelectDate">选择日期</el-button>
-            <el-button type="plain" icon="el-icon-magic-stick" style="margin: 5px" @click="onButtonSubmit">提交</el-button>
-          </div>
-          <div class="text item">
-            <span style="color: red">
-              {{ getYearMonthDay(calendarDate) }} 的监控录像
-            </span>
-            <el-divider />
-            <el-table
-              :data="dataList"
-              :show-header="true"
-              style="width: 100%"
-            >
-              <el-table-column
-                prop="startTime"
-                label="开始时间"
-              />
-              <el-table-column
-                prop="duration"
-                label="时长(秒)"
-              />
-              <el-table-column label="操作">
-                <template slot-scope="scope">
-                  <el-button
-                    size="mini"
-                    icon="el-icon-video-play"
-                    @click="handlePlay(scope.row.recordId)"
-                  >播放</el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-dialog
-      title="日历"
-      append-to-body
-      :visible.sync="showCalenderDialog"
-      width="50%"
-      center
-    >
-      <div style="padding: 5px; text-align: center">
-        <span>
-          <span style="color: red">红色 ✔</span>标记的日期表示当天有监控录像
-        </span>
-        <el-calendar v-model="calendarDate">
-          <div
-            slot="dateCell"
-            slot-scope="{ date, data }"
-            @click="handleCellClick(date, data)"
-          >
-            <p v-if="dealMyDate(data.day)" style="color: red">
-              {{ data.day.split("-").slice(2).join() }} {{ '✔' }}
-            </p>
-            <p v-else>{{ data.day.split("-").slice(2).join() }}</p>
-          </div>
-        </el-calendar>
-      </div>
-    </el-dialog>
-
-    <el-dialog
-      :visible.sync="showCreateShareDialog"
-      width="100%"
-      center
-    >
-      <div>
-        <el-form ref="createAlbumForm" :model="createShareForm">
-          <el-form-item label="选择用户" label-width="120px" prop="title">
-            <el-checkbox-group v-model="createShareForm.shareToList">
-              <el-checkbox v-for="user in userContactList" :key="user.userIdStr" :label="user.username" />
-            </el-checkbox-group>
-          </el-form-item>
-          <el-button
-            type="primary"
-            plain
-            size="small"
-            icon="el-icon-plus"
-            style="margin-left: 10px"
-            @click="createShare"
-          >
-            分享摄像头
-          </el-button>
-        </el-form>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import flvjs from 'flv.js'
-
-import { createShare, getCamDetail, getCamKeyValue, getRecordByMonth, getRecordUrl, submitActivity } from '@/api/disk'
-import { getUserContact } from '@/api/user'
-
-export default {
-  name: 'DiskCam',
-  data() {
-    return {
-      selectOptions: [],
-      selectedOption: '',
-      calendarDate: new Date(),
-      dateMap: new Map(),
-      query: {
-        camId: '',
-        yearMonth: '',
-        yearMonthDay: ''
-      },
-      dataList: [],
-      showCalenderDialog: false,
-      camDetail: null,
-      camRecordDetail: null,
-      // ****************************************************************************************************************
-      showCreateShareDialog: false,
-      userContactList: [],
-      createShareForm: {
-        albumType: 1,
-        albumId: null,
-        shareToList: []
-      },
-      flvPlayer: null
-    }
-  },
-  watch: {
-    $route() {
-      this.$router.go()
-    },
-    calendarDate: {
-      handler(newValue, oldValue) {
-        const oldMonth = this.getYearMonth(oldValue)
-        const newMonth = this.getYearMonth(newValue)
-        if (oldMonth !== newMonth) {
-          this.query.yearMonth = this.getYearMonth(newValue)
-          getRecordByMonth(this.query).then(resp => {
-            if (resp.code === 0) {
-              this.dateMap.clear()
-              for (const item of resp.data) {
-                const date1 = new Date(item)
-                const dayStr = this.getYearMonthDay(date1)
-                this.dateMap.set(dayStr, date1)
-              }
-              // 对 calendarDate 赋值重新渲染日历, 目的是调用 dealMyDate 处理当月的日期
-              this.calendarDate = new Date(newMonth)
-            }
-          }).catch(error => {
-            this.$message.error(error.message)
-          })
-        }
-      }
-    }
-  },
-  created() {
-    document.title = '监控'
-    getCamKeyValue().then(resp => {
-      if (resp.code === 0) {
-        this.selectOptions = resp.data
-      }
-    })
-  },
-  methods: {
-    getData() {
-      getCamDetail(this.query).then(resp => {
-        if (resp.code === 0) {
-          this.camDetail = resp.data
-          if (this.camDetail.onLive) {
-            this.initVideoPlayer(this.camDetail.liveUrl, true)
-          } else {
-            this.initVideoPlayer(this.camDetail.url, false)
-          }
-        }
-      })
-    },
-    onSelectChange() {
-      if (this.selectedOption === '') {
-        this.query.camId = ''
-        this.query.yearMonth = ''
-        this.query.yearMonthDay = ''
-        this.camDetail = null
-        this.camRecordDetail = null
-        return
-      }
-
-      this.query.camId = this.selectedOption
-      this.query.yearMonth = ''
-      this.query.yearMonthDay = ''
-      this.getData()
-    },
-    handleCellClick(date, data) {
-      this.showCalenderDialog = false
-      this.query.yearMonthDay = this.getYearMonthDay(date)
-      getCamDetail(this.query).then(resp => {
-        if (resp.code === 0) {
-          const respData = resp.data
-          this.camDetail = respData
-          this.dataList = respData.dayRecords
-        }
-      })
-    },
-    // 渲染日历时会处理每个日期
-    dealMyDate(val) {
-      return this.dateMap.get(val) !== undefined
-    },
-    getYearMonth(date) {
-      const year = date.getFullYear().toString().padStart(4, '0')
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      // const day = date.getDate().toString().padStart(2, '0')
-      // const hour = date.getHours().toString().padStart(2, '0')
-      // const minute = date.getMinutes().toString().padStart(2, '0')
-      // const second = date.getSeconds().toString().padStart(2, '0')
-      // 2023-02-16 08:25:05
-      // console.log(`${year}-${month}-${day} ${hour}:${minute}:${second}`)
-      return year + '-' + month
-    },
-    getYearMonthDay(date) {
-      const year = date.getFullYear().toString().padStart(4, '0')
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      return year + '-' + month + '-' + day
-    },
-    handlePlay(recordId) {
-      getRecordUrl(recordId).then(resp => {
-        if (resp.code === 0) {
-          this.camRecordDetail = resp.data
-          this.initVideoPlayer(this.camRecordDetail.url, false)
-        } else {
-          this.$message.error(resp.msg)
-        }
-      })
-    },
-    initVideoPlayer(videoUrl, live) {
-      var videoElement = document.getElementById('videoElement')
-      if (!live) {
-        videoElement.src = videoUrl
-        return
-      }
-
-      if (!flvjs.isSupported()) {
-        this.$message.error('not support flv')
-        return
-      }
-
-      this.flvPlayer = flvjs.createPlayer({
-        type: 'flv',
-        isLive: true,
-        url: videoUrl,
-        duration: 0,
-        filesize: 0,
-        enableStashBuffer: false,
-        hasAudio: true,
-        hasVideo: true
-      })
-      this.flvPlayer.attachMediaElement(videoElement)
-      this.flvPlayer.load()
-      this.flvPlayer.play()
-
-      videoElement.addEventListener('progress', () => {
-        const end = this.flvPlayer.buffered.end(0) // 获取当前buffered值(缓冲区末尾)
-        const delta = end - this.flvPlayer.currentTime // 获取buffered与当前播放位置的差值
-        // 延迟过大,通过跳帧的方式更新视频
-        if (delta > 10 || delta < 0) {
-          this.flvPlayer.currentTime = this.flvPlayer.buffered.end(0) - 1
-          return
-        }
-
-        // 追帧
-        if (delta > 1) {
-          videoElement.playbackRate = 1.1
-        } else {
-          videoElement.playbackRate = 1
-        }
-      })
-      // 点击播放按钮后,更新视频
-      videoElement.addEventListener('play', () => {
-        if (this.flvPlayer.buffered.length > 0) {
-          const end = this.flvPlayer.buffered.end(0) - 1
-          this.flvPlayer.currentTime = end
-        }
-      })
-      // 网页重新激活后,更新视频
-      window.onfocus = () => {
-        const end = this.flvPlayer.buffered.end(0) - 1
-        this.flvPlayer.currentTime = end
-      }
-    },
-    onSelectDate() {
-      if (this.selectedOption === '') {
-        this.$message.info('请先选择摄像头')
-      } else {
-        this.showCalenderDialog = true
-      }
-    },
-    onButtonSubmit() {
-      this.$confirm('确认提交?', '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        submitActivity().then(resp => {
-          this.$message.info(resp.msg)
-        }).catch(error => {
-          this.$message.error(error.message)
-        })
-      }).catch(() => {
-        this.$message({
-          type: 'info',
-          message: '已取消'
-        })
-      })
-    },
-    onShareCam() {
-      this.createShareForm.albumId = this.camDetail.camId
-      getUserContact(1).then(resp => {
-        if (resp.code === 0) {
-          this.userContactList = resp.data
-          this.showCreateShareDialog = true
-        } else {
-          this.$message.error(resp.msg)
-        }
-      })
-    },
-    createShare() {
-      createShare(this.createShareForm).then(resp => {
-        this.$message.info(resp.msg)
-      }).finally(() => {
-        this.showCreateShareDialog = false
-        this.createShareForm = {
-          albumType: 1,
-          albumId: null,
-          shareToList: []
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style>
-</style>