| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844 |
- <template>
- <view class="container">
- <!-- 设备头部信息区域 -->
- <view class="device-header">
- <view class="device-info-row">
- <text class="device-name">{{ deviceInfo.name }}</text>
- <view
- class="status-tag"
- :class="deviceInfo.status === 'online' ? 'status-online' : 'status-offline'"
- >
- <text class="status-dot" :class="{'offline-dot': deviceInfo.status === 'offline'}"></text>
- {{ deviceInfo.status === 'online' ? '在线' : '离线' }}
- </view>
- </view>
-
- <view class="device-meta-row">
- <view class="device-meta-item">
- <text class="meta-label">设备编号:</text>
- <text class="meta-value">{{ deviceInfo.deviceId }}</text>
- </view>
-
- <view class="device-meta-item">
- <text class="meta-label">安装位置:</text>
- <text class="meta-value">{{ deviceInfo.location }}</text>
- </view>
-
- <view class="device-meta-item">
- <text class="meta-label">最近更新:</text>
- <text class="meta-value">{{ deviceInfo.lastUpdate }}</text>
- </view>
- </view>
- </view>
-
- <!-- 视频预览区域 -->
- <view class="video-section">
- <view class="video-container">
- <image v-if="!isPlaying" src="/static/images/video-placeholder.jpg" mode="aspectFill" class="video-placeholder"></image>
- <video
- v-else
- id="videoPlayer"
- :src="deviceInfo.streamUrl"
- class="video-player"
- object-fit="cover"
- autoplay
- :controls="false"
- :show-center-play-btn="false"
- :show-fullscreen-btn="false"
- :show-play-btn="false"
- :enable-progress-gesture="false"
- @error="handleVideoError"
- ></video>
-
- <!-- 视频控制层 -->
- <view class="video-controls">
- <view class="control-row top-controls">
- <view class="signal-indicator">
- <text class="signal-icon iconfont icon-signal"></text>
- <text class="signal-text">信号良好</text>
- </view>
-
- <view class="video-time">{{ currentTime }}</view>
- </view>
-
- <view class="control-row center-controls">
- <view class="center-button" @click="togglePlayState">
- <view class="play-icon" v-if="!isPlaying">
- <text class="iconfont icon-play-fill"></text>
- </view>
- <view class="pause-icon" v-else>
- <text class="iconfont icon-pause-fill"></text>
- </view>
- </view>
- </view>
-
- <view class="control-row bottom-controls">
- <view class="control-button" @click="toggleMute">
- <text :class="isMuted ? 'iconfont icon-volume-off-fill' : 'iconfont icon-volume-up-fill'"></text>
- </view>
-
- <view class="control-button" @click="takeScreenshot">
- <text class="iconfont icon-camera-fill"></text>
- </view>
-
- <view class="control-button" @click="toggleRecording">
- <text :class="isRecording ? 'iconfont icon-stop-fill' : 'iconfont icon-record-fill'"></text>
- </view>
-
- <view class="control-button" @click="toggleFullscreen">
- <text class="iconfont icon-fullscreen"></text>
- </view>
- </view>
- </view>
- </view>
- </view>
-
- <!-- 云台控制区域 -->
- <view class="ptz-section">
- <view class="section-title">云台控制</view>
-
- <view class="ptz-container">
- <view class="ptz-controls">
- <!-- 上方向箭头 -->
- <view class="ptz-arrow ptz-up" @touchstart="controlPTZ('up', true)" @touchend="controlPTZ('up', false)">
- <text class="iconfont icon-arrow-up-fill"></text>
- </view>
-
- <!-- 左上方向箭头 -->
- <view class="ptz-arrow ptz-left-up" @touchstart="controlPTZ('leftUp', true)" @touchend="controlPTZ('leftUp', false)">
- <text class="iconfont icon-arrow-left-up"></text>
- </view>
-
- <!-- 右上方向箭头 -->
- <view class="ptz-arrow ptz-right-up" @touchstart="controlPTZ('rightUp', true)" @touchend="controlPTZ('rightUp', false)">
- <text class="iconfont icon-arrow-right-up"></text>
- </view>
-
- <!-- 左方向箭头 -->
- <view class="ptz-arrow ptz-left" @touchstart="controlPTZ('left', true)" @touchend="controlPTZ('left', false)">
- <text class="iconfont icon-arrow-left-fill"></text>
- </view>
-
- <!-- 中心点 -->
- <view class="ptz-center">
- <text class="iconfont icon-dot"></text>
- </view>
-
- <!-- 右方向箭头 -->
- <view class="ptz-arrow ptz-right" @touchstart="controlPTZ('right', true)" @touchend="controlPTZ('right', false)">
- <text class="iconfont icon-arrow-right-fill"></text>
- </view>
-
- <!-- 左下方向箭头 -->
- <view class="ptz-arrow ptz-left-down" @touchstart="controlPTZ('leftDown', true)" @touchend="controlPTZ('leftDown', false)">
- <text class="iconfont icon-arrow-left-down"></text>
- </view>
-
- <!-- 下方向箭头 -->
- <view class="ptz-arrow ptz-down" @touchstart="controlPTZ('down', true)" @touchend="controlPTZ('down', false)">
- <text class="iconfont icon-arrow-down-fill"></text>
- </view>
-
- <!-- 右下方向箭头 -->
- <view class="ptz-arrow ptz-right-down" @touchstart="controlPTZ('rightDown', true)" @touchend="controlPTZ('rightDown', false)">
- <text class="iconfont icon-arrow-right-down"></text>
- </view>
- </view>
-
- <!-- 变焦控制 -->
- <view class="zoom-controls">
- <view class="zoom-button zoom-in" @touchstart="controlZoom('in', true)" @touchend="controlZoom('in', false)">
- <text class="iconfont icon-plus"></text>
- </view>
-
- <view class="zoom-label">
- <text>变焦</text>
- </view>
-
- <view class="zoom-button zoom-out" @touchstart="controlZoom('out', true)" @touchend="controlZoom('out', false)">
- <text class="iconfont icon-minus"></text>
- </view>
- </view>
- </view>
- </view>
-
- <!-- 快捷功能按钮 -->
- <view class="quick-actions">
- <view class="action-button" @click="takeScreenshot">
- <view class="action-icon">
- <text class="iconfont icon-camera-fill"></text>
- </view>
- <text class="action-text">截图保存</text>
- </view>
-
- <view class="action-button" @click="toggleRecording">
- <view class="action-icon">
- <text :class="isRecording ? 'iconfont icon-stop-fill' : 'iconfont icon-record-fill'"></text>
- </view>
- <text class="action-text">{{ isRecording ? '停止录像' : '开始录像' }}</text>
- </view>
-
- <view class="action-button" @click="copyStreamUrl">
- <view class="action-icon">
- <text class="iconfont icon-copy"></text>
- </view>
- <text class="action-text">复制流地址</text>
- </view>
-
- <view class="action-button" @click="toggleVoiceIntercom">
- <view class="action-icon">
- <text :class="isVoiceActive ? 'iconfont icon-mic-fill' : 'iconfont icon-mic'"></text>
- </view>
- <text class="action-text">语音对讲</text>
- </view>
- </view>
-
- <!-- 历史视频/录像入口 -->
- <view class="history-section">
- <view class="section-title">历史录像</view>
-
- <view class="history-list">
- <view
- v-for="(item, index) in recordHistory"
- :key="index"
- class="history-item"
- @click="playHistoryVideo(item)"
- >
- <view class="history-icon">
- <text class="iconfont icon-video-history"></text>
- </view>
-
- <view class="history-info">
- <text class="history-time">{{ item.startTime }}</text>
- <text class="history-duration">时长: {{ item.duration }}</text>
- </view>
-
- <view class="history-action">
- <text class="iconfont icon-play"></text>
- </view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- export default {
- data() {
- return {
- deviceInfo: {
- deviceId: 'DEV1001',
- name: '监控设备-1',
- status: 'online',
- location: '西区B2地块',
- lastUpdate: '5分钟前',
- streamUrl: 'https://demo-rtsp-server-2h4n.onrender.com/stream.mp4',
- alertCount: 2
- },
- isPlaying: false,
- isMuted: false,
- isRecording: false,
- isFullscreen: false,
- isVoiceActive: false,
- currentTime: '14:30:25',
- // 模拟历史录像数据
- recordHistory: [
- { id: 1, startTime: '今天 12:30', duration: '00:15:30', url: '' },
- { id: 2, startTime: '今天 10:15', duration: '00:05:22', url: '' },
- { id: 3, startTime: '昨天 18:45', duration: '00:30:10', url: '' },
- { id: 4, startTime: '昨天 14:20', duration: '00:10:05', url: '' }
- ],
- videoContext: null
- }
- },
-
- onReady() {
- // 获取视频实例
- this.videoContext = uni.createVideoContext('videoPlayer')
-
- // 设置页面标题
- uni.setNavigationBarTitle({
- title: this.deviceInfo.name
- })
-
- // 模拟更新时间
- this.startTimeUpdate()
- },
-
- onLoad(options) {
- // 如果有传入设备ID,则获取设备信息
- if (options && options.id) {
- this.fetchDeviceInfo(options.id)
- }
- },
-
- methods: {
- // 获取设备信息
- fetchDeviceInfo(deviceId) {
- // 这里应该是API请求,暂时用模拟数据
- console.log('获取设备信息:', deviceId)
- // 模拟异步获取数据
- setTimeout(() => {
- // 实际应该是API请求结果
- }, 500)
- },
-
- // 播放/暂停切换
- togglePlayState() {
- this.isPlaying = !this.isPlaying
-
- if (this.isPlaying) {
- setTimeout(() => {
- this.videoContext && this.videoContext.play()
- }, 300)
- } else {
- this.videoContext && this.videoContext.pause()
- }
- },
-
- // 静音切换
- toggleMute() {
- this.isMuted = !this.isMuted
- if (this.videoContext) {
- if (this.isMuted) {
- this.videoContext.mute()
- } else {
- this.videoContext.unmute()
- }
- }
- },
-
- // 录像切换
- toggleRecording() {
- this.isRecording = !this.isRecording
-
- if (this.isRecording) {
- // 模拟开始录像
- uni.showToast({
- title: '开始录像',
- icon: 'none'
- })
- } else {
- // 模拟结束录像并保存
- uni.showToast({
- title: '录像已保存',
- icon: 'success'
- })
- }
- },
-
- // 全屏切换
- toggleFullscreen() {
- if (this.videoContext) {
- if (!this.isFullscreen) {
- this.videoContext.requestFullScreen()
- } else {
- this.videoContext.exitFullScreen()
- }
- this.isFullscreen = !this.isFullscreen
- }
- },
-
- // 截图
- takeScreenshot() {
- // 模拟截图功能
- uni.showLoading({
- title: '截图中...'
- })
-
- setTimeout(() => {
- uni.hideLoading()
- uni.showToast({
- title: '截图已保存',
- icon: 'success'
- })
- }, 1000)
- },
-
- // 复制流地址
- copyStreamUrl() {
- uni.setClipboardData({
- data: this.deviceInfo.streamUrl,
- success: () => {
- uni.showToast({
- title: '地址已复制',
- icon: 'success'
- })
- }
- })
- },
-
- // 语音对讲
- toggleVoiceIntercom() {
- this.isVoiceActive = !this.isVoiceActive
-
- if (this.isVoiceActive) {
- uni.showToast({
- title: '语音对讲已开启',
- icon: 'none'
- })
- } else {
- uni.showToast({
- title: '语音对讲已关闭',
- icon: 'none'
- })
- }
- },
-
- // 云台控制
- controlPTZ(direction, isStart) {
- // 模拟云台控制
- if (isStart) {
- console.log(`开始控制云台: ${direction}`)
- uni.vibrateShort() // 短震动反馈
- } else {
- console.log(`停止控制云台: ${direction}`)
- }
- },
-
- // 变焦控制
- controlZoom(type, isStart) {
- // 模拟变焦控制
- if (isStart) {
- console.log(`开始控制变焦: ${type}`)
- uni.vibrateShort() // 短震动反馈
- } else {
- console.log(`停止控制变焦: ${type}`)
- }
- },
-
- // 播放历史视频
- playHistoryVideo(item) {
- uni.showToast({
- title: `播放录像: ${item.startTime}`,
- icon: 'none'
- })
- },
-
- // 处理视频错误
- handleVideoError(e) {
- console.error('视频播放错误:', e)
- uni.showToast({
- title: '视频播放出错,请稍后再试',
- icon: 'none'
- })
- },
-
- // 更新时间
- startTimeUpdate() {
- // 模拟时间更新
- setInterval(() => {
- const now = new Date()
- const hours = String(now.getHours()).padStart(2, '0')
- const minutes = String(now.getMinutes()).padStart(2, '0')
- const seconds = String(now.getSeconds()).padStart(2, '0')
- this.currentTime = `${hours}:${minutes}:${seconds}`
- }, 1000)
- }
- }
- }
- </script>
- <style>
- /* 基础样式 */
- .container {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- background-color: #F9FCFA;
- padding-bottom: 30rpx;
- }
- /* 设备头部样式 */
- .device-header {
- background-color: #FFFFFF;
- border-radius: 20rpx;
- padding: 26rpx 30rpx;
- margin: 20rpx 30rpx;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
- }
- .device-info-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16rpx;
- }
- .device-name {
- font-size: 34rpx;
- color: #333333;
- font-weight: 600;
- }
- .status-tag {
- padding: 6rpx 16rpx 6rpx 32rpx;
- border-radius: 8rpx;
- font-size: 24rpx;
- font-weight: 500;
- flex-shrink: 0;
- border: 1rpx solid;
- position: relative;
- overflow: hidden;
- }
- .status-online {
- background-color: rgba(76, 175, 80, 0.1);
- color: #4CAF50;
- border-color: rgba(76, 175, 80, 0.3);
- }
- .status-offline {
- background-color: rgba(245, 108, 108, 0.1);
- color: #F56C6C;
- border-color: rgba(245, 108, 108, 0.3);
- padding-left: 32rpx;
- }
- .status-dot {
- position: absolute;
- width: 8rpx;
- height: 8rpx;
- background-color: #4CAF50;
- border-radius: 50%;
- top: 50%;
- left: 16rpx;
- transform: translateY(-50%);
- box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
- animation: blink 1.5s infinite;
- display: inline-block;
- }
- .status-dot.offline-dot {
- background-color: #F56C6C;
- box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
- }
- @keyframes blink {
- 0% {
- opacity: 0.4;
- }
- 50% {
- opacity: 1;
- }
- 100% {
- opacity: 0.4;
- }
- }
- .device-meta-row {
- display: flex;
- flex-direction: column;
- }
- .device-meta-item {
- display: flex;
- font-size: 26rpx;
- margin-top: 8rpx;
- color: #666666;
- }
- .meta-label {
- color: #999999;
- min-width: 140rpx;
- }
- .meta-value {
- color: #666666;
- flex: 1;
- }
- /* 视频预览区域 */
- .video-section {
- margin: 0 30rpx 20rpx;
- }
- .video-container {
- position: relative;
- width: 100%;
- height: 400rpx;
- background-color: #000000;
- border-radius: 16rpx;
- overflow: hidden;
- box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
- }
- .video-player, .video-placeholder {
- width: 100%;
- height: 100%;
- }
- .video-controls {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: 20rpx;
- box-sizing: border-box;
- background: linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.3) 100%);
- }
- .control-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
- }
- .top-controls {
- height: 60rpx;
- }
- .center-controls {
- height: 120rpx;
- justify-content: center;
- }
- .bottom-controls {
- height: 60rpx;
- justify-content: flex-end;
- }
- .signal-indicator {
- display: flex;
- align-items: center;
- color: #FFFFFF;
- font-size: 24rpx;
- }
- .signal-icon {
- margin-right: 8rpx;
- }
- .video-time {
- color: #FFFFFF;
- font-size: 24rpx;
- }
- .center-button {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
- background-color: rgba(255, 255, 255, 0.2);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #FFFFFF;
- font-size: 40rpx;
- }
- .control-button {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #FFFFFF;
- font-size: 36rpx;
- margin-left: 20rpx;
- }
- /* 云台控制区域 */
- .ptz-section {
- margin: 0 30rpx 20rpx;
- background-color: #FFFFFF;
- border-radius: 20rpx;
- padding: 20rpx;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
- }
- .section-title {
- font-size: 30rpx;
- font-weight: 600;
- color: #333333;
- margin-bottom: 20rpx;
- padding: 0 10rpx;
- }
- .ptz-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .ptz-controls {
- display: grid;
- grid-template-columns: repeat(3, 80rpx);
- grid-template-rows: repeat(3, 80rpx);
- gap: 10rpx;
- width: 260rpx;
- height: 260rpx;
- }
- .ptz-arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #F0F9F0;
- border-radius: 50%;
- color: #4CAF50;
- font-size: 36rpx;
- transition: all 0.2s;
- }
- .ptz-arrow:active {
- background-color: #4CAF50;
- color: #FFFFFF;
- }
- .ptz-up { grid-column: 2; grid-row: 1; }
- .ptz-left-up { grid-column: 1; grid-row: 1; }
- .ptz-right-up { grid-column: 3; grid-row: 1; }
- .ptz-left { grid-column: 1; grid-row: 2; }
- .ptz-center {
- grid-column: 2;
- grid-row: 2;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #4CAF50;
- font-size: 30rpx;
- }
- .ptz-right { grid-column: 3; grid-row: 2; }
- .ptz-left-down { grid-column: 1; grid-row: 3; }
- .ptz-down { grid-column: 2; grid-row: 3; }
- .ptz-right-down { grid-column: 3; grid-row: 3; }
- .zoom-controls {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-right: 20rpx;
- }
- .zoom-button {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- background-color: #F0F9F0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #4CAF50;
- font-size: 36rpx;
- margin: 10rpx 0;
- transition: all 0.2s;
- }
- .zoom-button:active {
- background-color: #4CAF50;
- color: #FFFFFF;
- }
- .zoom-label {
- font-size: 24rpx;
- color: #999999;
- margin: 5rpx 0;
- }
- /* 快捷功能按钮 */
- .quick-actions {
- display: flex;
- justify-content: space-between;
- margin: 0 30rpx 30rpx;
- background-color: #FFFFFF;
- border-radius: 20rpx;
- padding: 30rpx;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
- }
- .action-button {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 120rpx;
- }
- .action-icon {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
- background-color: #F0F9F0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #4CAF50;
- font-size: 36rpx;
- margin-bottom: 10rpx;
- }
- .action-text {
- font-size: 24rpx;
- color: #666666;
- text-align: center;
- }
- /* 历史视频/录像区域 */
- .history-section {
- margin: 0 30rpx;
- background-color: #FFFFFF;
- border-radius: 20rpx;
- padding: 20rpx;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
- }
- .history-list {
- display: flex;
- flex-direction: column;
- }
- .history-item {
- display: flex;
- align-items: center;
- padding: 20rpx 10rpx;
- border-bottom: 1rpx solid #F5F5F5;
- }
- .history-item:last-child {
- border-bottom: none;
- }
- .history-icon {
- width: 60rpx;
- height: 60rpx;
- border-radius: 8rpx;
- background-color: #F0F9F0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #4CAF50;
- font-size: 30rpx;
- margin-right: 16rpx;
- }
- .history-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
- .history-time {
- font-size: 28rpx;
- color: #333333;
- margin-bottom: 6rpx;
- }
- .history-duration {
- font-size: 24rpx;
- color: #999999;
- }
- .history-action {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #4CAF50;
- font-size: 30rpx;
- }
- </style>
|