Compare commits
2 Commits
a8b6fb4b8b
...
220677785d
| Author | SHA1 | Date | |
|---|---|---|---|
| 220677785d | |||
| 9f60742bcf |
@ -2,7 +2,7 @@
|
|||||||
VITE_NODE_ENV=production
|
VITE_NODE_ENV=production
|
||||||
|
|
||||||
# API 基础路径 - 实际项目中替换为真实的后端接口地址
|
# API 基础路径 - 实际项目中替换为真实的后端接口地址
|
||||||
VITE_API_BASE_URL=https://api.your-domain.com
|
VITE_API_BASE_URL=http://127.0.0.1:3000
|
||||||
|
|
||||||
# 项目基础路径
|
# 项目基础路径
|
||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
|
|||||||
9
src/api/videos.js
Normal file
9
src/api/videos.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视频列表
|
||||||
|
* @returns {Promise} 返回视频列表数据
|
||||||
|
*/
|
||||||
|
export function getVideoList() {
|
||||||
|
return request.get('/api/videos/list')
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import AdminLayout from '../layout/AdminLayout.vue'
|
import AdminLayout from '../layout/AdminLayout.vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ Promise.all([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -269,6 +269,15 @@ const router = createRouter({
|
|||||||
title: '无人机管理',
|
title: '无人机管理',
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'videos',
|
||||||
|
name: 'VideoList',
|
||||||
|
component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/videos/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '视频列表',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
309
src/views/AIPatrol/videos/index.vue
Normal file
309
src/views/AIPatrol/videos/index.vue
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Search, View, Download, Refresh, Calendar, Document } from '@element-plus/icons-vue'
|
||||||
|
import { getVideoList } from '@/api/videos'
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const videoList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchText = ref('')
|
||||||
|
const pagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 12, // 默认显示12个视频
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 视频查看状态
|
||||||
|
const videoModalVisible = ref(false)
|
||||||
|
const selectedVideo = ref(null)
|
||||||
|
|
||||||
|
// 获取视频列表数据
|
||||||
|
const fetchVideoList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await getVideoList()
|
||||||
|
videoList.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取视频列表错误:', error)
|
||||||
|
ElMessage.error('获取视频列表失败:' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取视频URL
|
||||||
|
const getVideoUrl = (video) => {
|
||||||
|
const url = `${import.meta.env.VITE_API_BASE_URL}${video.videoUrl}`
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件处理函数
|
||||||
|
const onSearch = () => {
|
||||||
|
pagination.value.current = 1
|
||||||
|
fetchVideoList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchVideoList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pagination.value.pageSize = val
|
||||||
|
fetchVideoList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
pagination.value.current = val
|
||||||
|
fetchVideoList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWatch = (record) => {
|
||||||
|
selectedVideo.value = record
|
||||||
|
videoModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = (record) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = getVideoUrl(record)
|
||||||
|
link.download = record.fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
ElMessage.success('下载成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
fetchVideoList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="video-list-container">
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">AI巡检视频列表</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索和过滤区域 -->
|
||||||
|
<div class="table-operations">
|
||||||
|
<el-space>
|
||||||
|
<el-input
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索视频名称"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="onSearch"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="handleRefresh">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</el-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频卡片列表 -->
|
||||||
|
<div v-loading="loading" class="video-grid">
|
||||||
|
<div
|
||||||
|
v-for="video in videoList"
|
||||||
|
:key="video.id"
|
||||||
|
class="video-card"
|
||||||
|
>
|
||||||
|
<!-- 视频预览 -->
|
||||||
|
<div class="video-preview">
|
||||||
|
<video
|
||||||
|
:src="getVideoUrl(video)"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频信息 -->
|
||||||
|
<div class="video-info">
|
||||||
|
<h3 class="video-title" :title="video.fileName">{{ video.fileName }}</h3>
|
||||||
|
<div class="video-meta">
|
||||||
|
<el-text class="video-time">
|
||||||
|
<el-icon><Calendar /></el-icon>
|
||||||
|
{{ video.createTime }}
|
||||||
|
</el-text>
|
||||||
|
<el-text class="video-size">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
{{ formatFileSize(video.fileSize) }}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="video-actions">
|
||||||
|
<el-button type="primary" @click="handleWatch(video)">
|
||||||
|
<el-icon><View /></el-icon>
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" @click="handleDownload(video)">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[12, 24, 36, 48]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
v-if="pagination.total > 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频查看弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="videoModalVisible"
|
||||||
|
title="视频查看"
|
||||||
|
width="800px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
v-if="selectedVideo"
|
||||||
|
:src="getVideoUrl(selectedVideo)"
|
||||||
|
style="width: 100%"
|
||||||
|
controls
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-operations {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px; /* 为滚动条预留空间 */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-card:hover {
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 56.25%; /* 16:9 宽高比 */
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-preview video {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-info {
|
||||||
|
padding: 12px 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-meta .el-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条样式 */
|
||||||
|
.video-grid::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #dcdfe6;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -4,6 +4,7 @@ import path from 'path'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user