This commit is contained in:
2026-03-04 19:00:06 +08:00
parent af0b0c5070
commit 2d7ba035ca
6 changed files with 391 additions and 101 deletions

View File

@@ -164,6 +164,11 @@
<div class="material-item__overlay">
<span class="material-item__type-tag">{{ getFileTypeText(file.fileName) }}</span>
</div>
<!-- hover 播放按钮 -->
<div class="material-item__play-overlay" @click.stop="handlePreview(file)">
<PlayCircleOutlined />
<span v-if="file.duration" class="duration-tag">{{ formatDuration(file.duration) }}</span>
</div>
</div>
<!-- 文件信息 -->
@@ -261,6 +266,13 @@
</a-form-item>
</a-form>
</a-modal>
<!-- 视频预览弹窗 -->
<VideoPreviewModal
v-model:open="previewVisible"
:video-url="previewUrl"
:title="previewTitle"
/>
</div>
</FullWidthLayout>
</template>
@@ -279,13 +291,15 @@ import {
VideoCameraOutlined,
UserOutlined,
CheckOutlined,
CheckSquareOutlined
CheckSquareOutlined,
PlayCircleOutlined
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
import MaterialService, { MaterialGroupService } from '@/api/material';
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue';
import { useUserStore } from '@/stores/user';
import VideoPreviewModal from '@/components/VideoPreviewModal.vue';
// 用户状态(获取存储配额)
const userStore = useUserStore()
@@ -313,6 +327,11 @@ const createGroupForm = reactive({
// 文件选择
const selectedFileIds = ref([])
// 视频预览状态
const previewVisible = ref(false)
const previewUrl = ref('')
const previewTitle = ref('')
// 编辑状态
const editingFileId = ref(null)
const editingDisplayName = ref('')
@@ -398,6 +417,22 @@ const handleSaveGroupName = async (group) => {
}
}
// 通用的确认删除弹窗
const showConfirmDelete = (title, content) => {
return new Promise((resolve) => {
Modal.confirm({
title,
content,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
centered: true,
onOk: () => resolve(true),
onCancel: () => resolve(false),
})
})
}
const handleDeleteGroup = async (group, event) => {
event?.stopPropagation?.()
@@ -407,39 +442,17 @@ const handleDeleteGroup = async (group, event) => {
return
}
const confirmed = await new Promise((resolve) => {
Modal.confirm({
title: '删除分组',
content: `确定要删除分组「${group.name}」吗?此操作不可恢复。`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
centered: true,
class: 'delete-group-modal',
onOk() {
resolve(true)
},
onCancel() {
resolve(false)
},
})
})
const confirmed = await showConfirmDelete('删除分组', `确定要删除分组「${group.name}」吗?此操作不可恢复。`)
if (!confirmed) return
try {
loading.value = true
await MaterialGroupService.deleteGroup(group.id)
// 移除已删除的分组
const index = groupList.value.findIndex(g => g.id === group.id)
if (index > -1) {
groupList.value.splice(index, 1)
}
// 重新选择分组
// 移除已删除的分组并重新选择
groupList.value = groupList.value.filter(g => g.id !== group.id)
if (selectedGroupId.value === group.id) {
selectedGroupId.value = groupList.value.length > 0 ? groupList.value[0].id : null
selectedGroupId.value = groupList.value[0]?.id ?? null
await loadFileList()
}
@@ -582,34 +595,16 @@ const handleBatchDelete = async () => {
const count = selectedFileIds.value.length
const fileIdsToDelete = [...selectedFileIds.value]
const confirmed = await new Promise((resolve) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${count} 个文件吗?此操作不可恢复。`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
centered: true,
class: 'batch-delete-modal',
onOk() {
resolve(true)
},
onCancel() {
resolve(false)
},
})
})
const confirmed = await showConfirmDelete('确认删除', `确定要删除选中的 ${count} 个文件吗?此操作不可恢复。`)
if (!confirmed) return
try {
loading.value = true
await MaterialService.deleteFiles(fileIdsToDelete)
// 移除已删除的文件
// 移除已删除的文件并更新状态
fileList.value = fileList.value.filter(file => !fileIdsToDelete.includes(file.id))
// 更新状态
totalFileCount.value = Math.max(0, totalFileCount.value - count)
selectedFileIds.value = []
@@ -618,7 +613,7 @@ const handleBatchDelete = async () => {
// 如果删除后当前页没有数据了,则加载上一页
if (fileList.value.length === 0 && pagination.current > 1) {
pagination.current = pagination.current - 1
pagination.current--
await loadFileList()
}
@@ -631,14 +626,12 @@ const handleBatchDelete = async () => {
}
}
const formatFileSize = (size) => {
const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
const index = Math.floor(Math.log(bytes) / Math.log(1024))
const size = bytes / Math.pow(1024, index)
return `${size.toFixed(1)} ${units[index]}`
}
const formatDate = (date) => {
@@ -707,6 +700,43 @@ const handleImageError = (e) => {
e.target.style.display = 'none'
}
// 视频预览
const handlePreview = async (file) => {
if (!file.id) {
message.warning('文件信息不完整')
return
}
// 显示加载提示
const hideLoading = message.loading('正在获取视频地址...', 0)
try {
// 调用后端API获取带签名的视频播放URL
const res = await MaterialService.getVideoPlayUrl(file.id)
hideLoading()
if (res.code === 0 && res.data) {
previewUrl.value = res.data
previewTitle.value = file.displayName || file.fileName || '视频预览'
previewVisible.value = true
} else {
message.error(res.msg || '获取视频地址失败')
}
} catch (error) {
hideLoading()
console.error('获取视频播放URL失败:', error)
message.error('获取视频地址失败,请重试')
}
}
// 格式化时长
const formatDuration = (seconds) => {
if (!seconds) return ''
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
}
// 监听分类变化
watch(activeCategory, () => {
selectedFileIds.value = []
@@ -1251,6 +1281,37 @@ onMounted(async () => {
transition: opacity var(--duration-slow) ease;
}
&__play-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
background: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity var(--duration-base);
cursor: pointer;
.anticon {
font-size: 48px;
color: white;
}
.duration-tag {
padding: 2px 8px;
background: rgba(0, 0, 0, 0.6);
border-radius: 4px;
color: white;
font-size: var(--font-size-xs);
}
}
&__preview:hover &__play-overlay {
opacity: 1;
}
&__type-tag {
font-size: var(--font-size-xs);
font-weight: 600;
@@ -1358,8 +1419,7 @@ onMounted(async () => {
// ========================================
// 弹窗样式
// ========================================
:deep(.batch-delete-modal),
:deep(.delete-group-modal) {
:deep(.ant-modal-confirm) {
.ant-modal-content {
border-radius: var(--radius-lg);
overflow: hidden;