返回 Social Auto Upload
MaterialManagement.vue
根目录 / sau_frontend / src / views / MaterialManagement.vue
1 <template>
2 <div class="material-management">
3 <div class="page-header">
4 <h1>素材管理</h1>
5 </div>
6
7 <div class="material-list-container">
8 <div class="material-search">
9 <el-input
10 v-model="searchKeyword"
11 placeholder="输入文件名搜索"
12 prefix-icon="Search"
13 clearable
14 @clear="handleSearch"
15 @input="handleSearch"
16 />
17 <div class="action-buttons">
18 <el-button type="primary" @click="handleUploadMaterial">上传素材</el-button>
19 <el-button type="info" @click="fetchMaterials" :loading="false">
20 <el-icon :class="{ 'is-loading': isRefreshing }"><Refresh /></el-icon>
21 <span v-if="isRefreshing">刷新中</span>
22 </el-button>
23 </div>
24 </div>
25
26 <div v-if="filteredMaterials.length > 0" class="material-list">
27 <el-table :data="filteredMaterials" style="width: 100%">
28 <el-table-column prop="uuid" label="UUID" width="180" />
29 <el-table-column prop="filename" label="文件名" width="300" />
30 <el-table-column prop="filesize" label="文件大小" width="120">
31 <template #default="scope">
32 {{ scope.row.filesize }} MB
33 </template>
34 </el-table-column>
35 <el-table-column prop="upload_time" label="上传时间" width="180" />
36 <el-table-column label="操作">
37 <template #default="scope">
38 <el-button size="small" @click="handlePreview(scope.row)">预览</el-button>
39 <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
40 </template>
41 </el-table-column>
42 </el-table>
43 </div>
44
45 <div v-else class="empty-data">
46 <el-empty description="暂无素材数据" />
47 </div>
48 </div>
49
50 <!-- 上传对话框 -->
51 <el-dialog
52 v-model="uploadDialogVisible"
53 title="上传素材"
54 width="40%"
55 @close="handleUploadDialogClose"
56 >
57 <div class="upload-form">
58 <el-form label-width="80px">
59 <el-form-item label="文件名称:">
60 <el-input
61 v-model="customFilename"
62 placeholder="选填 (仅单个文件时生效)"
63 :disabled="customFilenameDisabled"
64 clearable
65 />
66 </el-form-item>
67 <el-form-item label="选择文件">
68 <el-upload
69 class="upload-demo"
70 drag
71 multiple
72 :auto-upload="false"
73 :on-change="handleFileChange"
74 :on-remove="handleFileRemove"
75 :file-list="fileList"
76 >
77 <el-icon class="el-icon--upload"><Upload /></el-icon>
78 <div class="el-upload__text">
79 将文件拖到此处,或<em>点击上传</em>
80 </div>
81 <template #tip>
82 <div class="el-upload__tip">
83 支持视频、图片等格式文件,可一次选择多个文件
84 </div>
85 </template>
86 </el-upload>
87 </el-form-item>
88 <el-form-item label="上传列表" v-if="fileList.length > 0">
89 <div class="upload-file-list">
90 <div v-for="file in fileList" :key="file.uid" class="upload-file-item">
91 <span class="file-name">{{ file.name }}</span>
92 <el-progress
93 :percentage="uploadProgress[file.uid]?.percentage || 0"
94 :text-inside="true"
95 :stroke-width="20"
96 style="width: 100%; margin-top: 5px;"
97 >
98 <span>{{ uploadProgress[file.uid]?.speed || '' }}</span>
99 </el-progress>
100 </div>
101 </div>
102 </el-form-item>
103 </el-form>
104 </div>
105 <template #footer>
106 <div class="dialog-footer">
107 <el-button @click="uploadDialogVisible = false">取消</el-button>
108 <el-button type="primary" @click="submitUpload" :loading="isUploading">
109 {{ isUploading ? '上传中' : '确认上传' }}
110 </el-button>
111 </div>
112 </template>
113 </el-dialog>
114
115 <!-- 预览对话框 -->
116 <el-dialog
117 v-model="previewDialogVisible"
118 title="素材预览"
119 width="50%"
120 :top="'10vh'"
121 >
122 <div class="preview-container" v-if="currentMaterial">
123 <div v-if="isVideoFile(currentMaterial.filename)" class="video-preview">
124 <video controls style="max-width: 100%; max-height: 60vh;">
125 <source :src="getPreviewUrl(currentMaterial.file_path)" type="video/mp4">
126 您的浏览器不支持视频播放
127 </video>
128 </div>
129 <div v-else-if="isImageFile(currentMaterial.filename)" class="image-preview">
130 <img :src="getPreviewUrl(currentMaterial.file_path)" style="max-width: 100%; max-height: 60vh;" />
131 </div>
132 <div v-else class="file-info">
133 <p>文件名: {{ currentMaterial.filename }}</p>
134 <p>文件大小: {{ currentMaterial.filesize }} MB</p>
135 <p>上传时间: {{ currentMaterial.upload_time }}</p>
136 <el-button type="primary" @click="downloadFile(currentMaterial)">下载文件</el-button>
137 </div>
138 </div>
139 </el-dialog>
140 </div>
141 </template>
142
143 <script setup>
144 import { ref, computed, onMounted, watch } from 'vue'
145 import { Refresh, Upload } from '@element-plus/icons-vue'
146 import { ElMessage, ElMessageBox } from 'element-plus'
147 import { materialApi } from '@/api/material'
148 import { useAppStore } from '@/stores/app'
149
150 // 获取应用状态管理
151 const appStore = useAppStore()
152
153 // 搜索和状态控制
154 const searchKeyword = ref('')
155 const isRefreshing = ref(false)
156 const isUploading = ref(false)
157
158 // 对话框控制
159 const uploadDialogVisible = ref(false)
160 const previewDialogVisible = ref(false)
161 const currentMaterial = ref(null)
162
163 // 文件上传
164 const fileList = ref([])
165 const customFilename = ref('')
166 const customFilenameDisabled = computed(() => fileList.value.length > 1)
167 const uploadProgress = ref({}); // { [uid]: { percentage: 0, speed: '' } }
168
169
170 watch(fileList, (newList) => {
171 if (newList.length <= 1) {
172 // If you want to clear the custom name when going back to single file, uncomment below
173 // customFilename.value = ''
174 }
175 });
176
177
178 // 获取素材列表
179 const fetchMaterials = async () => {
180 isRefreshing.value = true
181 try {
182 const response = await materialApi.getAllMaterials()
183
184 if (response.code === 200) {
185 appStore.setMaterials(response.data)
186 ElMessage.success('刷新成功')
187 } else {
188 ElMessage.error('获取素材列表失败')
189 }
190 } catch (error) {
191 console.error('获取素材列表出错:', error)
192 ElMessage.error('获取素材列表失败')
193 } finally {
194 isRefreshing.value = false
195 }
196 }
197
198 // 过滤素材
199 const filteredMaterials = computed(() => {
200 if (!searchKeyword.value) return appStore.materials
201
202 const keyword = searchKeyword.value.toLowerCase()
203 return appStore.materials.filter(material =>
204 material.filename.toLowerCase().includes(keyword)
205 )
206 })
207
208 // 搜索处理
209 const handleSearch = () => {
210 // 搜索逻辑已通过计算属性实现
211 }
212
213 // 上传素材
214 const handleUploadMaterial = () => {
215 // 清空变量
216 fileList.value = []
217 customFilename.value = ''
218 uploadProgress.value = {};
219 uploadDialogVisible.value = true
220 }
221
222 // 关闭上传对话框时清空变量
223 const handleUploadDialogClose = () => {
224 fileList.value = []
225 customFilename.value = ''
226 uploadProgress.value = {};
227 }
228
229 // 文件选择变更
230 const handleFileChange = (file, uploadFileList) => {
231 fileList.value = uploadFileList;
232 const newProgress = {};
233 for (const f of uploadFileList) {
234 newProgress[f.uid] = { percentage: 0, speed: '' };
235 }
236 uploadProgress.value = newProgress;
237 }
238
239 const handleFileRemove = (file, uploadFileList) => {
240 fileList.value = uploadFileList;
241 const newProgress = { ...uploadProgress.value };
242 delete newProgress[file.uid];
243 uploadProgress.value = newProgress;
244 }
245
246 // 提交上传
247 const submitUpload = async () => {
248 if (fileList.value.length === 0) {
249 ElMessage.warning('请选择要上传的文件')
250 return
251 }
252
253 isUploading.value = true
254
255 for (const file of fileList.value) {
256 try {
257 // 确保文件对象存在
258 if (!file || !file.raw) {
259 ElMessage.warning(`文件 ${file.name} 对象无效,已跳过`)
260 continue
261 }
262
263 const formData = new FormData()
264 formData.append('file', file.raw)
265
266 // 只有当只有一个文件时,自定义文件名才生效
267 if (fileList.value.length === 1 && customFilename.value.trim()) {
268 formData.append('filename', customFilename.value.trim())
269 }
270
271 let lastLoaded = 0;
272 let lastTime = Date.now();
273
274 const response = await materialApi.uploadMaterial(formData, (progressEvent) => {
275 const progressData = uploadProgress.value[file.uid];
276 if (!progressData) return;
277
278 const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
279 progressData.percentage = progress;
280
281 const currentTime = Date.now();
282 const timeDiff = (currentTime - lastTime) / 1000; // in seconds
283 const loadedDiff = progressEvent.loaded - lastLoaded;
284
285 if (timeDiff > 0.5) { // Update speed every 0.5 seconds
286 const speed = loadedDiff / timeDiff; // bytes per second
287 if (speed > 1024 * 1024) {
288 progressData.speed = (speed / (1024 * 1024)).toFixed(2) + ' MB/s';
289 } else {
290 progressData.speed = (speed / 1024).toFixed(2) + ' KB/s';
291 }
292 lastLoaded = progressEvent.loaded;
293 lastTime = currentTime;
294 }
295 })
296
297 if (response.code === 200) {
298 ElMessage.success(`文件 ${file.name} 上传成功`)
299 const progressData = uploadProgress.value[file.uid];
300 if(progressData) progressData.speed = '完成';
301 } else {
302 ElMessage.error(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`)
303 }
304 } catch (error) {
305 console.error(`上传文件 ${file.name} 出错:`, error)
306 ElMessage.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`)
307 }
308 }
309
310 isUploading.value = false
311 // Keep dialog open to show results
312 // uploadDialogVisible.value = false
313 await fetchMaterials()
314 }
315
316 // 预览素材
317 const handlePreview = async (material) => {
318 currentMaterial.value = null
319 previewDialogVisible.value = true
320 ElMessage.info('加载中...')
321 try {
322 // 等待一小段时间以确保对话框已打开
323 await new Promise(resolve => setTimeout(resolve, 100))
324 currentMaterial.value = material
325 } catch (error) {
326 console.error('预览素材出错:', error)
327 ElMessage.error('预览加载失败')
328 previewDialogVisible.value = false
329 }
330 }
331
332 // 删除素材
333 const handleDelete = (material) => {
334 ElMessageBox.confirm(
335 `确定要删除素材 ${material.filename} 吗?`,
336 '警告',
337 {
338 confirmButtonText: '确定',
339 cancelButtonText: '取消',
340 type: 'warning',
341 }
342 )
343 .then(async () => {
344 try {
345 const response = await materialApi.deleteMaterial(material.id)
346
347 if (response.code === 200) {
348 appStore.removeMaterial(material.id)
349 ElMessage.success('删除成功')
350 } else {
351 ElMessage.error(response.msg || '删除失败')
352 }
353 } catch (error) {
354 console.error('删除素材出错:', error)
355 ElMessage.error('删除失败')
356 }
357 })
358 .catch(() => {
359 // 取消删除
360 })
361 }
362
363 // 获取预览URL
364 const getPreviewUrl = (filePath) => {
365 const filename = filePath.split('/').pop()
366 return materialApi.getMaterialPreviewUrl(filename)
367 }
368
369 // 下载文件
370 const downloadFile = (material) => {
371 const url = materialApi.downloadMaterial(material.file_path)
372 window.open(url, '_blank')
373 }
374
375 // 判断文件类型
376 const isVideoFile = (filename) => {
377 const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
378 return videoExtensions.some(ext => filename.toLowerCase().endsWith(ext))
379 }
380
381 const isImageFile = (filename) => {
382 const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
383 return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))
384 }
385
386 // 组件挂载时获取素材列表
387 onMounted(() => {
388 // 只有store中没有数据时才获取
389 if (appStore.materials.length === 0) {
390 fetchMaterials()
391 }
392 })
393 </script>
394
395 <style lang="scss" scoped>
396 @use '@/styles/variables.scss' as *;
397
398 @keyframes rotate {
399 from {
400 transform: rotate(0deg);
401 }
402 to {
403 transform: rotate(360deg);
404 }
405 }
406
407 .material-management {
408
409 .page-header {
410 margin-bottom: 20px;
411
412 h1 {
413 font-size: 24px;
414 font-weight: 500;
415 color: $text-primary;
416 margin: 0;
417 }
418 }
419
420 .material-list-container {
421 background-color: #fff;
422 border-radius: 4px;
423 box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
424 padding: 20px;
425
426 .material-search {
427 display: flex;
428 justify-content: space-between;
429 align-items: center;
430 margin-bottom: 20px;
431
432 .el-input {
433 width: 300px;
434 }
435
436 .action-buttons {
437 display: flex;
438 gap: 10px;
439
440 .is-loading {
441 animation: rotate 1s linear infinite;
442 }
443 }
444 }
445
446 .material-list {
447 margin-top: 20px;
448 }
449
450 .empty-data {
451 padding: 40px 0;
452 }
453 }
454
455 .material-upload {
456 width: 100%;
457 }
458
459 .preview-container {
460 display: flex;
461 justify-content: center;
462 align-items: center;
463 flex-direction: column;
464 padding: 0 20px;
465
466 .file-info {
467 text-align: center;
468 margin-top: 20px;
469 }
470 }
471 }
472
473 .upload-form {
474 padding: 0 20px;
475
476 .form-tip {
477 font-size: 12px;
478 color: #909399;
479 margin-top: 5px;
480 }
481
482 .upload-demo {
483 width: 100%;
484 }
485 }
486
487 .dialog-footer {
488 padding: 0 20px;
489 display: flex;
490 justify-content: flex-end;
491 }
492
493 .upload-file-list {
494 width: 100%;
495 }
496
497 .upload-file-item {
498 border: 1px solid #dcdfe6;
499 border-radius: 4px;
500 padding: 10px;
501 margin-bottom: 10px;
502 }
503
504 .upload-file-item .file-name {
505 font-size: 14px;
506 color: #606266;
507 margin-bottom: 5px;
508 display: block;
509 }
510
511 /* 覆盖Element Plus对话框样式 */
512 :deep(.el-dialog__body) {
513 padding: 20px 0;
514 }
515
516 :deep(.el-dialog__header) {
517 padding-left: 20px;
518 padding-right: 20px;
519 margin-right: 0;
520 }
521
522 :deep(.el-dialog__footer) {
523 padding-top: 10px;
524 padding-bottom: 15px;
525 }
526
527 /* 修改上传进度条样式 */
528 :deep(.el-progress__text) {
529 color: #303133 !important; /* 深灰色字体,确保在各种背景上都可见 */
530 font-size: 12px;
531 }
532
533 :deep(.el-progress--line) {
534 margin-bottom: 10px;
535 }
536
537 .upload-file-item {
538 border: 1px solid #dcdfe6;
539 border-radius: 6px; /* 增加圆角 */
540 padding: 12px; /* 增加内边距 */
541 margin-bottom: 12px; /* 增加外边距 */
542 background-color: #fafafa; /* 轻微背景色 */
543 transition: box-shadow 0.3s; /* 添加过渡效果 */
544 }
545
546 .upload-file-item:hover {
547 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 悬停效果 */
548 }
549
550 .upload-file-item .file-name {
551 font-size: 14px;
552 color: #303133; /* 深灰色字体 */
553 margin-bottom: 8px; /* 增加底部间距 */
554 display: block;
555 font-weight: 500;
556 }
557 </style>
558
558 lines Plain Text