Files
social-auto-upload/sau_frontend/src/views/MaterialManagement.vue
T
Deroino 1a02b8fa50 feat(frontend): add UUID column to material management table
- Add UUID field extraction in backend /getFiles endpoint
- Extract UUID from file_path (first part before underscore)
- Add UUID column to material management table in frontend
- Set appropriate width for UUID column display
- Maintain data consistency between backend and frontend
2025-11-13 06:02:15 +08:00

558 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="material-management">
<div class="page-header">
<h1>素材管理</h1>
</div>
<div class="material-list-container">
<div class="material-search">
<el-input
v-model="searchKeyword"
placeholder="输入文件名搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleUploadMaterial">上传素材</el-button>
<el-button type="info" @click="fetchMaterials" :loading="false">
<el-icon :class="{ 'is-loading': isRefreshing }"><Refresh /></el-icon>
<span v-if="isRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredMaterials.length > 0" class="material-list">
<el-table :data="filteredMaterials" style="width: 100%">
<el-table-column prop="uuid" label="UUID" width="180" />
<el-table-column prop="filename" label="文件名" width="300" />
<el-table-column prop="filesize" label="文件大小" width="120">
<template #default="scope">
{{ scope.row.filesize }} MB
</template>
</el-table-column>
<el-table-column prop="upload_time" label="上传时间" width="180" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handlePreview(scope.row)">预览</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无素材数据" />
</div>
</div>
<!-- 上传对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传素材"
width="40%"
@close="handleUploadDialogClose"
>
<div class="upload-form">
<el-form label-width="80px">
<el-form-item label="文件名称:">
<el-input
v-model="customFilename"
placeholder="选填 (仅单个文件时生效)"
:disabled="customFilenameDisabled"
clearable
/>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
class="upload-demo"
drag
multiple
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="fileList"
>
<el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持视频图片等格式文件可一次选择多个文件
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="上传列表" v-if="fileList.length > 0">
<div class="upload-file-list">
<div v-for="file in fileList" :key="file.uid" class="upload-file-item">
<span class="file-name">{{ file.name }}</span>
<el-progress
:percentage="uploadProgress[file.uid]?.percentage || 0"
:text-inside="true"
:stroke-width="20"
style="width: 100%; margin-top: 5px;"
>
<span>{{ uploadProgress[file.uid]?.speed || '' }}</span>
</el-progress>
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="isUploading">
{{ isUploading ? '上传中' : '确认上传' }}
</el-button>
</div>
</template>
</el-dialog>
<!-- 预览对话框 -->
<el-dialog
v-model="previewDialogVisible"
title="素材预览"
width="50%"
:top="'10vh'"
>
<div class="preview-container" v-if="currentMaterial">
<div v-if="isVideoFile(currentMaterial.filename)" class="video-preview">
<video controls style="max-width: 100%; max-height: 60vh;">
<source :src="getPreviewUrl(currentMaterial.file_path)" type="video/mp4">
您的浏览器不支持视频播放
</video>
</div>
<div v-else-if="isImageFile(currentMaterial.filename)" class="image-preview">
<img :src="getPreviewUrl(currentMaterial.file_path)" style="max-width: 100%; max-height: 60vh;" />
</div>
<div v-else class="file-info">
<p>文件名: {{ currentMaterial.filename }}</p>
<p>文件大小: {{ currentMaterial.filesize }} MB</p>
<p>上传时间: {{ currentMaterial.upload_time }}</p>
<el-button type="primary" @click="downloadFile(currentMaterial)">下载文件</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { Refresh, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { materialApi } from '@/api/material'
import { useAppStore } from '@/stores/app'
// 获取应用状态管理
const appStore = useAppStore()
// 搜索和状态控制
const searchKeyword = ref('')
const isRefreshing = ref(false)
const isUploading = ref(false)
// 对话框控制
const uploadDialogVisible = ref(false)
const previewDialogVisible = ref(false)
const currentMaterial = ref(null)
// 文件上传
const fileList = ref([])
const customFilename = ref('')
const customFilenameDisabled = computed(() => fileList.value.length > 1)
const uploadProgress = ref({}); // { [uid]: { percentage: 0, speed: '' } }
watch(fileList, (newList) => {
if (newList.length <= 1) {
// If you want to clear the custom name when going back to single file, uncomment below
// customFilename.value = ''
}
});
// 获取素材列表
const fetchMaterials = async () => {
isRefreshing.value = true
try {
const response = await materialApi.getAllMaterials()
if (response.code === 200) {
appStore.setMaterials(response.data)
ElMessage.success('刷新成功')
} else {
ElMessage.error('获取素材列表失败')
}
} catch (error) {
console.error('获取素材列表出错:', error)
ElMessage.error('获取素材列表失败')
} finally {
isRefreshing.value = false
}
}
// 过滤素材
const filteredMaterials = computed(() => {
if (!searchKeyword.value) return appStore.materials
const keyword = searchKeyword.value.toLowerCase()
return appStore.materials.filter(material =>
material.filename.toLowerCase().includes(keyword)
)
})
// 搜索处理
const handleSearch = () => {
// 搜索逻辑已通过计算属性实现
}
// 上传素材
const handleUploadMaterial = () => {
// 清空变量
fileList.value = []
customFilename.value = ''
uploadProgress.value = {};
uploadDialogVisible.value = true
}
// 关闭上传对话框时清空变量
const handleUploadDialogClose = () => {
fileList.value = []
customFilename.value = ''
uploadProgress.value = {};
}
// 文件选择变更
const handleFileChange = (file, uploadFileList) => {
fileList.value = uploadFileList;
const newProgress = {};
for (const f of uploadFileList) {
newProgress[f.uid] = { percentage: 0, speed: '' };
}
uploadProgress.value = newProgress;
}
const handleFileRemove = (file, uploadFileList) => {
fileList.value = uploadFileList;
const newProgress = { ...uploadProgress.value };
delete newProgress[file.uid];
uploadProgress.value = newProgress;
}
// 提交上传
const submitUpload = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要上传的文件')
return
}
isUploading.value = true
for (const file of fileList.value) {
try {
// 确保文件对象存在
if (!file || !file.raw) {
ElMessage.warning(`文件 ${file.name} 对象无效,已跳过`)
continue
}
const formData = new FormData()
formData.append('file', file.raw)
// 只有当只有一个文件时,自定义文件名才生效
if (fileList.value.length === 1 && customFilename.value.trim()) {
formData.append('filename', customFilename.value.trim())
}
let lastLoaded = 0;
let lastTime = Date.now();
const response = await materialApi.uploadMaterial(formData, (progressEvent) => {
const progressData = uploadProgress.value[file.uid];
if (!progressData) return;
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
progressData.percentage = progress;
const currentTime = Date.now();
const timeDiff = (currentTime - lastTime) / 1000; // in seconds
const loadedDiff = progressEvent.loaded - lastLoaded;
if (timeDiff > 0.5) { // Update speed every 0.5 seconds
const speed = loadedDiff / timeDiff; // bytes per second
if (speed > 1024 * 1024) {
progressData.speed = (speed / (1024 * 1024)).toFixed(2) + ' MB/s';
} else {
progressData.speed = (speed / 1024).toFixed(2) + ' KB/s';
}
lastLoaded = progressEvent.loaded;
lastTime = currentTime;
}
})
if (response.code === 200) {
ElMessage.success(`文件 ${file.name} 上传成功`)
const progressData = uploadProgress.value[file.uid];
if(progressData) progressData.speed = '完成';
} else {
ElMessage.error(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`)
}
} catch (error) {
console.error(`上传文件 ${file.name} 出错:`, error)
ElMessage.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`)
}
}
isUploading.value = false
// Keep dialog open to show results
// uploadDialogVisible.value = false
await fetchMaterials()
}
// 预览素材
const handlePreview = async (material) => {
currentMaterial.value = null
previewDialogVisible.value = true
ElMessage.info('加载中...')
try {
// 等待一小段时间以确保对话框已打开
await new Promise(resolve => setTimeout(resolve, 100))
currentMaterial.value = material
} catch (error) {
console.error('预览素材出错:', error)
ElMessage.error('预览加载失败')
previewDialogVisible.value = false
}
}
// 删除素材
const handleDelete = (material) => {
ElMessageBox.confirm(
`确定要删除素材 ${material.filename} 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
const response = await materialApi.deleteMaterial(material.id)
if (response.code === 200) {
appStore.removeMaterial(material.id)
ElMessage.success('删除成功')
} else {
ElMessage.error(response.msg || '删除失败')
}
} catch (error) {
console.error('删除素材出错:', error)
ElMessage.error('删除失败')
}
})
.catch(() => {
// 取消删除
})
}
// 获取预览URL
const getPreviewUrl = (filePath) => {
const filename = filePath.split('/').pop()
return materialApi.getMaterialPreviewUrl(filename)
}
// 下载文件
const downloadFile = (material) => {
const url = materialApi.downloadMaterial(material.file_path)
window.open(url, '_blank')
}
// 判断文件类型
const isVideoFile = (filename) => {
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
return videoExtensions.some(ext => filename.toLowerCase().endsWith(ext))
}
const isImageFile = (filename) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))
}
// 组件挂载时获取素材列表
onMounted(() => {
// 只有store中没有数据时才获取
if (appStore.materials.length === 0) {
fetchMaterials()
}
})
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.material-management {
.page-header {
margin-bottom: 20px;
h1 {
font-size: 24px;
font-weight: 500;
color: $text-primary;
margin: 0;
}
}
.material-list-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
.material-search {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.el-input {
width: 300px;
}
.action-buttons {
display: flex;
gap: 10px;
.is-loading {
animation: rotate 1s linear infinite;
}
}
}
.material-list {
margin-top: 20px;
}
.empty-data {
padding: 40px 0;
}
}
.material-upload {
width: 100%;
}
.preview-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 0 20px;
.file-info {
text-align: center;
margin-top: 20px;
}
}
}
.upload-form {
padding: 0 20px;
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.upload-demo {
width: 100%;
}
}
.dialog-footer {
padding: 0 20px;
display: flex;
justify-content: flex-end;
}
.upload-file-list {
width: 100%;
}
.upload-file-item {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.upload-file-item .file-name {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
display: block;
}
/* 覆盖Element Plus对话框样式 */
:deep(.el-dialog__body) {
padding: 20px 0;
}
:deep(.el-dialog__header) {
padding-left: 20px;
padding-right: 20px;
margin-right: 0;
}
:deep(.el-dialog__footer) {
padding-top: 10px;
padding-bottom: 15px;
}
/* 修改上传进度条样式 */
:deep(.el-progress__text) {
color: #303133 !important; /* 深灰色字体,确保在各种背景上都可见 */
font-size: 12px;
}
:deep(.el-progress--line) {
margin-bottom: 10px;
}
.upload-file-item {
border: 1px solid #dcdfe6;
border-radius: 6px; /* 增加圆角 */
padding: 12px; /* 增加内边距 */
margin-bottom: 12px; /* 增加外边距 */
background-color: #fafafa; /* 轻微背景色 */
transition: box-shadow 0.3s; /* 添加过渡效果 */
}
.upload-file-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 悬停效果 */
}
.upload-file-item .file-name {
font-size: 14px;
color: #303133; /* 深灰色字体 */
margin-bottom: 8px; /* 增加底部间距 */
display: block;
font-weight: 500;
}
</style>