From 54849757852f6ab40eb17afbd03d1d839b60a38d Mon Sep 17 00:00:00 2001
From: guoshilong <123456>
Date: Mon, 13 Nov 2023 17:09:15 +0800
Subject: [PATCH] 重复定时和连续执行
---
src/components/MediaPanel.vue | 509 ++++++++++++++++++++++++++++++++++++++++++++------------
1 files changed, 397 insertions(+), 112 deletions(-)
diff --git a/src/components/MediaPanel.vue b/src/components/MediaPanel.vue
index 33a7c55..f9e5d7d 100644
--- a/src/components/MediaPanel.vue
+++ b/src/components/MediaPanel.vue
@@ -1,67 +1,129 @@
<template>
-<!-- <div class="header">媒体文件</div>-->
- <!--搜索栏-->
- <div class="search-panel-wrapper">
- <div class="search-part">
- <a-range-picker :size="searchPanelOptions.size" :format="searchPanelOptions.dateFormat" :valueFormat="searchPanelOptions.valueFormat" v-model:value="timeRangeArr.data" @change="dateChange" style="width: 300px" />
+ <!-- <div class="header">媒体文件</div>-->
+ <!--搜索栏-->
+ <div class="search-panel-wrapper">
+ <div class="search-part">
+ <a-range-picker :size="searchPanelOptions.size" :format="searchPanelOptions.dateFormat"
+ :valueFormat="searchPanelOptions.valueFormat" v-model:value="timeRangeArr.data"
+ @change="dateChange" style="width: 300px"/>
+ </div>
+
+ <div class="search-part">
+ <a-select v-model:value="subFileTypeArr" allowClear @change="subFileTypeChange"
+ :options="subFileTypeOptions"
+ :maxTagCount="searchPanelOptions.maxTagCount" mode="multiple"
+ :size="searchPanelOptions.size" placeholder="所有类型" style="width: 300px">
+ </a-select>
+ </div>
+
+ <div class="search-part">
+ <a-select v-model:value="payloadArr" allowClear @change="payloadChange" :options="payloadOptions"
+ :maxTagCount="searchPanelOptions.maxTagCount" mode="multiple"
+ :size="searchPanelOptions.size" placeholder="所有负载" style="width: 300px">
+ </a-select>
+ </div>
+
+ <div class="search-part">
+ <a-input-search :size="searchPanelOptions.size" v-model:value="searchQuery.name" @change="inputChange"
+ placeholder="按文件名称搜索" style="width: 300px"/>
+ </div>
+
</div>
- <div class="search-part">
- <a-select v-model:value="subFileTypeArr" allowClear @change="subFileTypeChange" :options="subFileTypeOptions" :maxTagCount="searchPanelOptions.maxTagCount" mode="multiple"
- :size="searchPanelOptions.size" placeholder="所有类型" style="width: 300px">
- </a-select>
+ <div class="button-wrapper">
+ <a-button type="primary" @click="compress">压缩打包</a-button>
</div>
- <div class="search-part">
- <a-select v-model:value="payloadArr" allowClear @change="payloadChange" :options="payloadOptions" :maxTagCount="searchPanelOptions.maxTagCount" mode="multiple"
- :size="searchPanelOptions.size" placeholder="所有负载" style="width: 300px">
- </a-select>
+ <a-spin :spinning="loading" :delay="1000" tip="加载中" size="large">
+ <div class="media-panel-wrapper">
+ <a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint"
+ rowKey="file_id" :row-selection="rowSelection"
+ :pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
+ <template v-for="col in ['name']" #[col]="{text, record}" :key="col">
+
+ <div>
+ <a-input
+ v-if="editableData[record.file_id]"
+ v-model:value="editableData[record.file_id]['file_name']"
+ style="margin: -5px 0"
+ />
+ <template v-else>
+ <a-tooltip :title="text">
+ <a @click="viewFile(record.object_key)">{{ text }}</a>
+ </a-tooltip>
+ </template>
+ </div>
+
+ </template>
+ <template #original="{ text }">
+ {{ text }}
+ </template>
+ <template #action="{ record }">
+
+ <div class="editable-row-operations">
+ <!-- 编辑态操作 -->
+ <div v-if="editableData[record.file_id]">
+ <a-tooltip title="保存">
+ <span @click="save(record)" style="color: #28d445;"><CheckOutlined/></span>
+ </a-tooltip>
+ <a-tooltip title="取消">
+ <span @click="() => delete editableData[record.file_id]" style="color: #e70102;"><CloseOutlined/></span>
+ </a-tooltip>
+ </div>
+ <!-- 非编辑态操作 -->
+ <div v-else class="flex-align-center flex-row" style="color: #2d8cf0">
+ <a-tooltip title="下载">
+ <a class="fz18" @click="downloadMedia(record)">
+ <DownloadOutlined/>
+ </a>
+ </a-tooltip>
+
+ <a-tooltip title="编辑">
+ <a class="fz18" @click="edit(record)">
+ <EditOutlined/>
+ </a>
+ </a-tooltip>
+ </div>
+ </div>
+
+ </template>
+ </a-table>
+ </div>
+ </a-spin>
+
+ <div @click.self="showVideo = false" v-if="showVideo" class="modal">
+ <video class="video-js" :id="videoPlayerId"></video>
</div>
- <div class="search-part">
- <a-input-search :size="searchPanelOptions.size" v-model:value="searchQuery.name" @change="inputChange" placeholder="按文件名称搜索" style="width: 300px"/>
- </div>
-
- </div>
-
- <a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
- <div class="media-panel-wrapper">
- <a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint"
- :pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
- <template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col">
- <a-tooltip :title="text">
- <a v-if="col === 'name'">{{ text }}</a>
- <span v-else>{{ text }}</span>
- </a-tooltip>
- </template>
- <template #original="{ text }">
- {{ text }}
- </template>
- <template #action="{ record }">
- <a-tooltip title="download">
- <a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a>
- </a-tooltip>
- </template>
- </a-table>
- </div>
- </a-spin>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { TableState } from 'ant-design-vue/lib/table/interface'
-import { onMounted, reactive } from 'vue'
+import { onMounted, reactive, UnwrapRef } from 'vue'
import { IPage } from '../api/http/type'
import { ELocalStorageKey } from '../types/enums'
import { downloadFile } from '../utils/common'
-import { downloadMediaFile, getMediaFiles, MediaQueryParam } from '/@/api/media'
-import { DownloadOutlined } from '@ant-design/icons-vue'
+import { downloadMediaFile, getMediaFiles, MediaQueryParam, updateMediaFile } from '/@/api/media'
+import { DownloadOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
import { message, Pagination } from 'ant-design-vue'
-import { TaskStatus, TaskStatusMap } from '/@/types/task'
+import { ColumnProps } from 'ant-design-vue/es/table/interface'
+import JSZip from 'jszip'
+import { saveAs } from 'file-saver'
+import * as VueViewer from 'v-viewer' // 引入js
+import 'viewerjs/dist/viewer.css' // 引入css
+import { api as viewerApi } from 'v-viewer'
+import axios from 'axios'
+import videojs from 'video.js'
+
+type Key = ColumnProps['key'];
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const loading = ref(false)
-
+const showVideo = ref(false)
+const videoPlayerId = ref('videoPlayerId')
+// 文件前缀
+const prefix = 'https://dev.jxpskj.com:8026/cloud-bucket'
// 搜索栏配置项
const searchPanelOptions = reactive({
size: 'large',
@@ -70,22 +132,34 @@
valueFormat: 'YYYY-MM-DD'
})
+interface MediaFile {
+ fingerprint: string,
+ drone: string,
+ payload: string,
+ is_original: string,
+ file_name: string,
+ file_path: string,
+ create_time: string,
+ file_id: string,
+ object_key: string
+}
+
const subFileTypeOptions = [
- { value: 0, label: '普通图片' },
- { value: 1, label: '全景图' },
+ // { value: 0, label: '普通图片' },
+ // { value: 1, label: '全景图' },
]
const payloadOptions = [
+ { value: 'M30T Camera', label: 'M30T Camera' },
]
const timeRangeArr = reactive({
data: [] as string[]
})
-const searchQuery = reactive<MediaQueryParam>({
-})
+const searchQuery = reactive<MediaQueryParam>({})
const subFileTypeArr = reactive([])
const payloadArr = reactive([])
-
+const editableData: UnwrapRef<Record<string, MediaFile>> = reactive({})
const columns = [
{
title: '文件名称',
@@ -103,18 +177,9 @@
title: '拍摄负载',
dataIndex: 'payload'
},
- {
- title: '大小',
- dataIndex: 'size',
- },
// {
- // title: '拍摄负载',
- // dataIndex: 'drone'
- // },
- // {
- // title: 'Original',
- // dataIndex: 'is_original',
- // slots: { customRender: 'original' }
+ // title: '大小',
+ // dataIndex: 'size',
// },
{
title: '创建时间',
@@ -139,18 +204,11 @@
total: 0
})
-type Pagination = TableState['pagination']
+const selectedRow = reactive({
+ list: [] as MediaFile[]
+})
-interface MediaFile {
- fingerprint: string,
- drone: string,
- payload: string,
- is_original: string,
- file_name: string,
- file_path: string,
- create_time: string,
- file_id: string,
-}
+type Pagination = TableState['pagination']
const mediaData = reactive({
data: [] as MediaFile[]
@@ -160,30 +218,218 @@
getFiles()
})
-function dateChange (value:any) {
+const rowSelection = {
+ onChange: (selectedRowKeys: Key[], selectedRows: any[]) => {
+ selectedRow.list = selectedRows
+ },
+}
+
+function edit (record: MediaFile) {
+ editableData[record.file_id] = record
+}
+
+// 保存
+function save (record: MediaFile) {
+ delete editableData[record.file_id]
+ // updateDevice({ nickname: record.nickname }, workspaceId.value, record.device_sn)
+ updateMediaFile(workspaceId, { fileName: record.file_name, fileId: record.file_id }).then(res => {
+ if (res.code === 0) {
+ message.success('修改成功')
+ } else {
+ message.error('修改失败')
+ }
+ }).catch(err => {
+ console.log(err)
+ message.error('修改失败')
+ })
+}
+
+function viewFile (objectKey: string) {
+ const ext = objectKey.split('.')[1]
+ const fileType = getFileType(ext)
+ let url = ''
+ if (!objectKey.startsWith('/')) {
+ url = prefix + '/' + objectKey
+ } else {
+ url = prefix + objectKey
+ }
+ console.log('文件地址:', url)
+
+ if (fileType === 'image') {
+ viewImage(url)
+ } else if (fileType === 'video') {
+ viewVideo(url)
+ }
+}
+
+function getFileType (ext: string) {
+ // 获取类型结果
+ let result = ''
+ // fileName无后缀返回 false
+ if (!ext) {
+ return -1
+ }
+ ext = ext.toLocaleLowerCase()
+ // 图片格式
+ const imglist = ['png', 'jpg', 'jpeg', 'bmp', 'gif']
+ // 进行图片匹配
+ result = imglist.find(item => item === ext)
+ if (result) {
+ return 'image'
+ }
+ // 匹配txt
+ const txtlist = ['txt']
+ result = txtlist.find(item => item === ext)
+ if (result) {
+ return 'txt'
+ }
+ // 匹配 excel
+ const excelist = ['xls', 'xlsx']
+ result = excelist.find(item => item === ext)
+ if (result) {
+ return 'excel'
+ }
+ // 匹配 word
+ const wordlist = ['doc', 'docx']
+ result = wordlist.find(item => item === ext)
+ if (result) {
+ return 'word'
+ }
+ // 匹配 pdf
+ const pdflist = ['pdf']
+ result = pdflist.find(item => item === ext)
+ if (result) {
+ return 'pdf'
+ }
+ // 匹配 ppt
+ const pptlist = ['ppt', 'pptx']
+ result = pptlist.find(item => item === ext)
+ if (result) {
+ return 'ppt'
+ }
+ // 匹配 视频
+ const videolist = ['mp4', 'm2v', 'mkv', 'rmvb', 'wmv', 'avi', 'flv', 'mov', 'm4v', 'ogv']
+ result = videolist.find(item => item === ext)
+ if (result) {
+ return 'video'
+ }
+ // 匹配 音频
+ const radiolist = ['mp3', 'wav', 'wmv']
+ result = radiolist.find(item => item === ext)
+ if (result) {
+ return 'radio'
+ }
+ // 其他 文件类型
+ return 'other'
+}
+
+function viewImage (url: string) {
+ viewerApi({
+ images: [url]
+ })
+}
+
+async function viewVideo (url: string) {
+ showVideo.value = true
+ await nextTick()
+ const player = videojs(videoPlayerId.value, {
+ autoplay: true, // 自动播放
+ controls: true, // 控件 设置为true,控件才会显示
+ fullscreenToggle: true, // 是否显示全屏按钮
+ playToggle: true, // 是否显示播放按钮
+ progressControl: true, // 是否显示进度条。除了boolean,还可以设置一个ProgressControlOptions对象,更详细的配置进度条。
+ volumePanel: true, // 是否显示音量。除了boolean,还可以设置一个VolumePanelOptions对象,更详细的配置音量组件。
+ pictureInPictureToggle: false, // 是否显示画中画按钮
+ remainingTimeDisplay: true, // 是否显示时长
+
+ })
+
+ player.src(url)
+ player.on('ended', () => {
+ showVideo.value = false
+ })
+
+ player.on('error', () => {
+ const error = player.error()
+ console.log('video error:' + error.code + '-' + error.message)
+ })
+}
+
+// 打包选中项
+function compress () {
+ if (selectedRow.list.length === 0) {
+ message.warning('请先选择一条数据')
+ } else {
+ // http://dev.jxpskj.com:9000/cloud-bucket/a03c34be-1e4d-45c7-8e4b-0b4c14dbb334/DJI_202309251910_002_a03c34be-1e4d-45c7-8e4b-0b4c14dbb334/DJI_20230925191305_0006_W.jpeg
+ const zip = new JSZip()
+ const promiseList = [] as any
+ selectedRow.list.forEach(e => {
+ const url = prefix + e.object_key
+ const promise = getFile(url, e.file_name)
+ promiseList.push(promise)
+ })
+
+ Promise.all(promiseList).then(res => {
+ console.log(res, '+++++++++++')
+ res.forEach(e => {
+ zip.file(e.name, e.data, { base64: true })
+ })
+ zip.generateAsync({ type: 'blob' })
+ .then(function (content) {
+ // see FileSaver.js
+ saveAs(content, '打包文件.zip')
+ })
+ })
+ }
+}
+
+// 获取文件的blob
+function getFile (url: string, name: string) {
+ return new Promise((resolve, reject) => {
+ axios({
+ method: 'get',
+ url,
+ responseType: 'blob',
+ })
+ .then((response) => {
+ const ext = url.split('.')[1]
+ // 若文件名不以后缀结尾,则手动添加后缀
+ resolve({
+ name: name,
+ data: response.data
+ })
+ })
+ .catch((error) => {
+ reject(error.toString())
+ })
+ })
+}
+
+function dateChange (value: any) {
searchQuery.startTime = value[0]
searchQuery.endTime = value[1]
getFiles()
}
-function subFileTypeChange (value:any) {
+function subFileTypeChange (value: any) {
searchQuery.subFileType = value.join(',')
getFiles()
}
-function payloadChange (value:any) {
+function payloadChange (value: any) {
searchQuery.payload = value.join(',')
getFiles()
}
-function getFiles () {
- getMediaFiles(workspaceId, body).then(res => {
- console.log(res, '-------------------')
+function inputChange (searchValue: string) {
+ getFiles()
+}
+function getFiles () {
+ getMediaFiles(workspaceId, body, searchQuery).then(res => {
mediaData.data = res.data.list
paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page
- console.info(mediaData.data[0])
})
}
@@ -195,53 +441,92 @@
function downloadMedia (media: MediaFile) {
loading.value = true
- downloadMediaFile(workspaceId, media.file_id).then(res => {
- if (!res) {
- return
- }
- const data = new Blob([res])
- downloadFile(data, media.file_name)
- }).finally(() => {
+ const url = prefix + '/' + media.object_key
+ getFile(url, media.file_name).then(res => {
+ downloadFile(res.data, res.name)
loading.value = false
})
+
+ // downloadMediaFile(workspaceId, media.file_id).then(res => {
+ // if (!res) {
+ // return
+ // }
+ //
+ // const data = new Blob([url])
+ // downloadFile(data, media.file_name)
+ // loading.value = false
+ // }).finally(() => {
+ // loading.value = false
+ // })
}
</script>
<style lang="scss" scoped>
.media-panel-wrapper {
- width: 100%;
- padding: 16px;
- .media-table {
- background: #fff;
- margin-top: 10px;
- }
- .action-area {
- color: $primary;
- cursor: pointer;
- }
+ width: 100%;
+ padding: 16px;
+
+ .media-table {
+ background: #fff;
+ margin-top: 10px;
+ }
+
+ .action-area {
+ color: $primary;
+ cursor: pointer;
+ }
}
+
.header {
- width: 100%;
- height: 60px;
- background: #fff;
- padding: 16px;
- font-size: 20px;
- font-weight: bold;
- text-align: start;
- color: #000;
+ width: 100%;
+ height: 60px;
+ background: #fff;
+ padding: 16px;
+ font-size: 20px;
+ font-weight: bold;
+ text-align: start;
+ color: #000;
}
.search-panel-wrapper {
- height: 85px;
- background: #ffffff;
- padding: 0 20px;
- display: flex;
- align-items: center;
+ height: 85px;
+ background: #ffffff;
+ padding: 0 20px;
+ display: flex;
+ align-items: center;
- .search-part{
- margin-right: 10px;
- }
+ .search-part {
+ margin-right: 10px;
+ }
}
+
+.button-wrapper {
+ background: #ffffff;
+ padding-left: 20px;
+}
+
+.editable-row-operations {
+ div > span {
+ margin-right: 10px;
+ }
+}
+
+.modal {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ #videoPlayerId {
+ width: 70%;
+ height: 80%;
+ }
+}
</style>
--
Gitblit v1.9.3