| | |
| | | <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> |
| | | |
| | | <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 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> |
| | | |
| | | <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> |
| | | <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="button-wrapper"> |
| | | <a-button type="primary" @click="compress">压缩打包</a-button> |
| | | </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" |
| | | 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> |
| | | |
| | | </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:9000/cloud-bucket' |
| | | // 搜索栏配置项 |
| | | const searchPanelOptions = reactive({ |
| | | size: 'large', |
| | |
| | | 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 = [] |
| | | const payloadOptions = [ |
| | | { value: 'M30T Camera', label: 'M30T Camera' }, |
| | | ] |
| | | |
| | | const timeRangeArr = reactive({ |
| | | data: [] as string[] |
| | |
| | | const searchQuery = reactive<MediaQueryParam>({}) |
| | | const subFileTypeArr = reactive([]) |
| | | const payloadArr = reactive([]) |
| | | |
| | | const editableData: UnwrapRef<Record<string, MediaFile>> = reactive({}) |
| | | const columns = [ |
| | | { |
| | | title: '文件名称', |
| | |
| | | title: '拍摄负载', |
| | | dataIndex: 'payload' |
| | | }, |
| | | { |
| | | title: '大小', |
| | | dataIndex: 'size', |
| | | }, |
| | | // { |
| | | // title: '拍摄负载', |
| | | // dataIndex: 'drone' |
| | | // }, |
| | | // { |
| | | // title: 'Original', |
| | | // dataIndex: 'is_original', |
| | | // slots: { customRender: 'original' } |
| | | // title: '大小', |
| | | // dataIndex: 'size', |
| | | // }, |
| | | { |
| | | title: '创建时间', |
| | |
| | | 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[] |
| | |
| | | onMounted(() => { |
| | | getFiles() |
| | | }) |
| | | |
| | | 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 => { |
| | | 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] |
| | | // 若文件名不以后缀结尾,则手动添加后缀 |
| | | if (!name.endsWith(ext)) { |
| | | name = name + '.' + ext |
| | | } |
| | | |
| | | resolve({ |
| | | name: name, |
| | | data: response.data |
| | | }) |
| | | }) |
| | | .catch((error) => { |
| | | reject(error.toString()) |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | function dateChange (value: any) { |
| | | searchQuery.startTime = value[0] |
| | |
| | | mediaData.data = res.data.list |
| | | paginationProp.total = res.data.pagination.total |
| | | paginationProp.current = res.data.pagination.page |
| | | console.info(mediaData.data[0]) |
| | | }) |
| | | } |
| | | |
| | |
| | | if (!res) { |
| | | return |
| | | } |
| | | |
| | | const data = new Blob([res]) |
| | | downloadFile(data, media.file_name) |
| | | }).finally(() => { |
| | | loading.value = false |
| | | }).finally(() => { |
| | | |
| | | }) |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | } |
| | | |
| | | .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> |