完成原始数据回流后查看界面开发和功能开发,支持对所选图片的关联音频进行播放|修复典型图片设置不生效的问题

This commit is contained in:
wxy 2025-07-26 16:58:21 +08:00
parent 7728cfd8aa
commit 0108dd6068
4 changed files with 655 additions and 336 deletions

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -183,12 +183,11 @@ const handleSave = async () => {
console.log("requestData:",requestData);
//
const response = await axios.put(
const response = await axios.post(
`http://pms.dtyx.net:9158/image/setting-info/${editingData.value.partId}`,
requestData
)
if (response.data && response.data.code === 0) {
if (response.data && response.data.code === 200 && response.data.success) {
Message.success('图片信息保存成功')
emit('save', editingData.value)
emit('update:previewModalVisible', false)

View File

@ -0,0 +1,335 @@
<template>
<!-- 图片预览模态框 -->
<a-modal
v-model:visible="previewModalVisible"
:width="1000"
:footer="false"
title="多媒体预览"
class="preview-modal"
@close="handleModalClose"
>
<div class="preview-container">
<!-- 图片预览区域 -->
<div class="image-section">
<img
:src="currentPreviewItem?.url"
:alt="currentPreviewItem?.name"
class="preview-image"
/>
</div>
<!-- 音频列表区域 -->
<div class="audio-section">
<h3 class="audio-title">关联音频 ({{ audioList.length }})</h3>
<div class="audio-list">
<div
v-for="(audio, index) in audioList"
:key="index"
class="audio-item"
:class="{ 'active': currentAudioIndex === index }"
>
<div class="audio-info">
<span class="audio-name">{{ audio.audioId || `音频 ${index + 1}` }}</span>
<span class="audio-duration">{{ formatDuration(audio.duration) }}</span>
</div>
<div class="audio-controls">
<a-button
size="mini"
@click="toggleAudio(index)"
:loading="audioLoading && currentAudioIndex === index"
>
{{ currentAudioIndex === index && isPlaying ? '暂停' : '播放' }}
</a-button>
<a-progress
v-if="currentAudioIndex === index"
:percent="playProgress"
:show-text="false"
size="small"
class="progress-bar"
/>
</div>
<!-- 隐藏的audio元素 -->
<audio
ref="audioPlayers"
:src="audio.url"
@timeupdate="handleTimeUpdate"
@ended="handleAudioEnded"
preload="metadata"
/>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { Message } from '@arco-design/web-vue'
interface AudioItem {
url: string
name?: string
duration?: number //
}
interface PreviewItem {
url: string
name: string
audios?: AudioItem[]
}
//
const previewModalVisible = ref(false)
const currentPreviewItem = ref<PreviewItem | null>(null)
//
const audioList = ref<AudioItem[]>([])
const currentAudioIndex = ref(-1)
const isPlaying = ref(false)
const audioLoading = ref(false)
const playProgress = ref(0)
const audioPlayers = ref<HTMLAudioElement[]>([])
const getAudioUrl = (filePath: string): string => {
if (!filePath) return ''
if (filePath.startsWith('http')) return filePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${filePath}`
}
//
const openPreview = (item: PreviewItem) => {
currentPreviewItem.value = item
audioList.value = []
for (const audio of item.audios) {
let temp={
audioId:audio.audioId,
url:getAudioUrl(audio.filePath)
}
audioList.value.push(temp)
}
previewModalVisible.value = true
resetAudioState()
//
nextTick(() => {
loadAudioDurations()
})
}
//
const handleModalClose = () => {
stopAudio()
previewModalVisible.value = false
}
//
const resetAudioState = () => {
currentAudioIndex.value = -1
isPlaying.value = false
playProgress.value = 0
}
//
const loadAudioDurations = () => {
audioPlayers.value.forEach((player, index) => {
player.onloadedmetadata = () => {
if (!audioList.value[index].duration) {
audioList.value[index].duration = player.duration
}
}
})
}
//
const toggleAudio = (index: number) => {
if (currentAudioIndex.value === index) {
// /
if (isPlaying.value) {
pauseAudio()
} else {
playAudio(index)
}
} else {
//
stopAudio()
playAudio(index)
}
}
//
const playAudio = (index: number) => {
audioLoading.value = true
currentAudioIndex.value = index
const player = audioPlayers.value[index]
player.play()
.then(() => {
isPlaying.value = true
audioLoading.value = false
})
.catch(err => {
console.error('播放失败:', err)
Message.error('音频播放失败')
audioLoading.value = false
})
}
//
const pauseAudio = () => {
const player = audioPlayers.value[currentAudioIndex.value]
player.pause()
isPlaying.value = false
}
//
const stopAudio = () => {
if (currentAudioIndex.value !== -1) {
const player = audioPlayers.value[currentAudioIndex.value]
player.pause()
player.currentTime = 0
}
isPlaying.value = false
currentAudioIndex.value = -1
playProgress.value = 0
}
//
const handleTimeUpdate = (e: Event) => {
const player = e.target as HTMLAudioElement
if (player.duration) {
playProgress.value = (player.currentTime / player.duration) * 100
}
}
//
const handleAudioEnded = () => {
isPlaying.value = false
playProgress.value = 100
}
//
const formatDuration = (seconds?: number) => {
if (!seconds) return '--:--'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs < 10 ? '0' : ''}${secs}`
}
//
defineExpose({
openPreview
})
</script>
<style scoped lang="less">
.preview-modal {
:deep(.arco-modal-content) {
padding: 0;
}
}
.preview-container {
display: flex;
height: 70vh;
max-height: 800px;
}
.image-section {
flex: 1;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
overflow: hidden;
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.audio-section {
width: 350px;
border-left: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
.audio-title {
padding: 16px;
margin: 0;
font-size: 16px;
border-bottom: 1px solid #e5e5e5;
}
.audio-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.audio-item {
padding: 12px 16px;
transition: background-color 0.2s;
&.active {
background-color: #f0f7ff;
}
&:hover {
background-color: #f9f9f9;
}
}
.audio-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.audio-name {
font-weight: 500;
}
.audio-duration {
color: #8c8c8c;
font-size: 12px;
}
}
.audio-controls {
display: flex;
align-items: center;
gap: 12px;
.progress-bar {
flex: 1;
}
}
}
@media (max-width: 1200px) {
.preview-container {
flex-direction: column;
height: 80vh;
}
.image-section {
height: 60%;
}
.audio-section {
width: 100%;
height: 40%;
border-left: none;
border-top: 1px solid #e5e5e5;
}
}
</style>

View File

@ -1,60 +1,77 @@
<template>
<GiPageLayout>
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">数据入库 - 1号机组叶片A型检查</h2>
</div>
<!-- <div class="page-header">
<h2 class="page-title">图像音频关联查看</h2>
</div>-->
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs v-model:active-key="activeTab" type="line">
<a-tab-pane key="image" tab="图片" title="图片">
<a-tab-pane key="image" tab="列表" title="列表">
<div class="tab-content">
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
:custom-request="customUpload"
:show-file-list="false"
multiple
:accept="getAcceptType()"
@change="handleFileChange"
drag
class="upload-dragger"
>
<div class="upload-content">
<div class="upload-icon">
<icon-upload :size="48" />
</div>
<div class="upload-text">
<p class="primary-text">将文件拖拽到此处<span class="link-text">点击上传</span></p>
<p class="secondary-text">支持上传任意不超过100MB格式文件</p>
</div>
</div>
</a-upload>
<!-- 筛选条件区域 -->
<div class="filter-section">
<a-space size="large">
<!-- 项目选择 -->
<div class="filter-item">
<span class="filter-label">项目</span>
<a-select
v-model="filterParams.project"
placeholder="请选择项目"
:options="projectOptions"
allow-search
allow-clear
:loading="loading.project"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
<!-- 操作按钮区域 -->
<div class="action-buttons">
<a-button
type="primary"
@click="startUpload"
:loading="uploading"
:disabled="uploadQueue.length === 0"
>
开始上传
</a-button>
<a-button @click="clearFiles">清空文件</a-button>
<a-button @click="batchImport">批量导入...</a-button>
<!-- 机组选择 -->
<div class="filter-item">
<span class="filter-label">机组</span>
<a-select
v-model="filterParams.unit"
placeholder="请先选择项目"
:options="unitOptions"
allow-search
allow-clear
:disabled="!filterParams.project"
:loading="loading.unit"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
<!-- 部件选择 -->
<div class="filter-item">
<span class="filter-label">部件</span>
<a-select
v-model="filterParams.component"
placeholder="请先选择机组"
:options="componentOptions"
allow-search
allow-clear
:disabled="!filterParams.unit"
:loading="loading.component"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
</a-space>
</div>
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<h3 class="section-title">已上传数据</h3>
<a-table
:columns="fileColumns"
:data="uploadedFiles"
:data="imageList"
:pagination="false"
:scroll="{ x: '100%' }"
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }"
:loading="loading.image"
class="scrollable-table"
>
<!-- 文件类型 -->
<template #type="{ record }">
@ -65,13 +82,13 @@
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
<span>{{ record.imageTypeLabel}}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)" size="small">
{{ record.status }}
<a-tag :color="getStatusColor(record.preTreatment)" size="small">
{{ record.preTreatment }}
</a-tag>
</template>
@ -86,66 +103,188 @@
</div>
</div>
</a-tab-pane>
<a-tab-pane key="video" tab="视频" title="视频">
<div class="tab-content">
<a-empty description="视频上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="audio" tab="语音" title="语音">
<div class="tab-content">
<a-empty description="语音上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="document" tab="文档" title="文档">
<div class="tab-content">
<a-empty description="文档上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="other" tab="其他" title="其他">
<div class="tab-content">
<a-empty description="其他文件上传功能开发中..." />
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 文件预览模态框 -->
<a-modal
v-model:visible="previewModalVisible"
title="文件预览"
:width="800"
:footer="false"
>
<div class="preview-container">
<div v-if="previewFileData && previewFileData.type === 'image'" class="image-preview">
<img :src="previewFileData.url" :alt="previewFileData.name" style="max-width: 100%; height: auto;" />
</div>
<div v-else-if="previewFileData && previewFileData.type === 'video'" class="video-preview">
<video :src="previewFileData.url" controls style="max-width: 100%; height: auto;" />
</div>
<div v-else class="file-info">
<p>文件名{{ previewFileData?.name }}</p>
<p>文件类型{{ previewFileData?.type }}</p>
<p>文件大小{{ formatFileSize(previewFileData?.size) }}</p>
</div>
</div>
</a-modal>
<PreviewModal ref="previewModal" />
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ref, reactive, computed,onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import PreviewModal from './components/PreviewModal.vue'
import {
getProjectList,
getTurbineList,
getPartList,
getImageList,
deleteImage,
autoAnnotateImage,
generateReport,
batchUploadImages,
uploadImageToPartV2
} from '@/apis/industrial-image'
const previewModal = ref()
//
const activeTab = ref('image')
//
const filterParams = reactive({
project: null,
unit: null,
component: null
})
//
const projectOptions = ref<Array<{label: string, value: string}>>([])
const unitOptions = ref<Array<{label: string, value: string}>>([])
const componentOptions = ref<Array<{label: string, value: string}>>([])
//
const imageList = ref<Array<{
id: number
name: string
type: string
imageTypeLabel: string
shootingTime: string
preTreatment: string
url: string
}>>([])
//
const loading = reactive({
project: false,
unit: false,
component: false,
image: false
})
//
onMounted(() => {
fetchProjectList()
})
//
if (watch) {
watch(() => filterParams.project, (newVal) => {
//
filterParams.unit = null
filterParams.component = null
unitOptions.value = []
componentOptions.value = []
imageList.value = []
if (newVal) {
fetchTurbineList(newVal)
}
})
}
//
if (watch) {
watch(() => filterParams.unit, (newVal) => {
//
filterParams.component = null
componentOptions.value = []
imageList.value = []
if (newVal && filterParams.project) {
fetchPartList(filterParams.project, newVal)
}
})
}
//
const fetchProjectList = async () => {
loading.project = true
try {
const res = await getProjectList({ page: 1, pageSize: 1000 });
projectOptions.value = res.data.map(item => ({
label: item.projectName,
value: item.projectId
}))
} catch (error) {
Message.error('获取项目列表失败')
} finally {
loading.project = false
}
}
//
const fetchTurbineList = async (projectId: string) => {
loading.unit = true
try {
const res = await getTurbineList({ projectId })
unitOptions.value = res.data.map(item => ({
label: item.turbineName,
value: item.turbineId
}))
} catch (error) {
console.error('获取机组列表失败:', error)
Message.error('获取机组列表失败')
} finally {
loading.unit = false
}
}
//
const fetchPartList = async (projectId: string, turbineId: string) => {
loading.component = true
try {
const res = await getPartList({ projectId, turbineId })
componentOptions.value = res.data.map(item => ({
label: item.partName, //
value: item.partId //
}))
} catch (error) {
console.error('获取部件列表失败:', error)
Message.error('获取部件列表失败')
} finally {
loading.component = false
}
}
//
const handleFilterChange = async () => {
if (!filterParams.unit) return
loading.image = true
try {
let params = {
turbineId: filterParams.unit
}
if(filterParams.component){
params = {
turbineId: filterParams.unit,
partId: filterParams.component
}
}
const res = await getImageList(params)
imageList.value = res.data.map((item: any) => ({
id: item.imageId,
name: item.imageName,
type: item.imageType?item.imageType:"未指定类型",
imageTypeLabel: item.imageTypeLabel,
shootingTime: item.shootingTime,
preTreatment: item.preTreatment?"已审核":"未审核",
imagePath: item.imagePath,
audioList:item.audioList
}))
Message.success(`获取到 ${imageList.value.length} 条图像数据`)
} catch (error) {
Message.error('获取图像列表失败')
} finally {
loading.image = false
}
}
// const response = await getTurbineList({ projectId: node.id })
//
const uploading = ref(false)
const uploadQueue = ref<any[]>([])
@ -155,65 +294,32 @@ const previewModalVisible = ref(false)
const previewFileData = ref<any>(null)
//
const uploadedFiles = ref([
{
id: 1,
name: 'IMG_20231105_1430.jpg',
type: 'image',
size: 3355443, // 3.2MB
uploadTime: '2023-11-05 14:32',
status: '成功',
url: '/api/files/IMG_20231105_1430.jpg'
},
{
id: 2,
name: 'VID_20231106_0915.mp4',
type: 'video',
size: 47185920, // 45.6MB
uploadTime: '2023-11-06 09:18',
status: '成功',
url: '/api/files/VID_20231106_0915.mp4'
},
{
id: 3,
name: 'IMG_20231107_1645.jpg',
type: 'image',
size: 2936013, // 2.8MB
uploadTime: '2023-11-07 16:48',
status: '成功',
url: '/api/files/IMG_20231107_1645.jpg'
}
])
const uploadedFiles = ref([])
//
const fileColumns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'name', width: 250 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 100 },
{ title: '大小', dataIndex: 'size', slotName: 'size', width: 100 },
{ title: '上传时间', dataIndex: 'uploadTime', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '类型描述', dataIndex: 'imageTypeLabel', slotName: 'imageTypeLabel', width: 100 },
{ title: '上传时间', dataIndex: 'shootingTime', width: 150 },
{ title: '状态', dataIndex: 'preTreatment', slotName: 'preTreatment', width: 100 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const getAcceptType = () => {
const typeMap: Record<string, string> = {
'image': 'image/*',
'video': 'video/*',
'audio': 'audio/*',
'document': '.pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx',
'other': '*'
}
return typeMap[activeTab.value] || '*'
}
//
const filteredFiles = computed(() => {
return uploadedFiles.value.filter(file =>
(filterParams.project === null || file.project === filterParams.project) &&
(filterParams.unit === null || file.unit === filterParams.unit) &&
(filterParams.component === null || file.component === filterParams.component)
)
})
//
const getFileTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'image': 'blue',
'video': 'green',
'audio': 'orange',
'document': 'purple',
'TYPICAL': 'blue',
'DEFECT': 'orange',
'other': 'gray'
}
return colorMap[type] || 'gray'
@ -221,11 +327,10 @@ const getFileTypeColor = (type: string) => {
//
const getStatusColor = (status: string) => {
console.log(status);
const colorMap: Record<string, string> = {
'成功': 'green',
'失败': 'red',
'上传中': 'blue',
'等待': 'orange'
'已审核': 'green',
'未审核': 'red'
}
return colorMap[status] || 'gray'
}
@ -239,107 +344,30 @@ const formatFileSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
}
//
const customUpload = (option: any) => {
const { file } = option
//
if (file.size > 100 * 1024 * 1024) {
Message.error('文件大小不能超过100MB')
return
}
//
uploadQueue.value.push({
file,
name: file.name,
size: file.size,
type: getFileType(file.name),
status: '等待'
})
Message.success(`文件 ${file.name} 已添加到上传队列`)
}
//
const getFileType = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension || '')) {
return 'image'
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(extension || '')) {
return 'video'
} else if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension || '')) {
return 'audio'
} else if (['pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx'].includes(extension || '')) {
return 'document'
} else {
return 'other'
}
}
//
const handleFileChange = (fileList: any[]) => {
//
}
//
const startUpload = async () => {
if (uploadQueue.value.length === 0) {
Message.warning('请先选择要上传的文件')
return
}
uploading.value = true
try {
//
for (let i = 0; i < uploadQueue.value.length; i++) {
const queueItem = uploadQueue.value[i]
queueItem.status = '上传中'
//
await new Promise(resolve => setTimeout(resolve, 1000))
//
uploadedFiles.value.push({
id: Date.now() + i,
name: queueItem.name,
type: queueItem.type,
size: queueItem.size,
uploadTime: new Date().toLocaleString(),
status: '成功',
url: `/api/files/${queueItem.name}`
})
}
uploadQueue.value = []
Message.success('所有文件上传完成')
} catch (error) {
Message.error('上传失败,请重试')
} finally {
uploading.value = false
}
}
//
const clearFiles = () => {
uploadQueue.value = []
Message.success('已清空文件队列')
}
//
const batchImport = () => {
Message.info('批量导入功能开发中...')
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${imagePath}`
}
//
const previewFile = (file: any) => {
previewFileData.value = file
previewModalVisible.value = true
/* previewFileData.value = file
previewModalVisible.value = true*/
const fileObj ={
id: file.id,
name: file.name,
url: getImageUrl(file.imagePath),
audios: file.audioList
}
previewModal.value.openPreview(fileObj)
}
//
const deleteFile = (file: any) => {
console.log(index);
const index = uploadedFiles.value.findIndex(f => f.id === file.id)
if (index > -1) {
uploadedFiles.value.splice(index, 1)
@ -377,65 +405,22 @@ const deleteFile = (file: any) => {
margin-top: 16px;
}
.upload-section {
.filter-section {
margin-bottom: 16px;
.upload-dragger {
:deep(.arco-upload-drag) {
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 12px;
background: #fafafa;
padding: 40px;
text-align: center;
transition: all 0.3s ease;
border-radius: 4px;
&:hover {
border-color: #1890ff;
background: #f0f7ff;
}
}
}
.upload-content {
display: flex;
flex-direction: column;
.filter-item {
display: inline-flex;
align-items: center;
gap: 16px;
.upload-icon {
color: #bfbfbf;
font-size: 48px;
}
.upload-text {
.primary-text {
font-size: 16px;
color: #595959;
margin: 0 0 8px 0;
.link-text {
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.secondary-text {
.filter-label {
font-size: 14px;
color: #8c8c8c;
margin: 0;
color: #595959;
margin-right: 8px;
}
}
}
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.uploaded-files-section {