返回 Social Auto Upload
PublishCenter.vue
根目录 / sau_frontend / src / views / PublishCenter.vue
1 <template>
2 <div class="publish-center">
3 <!-- Tab管理区域 -->
4 <div class="tab-management">
5 <div class="tab-header">
6 <div class="tab-list">
7 <div
8 v-for="tab in tabs"
9 :key="tab.name"
10 :class="['tab-item', { active: activeTab === tab.name }]"
11 @click="activeTab = tab.name"
12 >
13 <span>{{ tab.label }}</span>
14 <el-icon
15 v-if="tabs.length > 1"
16 class="close-icon"
17 @click.stop="removeTab(tab.name)"
18 >
19 <Close />
20 </el-icon>
21 </div>
22 </div>
23 <div class="tab-actions">
24 <el-button
25 type="primary"
26 size="small"
27 @click="addTab"
28 class="add-tab-btn"
29 >
30 <el-icon><Plus /></el-icon>
31 添加Tab
32 </el-button>
33 <el-button
34 type="success"
35 size="small"
36 @click="batchPublish"
37 :loading="batchPublishing"
38 class="batch-publish-btn"
39 >
40 批量发布
41 </el-button>
42 </div>
43 </div>
44 </div>
45
46 <!-- 内容区域 -->
47 <div class="publish-content">
48 <div class="tab-content-wrapper">
49 <div
50 v-for="tab in tabs"
51 :key="tab.name"
52 v-show="activeTab === tab.name"
53 class="tab-content"
54 >
55 <!-- 发布状态提示 -->
56 <div v-if="tab.publishStatus" class="publish-status">
57 <el-alert
58 :title="tab.publishStatus.message"
59 :type="tab.publishStatus.type"
60 :closable="false"
61 show-icon
62 />
63 </div>
64
65 <!-- 视频上传区域 -->
66 <div class="upload-section">
67 <h3>视频</h3>
68 <div class="upload-options">
69 <el-button type="primary" @click="showUploadOptions(tab)" class="upload-btn">
70 <el-icon><Upload /></el-icon>
71 上传视频
72 </el-button>
73 </div>
74
75 <!-- 已上传文件列表 -->
76 <div v-if="tab.fileList.length > 0" class="uploaded-files">
77 <h4>已上传文件:</h4>
78 <div class="file-list">
79 <div v-for="(file, index) in tab.fileList" :key="index" class="file-item">
80 <el-link :href="file.url" target="_blank" type="primary">{{ file.name }}</el-link>
81 <span class="file-size">{{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
82 <el-button type="danger" size="small" @click="removeFile(tab, index)">删除</el-button>
83 </div>
84 </div>
85 </div>
86 </div>
87
88 <!-- 上传选项弹窗 -->
89 <el-dialog
90 v-model="uploadOptionsVisible"
91 title="选择上传方式"
92 width="400px"
93 class="upload-options-dialog"
94 >
95 <div class="upload-options-content">
96 <el-button type="primary" @click="selectLocalUpload" class="option-btn">
97 <el-icon><Upload /></el-icon>
98 本地上传
99 </el-button>
100 <el-button type="success" @click="selectMaterialLibrary" class="option-btn">
101 <el-icon><Folder /></el-icon>
102 素材库
103 </el-button>
104 </div>
105 </el-dialog>
106
107 <!-- 本地上传弹窗 -->
108 <el-dialog
109 v-model="localUploadVisible"
110 title="本地上传"
111 width="600px"
112 class="local-upload-dialog"
113 >
114 <el-upload
115 class="video-upload"
116 drag
117 :auto-upload="true"
118 :action="`${apiBaseUrl}/upload`"
119 :on-success="(response, file) => handleUploadSuccess(response, file, currentUploadTab)"
120 :on-error="handleUploadError"
121 multiple
122 accept="video/*"
123 :headers="authHeaders"
124 >
125 <el-icon class="el-icon--upload"><Upload /></el-icon>
126 <div class="el-upload__text">
127 将视频文件拖到此处,或<em>点击上传</em>
128 </div>
129 <template #tip>
130 <div class="el-upload__tip">
131 支持MP4、AVI等视频格式,可上传多个文件
132 </div>
133 </template>
134 </el-upload>
135 </el-dialog>
136
137 <!-- 批量发布进度对话框 -->
138 <el-dialog
139 v-model="batchPublishDialogVisible"
140 title="批量发布进度"
141 width="500px"
142 :close-on-click-modal="false"
143 :close-on-press-escape="false"
144 :show-close="false"
145 >
146 <div class="publish-progress">
147 <el-progress
148 :percentage="publishProgress"
149 :status="publishProgress === 100 ? 'success' : ''"
150 />
151 <div v-if="currentPublishingTab" class="current-publishing">
152 正在发布:{{ currentPublishingTab.label }}
153 </div>
154
155 <!-- 发布结果列表 -->
156 <div class="publish-results" v-if="publishResults.length > 0">
157 <div
158 v-for="(result, index) in publishResults"
159 :key="index"
160 :class="['result-item', result.status]"
161 >
162 <el-icon v-if="result.status === 'success'"><Check /></el-icon>
163 <el-icon v-else-if="result.status === 'error'"><Close /></el-icon>
164 <el-icon v-else><InfoFilled /></el-icon>
165 <span class="label">{{ result.label }}</span>
166 <span class="message">{{ result.message }}</span>
167 </div>
168 </div>
169 </div>
170
171 <template #footer>
172 <div class="dialog-footer">
173 <el-button
174 @click="cancelBatchPublish"
175 :disabled="publishProgress === 100"
176 >
177 取消发布
178 </el-button>
179 <el-button
180 type="primary"
181 @click="batchPublishDialogVisible = false"
182 v-if="publishProgress === 100"
183 >
184 关闭
185 </el-button>
186 </div>
187 </template>
188 </el-dialog>
189
190 <!-- 素材库选择弹窗 -->
191 <el-dialog
192 v-model="materialLibraryVisible"
193 title="选择素材"
194 width="800px"
195 class="material-library-dialog"
196 >
197 <div class="material-library-content">
198 <el-checkbox-group v-model="selectedMaterials">
199 <div class="material-list">
200 <div
201 v-for="material in materials"
202 :key="material.id"
203 class="material-item"
204 >
205 <el-checkbox :label="material.id" class="material-checkbox">
206 <div class="material-info">
207 <div class="material-name">{{ material.filename }}</div>
208 <div class="material-details">
209 <span class="file-size">{{ material.filesize }}MB</span>
210 <span class="upload-time">{{ material.upload_time }}</span>
211 </div>
212 </div>
213 </el-checkbox>
214 </div>
215 </div>
216 </el-checkbox-group>
217 </div>
218 <template #footer>
219 <div class="dialog-footer">
220 <el-button @click="materialLibraryVisible = false">取消</el-button>
221 <el-button type="primary" @click="confirmMaterialSelection">确定</el-button>
222 </div>
223 </template>
224 </el-dialog>
225
226 <!-- 账号选择 -->
227 <div class="account-section">
228 <h3>账号</h3>
229 <div class="account-display">
230 <div class="selected-accounts">
231 <el-tag
232 v-for="(account, index) in tab.selectedAccounts"
233 :key="index"
234 closable
235 @close="removeAccount(tab, index)"
236 class="account-tag"
237 >
238 {{ getAccountDisplayName(account) }}
239 </el-tag>
240 </div>
241 <el-button
242 type="primary"
243 plain
244 @click="openAccountDialog(tab)"
245 class="select-account-btn"
246 >
247 选择账号
248 </el-button>
249 </div>
250 </div>
251
252 <!-- 账号选择弹窗 -->
253 <el-dialog
254 v-model="accountDialogVisible"
255 title="选择账号"
256 width="600px"
257 class="account-dialog"
258 >
259 <div class="account-dialog-content">
260 <el-checkbox-group v-model="tempSelectedAccounts">
261 <div class="account-list">
262 <el-checkbox
263 v-for="account in availableAccounts"
264 :key="account.id"
265 :label="account.id"
266 class="account-item"
267 >
268 <div class="account-info">
269 <span class="account-name">{{ account.name }}</span>
270 </div>
271 </el-checkbox>
272 </div>
273 </el-checkbox-group>
274 </div>
275
276 <template #footer>
277 <div class="dialog-footer">
278 <el-button @click="accountDialogVisible = false">取消</el-button>
279 <el-button type="primary" @click="confirmAccountSelection">确定</el-button>
280 </div>
281 </template>
282 </el-dialog>
283
284 <!-- 平台选择 -->
285 <div class="platform-section">
286 <h3>平台</h3>
287 <el-radio-group v-model="tab.selectedPlatform" class="platform-radios">
288 <el-radio
289 v-for="platform in platforms"
290 :key="platform.key"
291 :label="platform.key"
292 class="platform-radio"
293 >
294 {{ platform.name }}
295 </el-radio>
296 </el-radio-group>
297 </div>
298
299 <!-- 原创声明 -->
300 <div class="original-section">
301 <el-checkbox
302 v-model="tab.isOriginal"
303 label="声明原创"
304 class="original-checkbox"
305 />
306 </div>
307
308 <!-- 草稿选项 (仅在视频号可见) -->
309 <div v-if="tab.selectedPlatform === 2" class="draft-section">
310 <el-checkbox
311 v-model="tab.isDraft"
312 label="视频号仅保存草稿(用手机发布)"
313 class="draft-checkbox"
314 />
315 </div>
316
317 <!-- 标签 (仅在抖音可见) -->
318 <div v-if="tab.selectedPlatform === 3" class="product-section">
319 <h3>商品链接</h3>
320 <el-input
321 v-model="tab.productTitle"
322 type="text"
323 :rows="1"
324 placeholder="请输入商品名称"
325 maxlength="200"
326 class="product-name-input"
327 />
328 <el-input
329 v-model="tab.productLink"
330 type="text"
331 :rows="1"
332 placeholder="请输入商品链接"
333 maxlength="200"
334 class="product-link-input"
335 />
336 </div>
337
338 <!-- 标题输入 -->
339 <div class="title-section">
340 <h3>标题</h3>
341 <el-input
342 v-model="tab.title"
343 type="textarea"
344 :rows="3"
345 placeholder="请输入标题"
346 maxlength="100"
347 show-word-limit
348 class="title-input"
349 />
350 </div>
351
352 <!-- 话题输入 -->
353 <div class="topic-section">
354 <h3>话题</h3>
355 <div class="topic-display">
356 <div class="selected-topics">
357 <el-tag
358 v-for="(topic, index) in tab.selectedTopics"
359 :key="index"
360 closable
361 @close="removeTopic(tab, index)"
362 class="topic-tag"
363 >
364 #{{ topic }}
365 </el-tag>
366 </div>
367 <el-button
368 type="primary"
369 plain
370 @click="openTopicDialog(tab)"
371 class="select-topic-btn"
372 >
373 添加话题
374 </el-button>
375 </div>
376 </div>
377
378 <!-- 添加话题弹窗 -->
379 <el-dialog
380 v-model="topicDialogVisible"
381 title="添加话题"
382 width="600px"
383 class="topic-dialog"
384 >
385 <div class="topic-dialog-content">
386 <!-- 自定义话题输入 -->
387 <div class="custom-topic-input">
388 <el-input
389 v-model="customTopic"
390 placeholder="输入自定义话题"
391 class="custom-input"
392 >
393 <template #prepend>#</template>
394 </el-input>
395 <el-button type="primary" @click="addCustomTopic">添加</el-button>
396 </div>
397
398 <!-- 推荐话题 -->
399 <div class="recommended-topics">
400 <h4>推荐话题</h4>
401 <div class="topic-grid">
402 <el-button
403 v-for="topic in recommendedTopics"
404 :key="topic"
405 :type="currentTab?.selectedTopics?.includes(topic) ? 'primary' : 'default'"
406 @click="toggleRecommendedTopic(topic)"
407 class="topic-btn"
408 >
409 {{ topic }}
410 </el-button>
411 </div>
412 </div>
413 </div>
414
415 <template #footer>
416 <div class="dialog-footer">
417 <el-button @click="topicDialogVisible = false">取消</el-button>
418 <el-button type="primary" @click="confirmTopicSelection">确定</el-button>
419 </div>
420 </template>
421 </el-dialog>
422
423 <!-- 定时发布 -->
424 <div class="schedule-section">
425 <h3>定时发布</h3>
426 <div class="schedule-controls">
427 <el-switch
428 v-model="tab.scheduleEnabled"
429 active-text="定时发布"
430 inactive-text="立即发布"
431 />
432 <div v-if="tab.scheduleEnabled" class="schedule-settings">
433 <div class="schedule-item">
434 <span class="label">每天发布视频数:</span>
435 <el-select v-model="tab.videosPerDay" placeholder="选择发布数量">
436 <el-option
437 v-for="num in 55"
438 :key="num"
439 :label="num"
440 :value="num"
441 />
442 </el-select>
443 </div>
444 <div class="schedule-item">
445 <span class="label">每天发布时间:</span>
446 <el-time-select
447 v-for="(time, index) in tab.dailyTimes"
448 :key="index"
449 v-model="tab.dailyTimes[index]"
450 start="00:00"
451 step="00:30"
452 end="23:30"
453 placeholder="选择时间"
454 />
455 <el-button
456 v-if="tab.dailyTimes.length < tab.videosPerDay"
457 type="primary"
458 size="small"
459 @click="tab.dailyTimes.push('10:00')"
460 >
461 添加时间
462 </el-button>
463 </div>
464 <div class="schedule-item">
465 <span class="label">开始天数:</span>
466 <el-select v-model="tab.startDays" placeholder="选择开始天数">
467 <el-option :label="'明天'" :value="0" />
468 <el-option :label="'后天'" :value="1" />
469 </el-select>
470 </div>
471 </div>
472 </div>
473 </div>
474
475 <!-- 操作按钮 -->
476 <div class="action-buttons">
477 <el-button size="small" @click="cancelPublish(tab)">取消</el-button>
478 <el-button
479 size="small"
480 type="primary"
481 @click="confirmPublish(tab)"
482 :loading="tab.publishing || false"
483 >
484 {{ tab.publishing ? '发布中...' : '发布' }}
485 </el-button>
486 </div>
487 </div>
488 </div>
489 </div>
490 </div>
491 </template>
492
493 <script setup>
494 import { ref, reactive, computed } from 'vue'
495 import { Upload, Plus, Close, Folder } from '@element-plus/icons-vue'
496 import { ElMessage } from 'element-plus'
497 import { useAccountStore } from '@/stores/account'
498 import { useAppStore } from '@/stores/app'
499 import { materialApi } from '@/api/material'
500 import { http } from '@/utils/request'
501
502 // API base URL
503 const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
504
505 // Authorization headers
506 const authHeaders = computed(() => ({
507 'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
508 }))
509
510 // 当前激活的tab
511 const activeTab = ref('tab1')
512
513 // tab计数器
514 let tabCounter = 1
515
516 // 获取应用状态管理
517 const appStore = useAppStore()
518
519 // 上传相关状态
520 const uploadOptionsVisible = ref(false)
521 const localUploadVisible = ref(false)
522 const materialLibraryVisible = ref(false)
523 const currentUploadTab = ref(null)
524 const selectedMaterials = ref([])
525 const materials = computed(() => appStore.materials)
526
527 // 批量发布相关状态
528 const batchPublishing = ref(false)
529 const batchPublishMessage = ref('')
530 const batchPublishType = ref('info')
531
532 // 平台列表 - 对应后端type字段
533 const platforms = [
534 { key: 3, name: '抖音' },
535 { key: 4, name: '快手' },
536 { key: 2, name: '视频号' },
537 { key: 1, name: '小红书' }
538 ]
539
540 const defaultTabInit = {
541 name: 'tab1',
542 label: '发布1',
543 fileList: [], // 后端返回的文件名列表
544 displayFileList: [], // 用于显示的文件列表
545 selectedAccounts: [], // 选中的账号ID列表
546 selectedPlatform: 1, // 选中的平台(单选)
547 title: '',
548 productLink: '', // 商品链接
549 productTitle: '', // 商品名称
550 selectedTopics: [], // 话题列表(不带#号)
551 scheduleEnabled: false, // 定时发布开关
552 videosPerDay: 1, // 每天发布视频数量
553 dailyTimes: ['10:00'], // 每天发布时间点列表
554 startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
555 publishStatus: null, // 发布状态,包含message和type
556 publishing: false, // 发布状态,用于控制按钮loading效果
557 isDraft: false, // 是否保存为草稿,仅视频号平台可见
558 isOriginal: false // 是否标记为原创
559 }
560
561 // helper to create a fresh deep-copied tab from defaultTabInit
562 const makeNewTab = () => {
563 // prefer structuredClone when available (newer browsers/node), fallback to JSON
564 try {
565 return typeof structuredClone === 'function' ? structuredClone(defaultTabInit) : JSON.parse(JSON.stringify(defaultTabInit))
566 } catch (e) {
567 return JSON.parse(JSON.stringify(defaultTabInit))
568 }
569 }
570
571 // tab页数据 - 默认只有一个tab (use deep copy to avoid shared refs)
572 const tabs = reactive([
573 makeNewTab()
574 ])
575
576 // 账号相关状态
577 const accountDialogVisible = ref(false)
578 const tempSelectedAccounts = ref([])
579 const currentTab = ref(null)
580
581 // 获取账号状态管理
582 const accountStore = useAccountStore()
583
584 // 根据选择的平台获取可用账号列表
585 const availableAccounts = computed(() => {
586 const platformMap = {
587 3: '抖音',
588 2: '视频号',
589 1: '小红书',
590 4: '快手'
591 }
592 const currentPlatform = currentTab.value ? platformMap[currentTab.value.selectedPlatform] : null
593 return currentPlatform ? accountStore.accounts.filter(acc => acc.platform === currentPlatform) : []
594 })
595
596 // 话题相关状态
597 const topicDialogVisible = ref(false)
598 const customTopic = ref('')
599
600 // 推荐话题列表
601 const recommendedTopics = [
602 '游戏', '电影', '音乐', '美食', '旅行', '文化',
603 '科技', '生活', '娱乐', '体育', '教育', '艺术',
604 '健康', '时尚', '美妆', '摄影', '宠物', '汽车'
605 ]
606
607 // 添加新tab
608 const addTab = () => {
609 tabCounter++
610 const newTab = makeNewTab()
611 newTab.name = `tab${tabCounter}`
612 newTab.label = `发布${tabCounter}`
613 tabs.push(newTab)
614 activeTab.value = newTab.name
615 }
616
617 // 删除tab
618 const removeTab = (tabName) => {
619 const index = tabs.findIndex(tab => tab.name === tabName)
620 if (index > -1) {
621 tabs.splice(index, 1)
622 // 如果删除的是当前激活的tab,切换到第一个tab
623 if (activeTab.value === tabName && tabs.length > 0) {
624 activeTab.value = tabs[0].name
625 }
626 }
627 }
628
629 // 处理文件上传成功
630 const handleUploadSuccess = (response, file, tab) => {
631 if (response.code === 200) {
632 // 获取文件路径
633 const filePath = response.data.path || response.data
634 // 从路径中提取文件名
635 const filename = filePath.split('/').pop()
636
637 // 保存文件信息到fileList,包含文件路径和其他信息
638 const fileInfo = {
639 name: file.name,
640 url: materialApi.getMaterialPreviewUrl(filename), // 使用getMaterialPreviewUrl生成预览URL
641 path: filePath,
642 size: file.size,
643 type: file.type
644 }
645
646 // 添加到文件列表
647 tab.fileList.push(fileInfo)
648
649 // 更新显示列表
650 tab.displayFileList = [...tab.fileList.map(item => ({
651 name: item.name,
652 url: item.url
653 }))]
654
655 ElMessage.success('文件上传成功')
656 } else {
657 ElMessage.error(response.msg || '上传失败')
658 }
659 }
660
661 // 处理文件上传失败
662 const handleUploadError = (error) => {
663 ElMessage.error('文件上传失败')
664 }
665
666 // 删除已上传文件
667 const removeFile = (tab, index) => {
668 // 从文件列表中删除
669 tab.fileList.splice(index, 1)
670
671 // 更新显示列表
672 tab.displayFileList = [...tab.fileList.map(item => ({
673 name: item.name,
674 url: item.url
675 }))]
676
677 ElMessage.success('文件删除成功')
678 }
679
680 // 话题相关方法
681 // 打开添加话题弹窗
682 const openTopicDialog = (tab) => {
683 currentTab.value = tab
684 topicDialogVisible.value = true
685 }
686
687 // 添加自定义话题
688 const addCustomTopic = () => {
689 if (!customTopic.value.trim()) {
690 ElMessage.warning('请输入话题内容')
691 return
692 }
693 if (currentTab.value && !currentTab.value.selectedTopics.includes(customTopic.value.trim())) {
694 currentTab.value.selectedTopics.push(customTopic.value.trim())
695 customTopic.value = ''
696 ElMessage.success('话题添加成功')
697 } else {
698 ElMessage.warning('话题已存在')
699 }
700 }
701
702 // 切换推荐话题
703 const toggleRecommendedTopic = (topic) => {
704 if (!currentTab.value) return
705
706 const index = currentTab.value.selectedTopics.indexOf(topic)
707 if (index > -1) {
708 currentTab.value.selectedTopics.splice(index, 1)
709 } else {
710 currentTab.value.selectedTopics.push(topic)
711 }
712 }
713
714 // 删除话题
715 const removeTopic = (tab, index) => {
716 tab.selectedTopics.splice(index, 1)
717 }
718
719 // 确认添加话题
720 const confirmTopicSelection = () => {
721 topicDialogVisible.value = false
722 customTopic.value = ''
723 currentTab.value = null
724 ElMessage.success('添加话题完成')
725 }
726
727 // 账号选择相关方法
728 // 打开账号选择弹窗
729 const openAccountDialog = (tab) => {
730 currentTab.value = tab
731 tempSelectedAccounts.value = [...tab.selectedAccounts]
732 accountDialogVisible.value = true
733 }
734
735 // 确认账号选择
736 const confirmAccountSelection = () => {
737 if (currentTab.value) {
738 currentTab.value.selectedAccounts = [...tempSelectedAccounts.value]
739 }
740 accountDialogVisible.value = false
741 currentTab.value = null
742 ElMessage.success('账号选择完成')
743 }
744
745 // 删除选中的账号
746 const removeAccount = (tab, index) => {
747 tab.selectedAccounts.splice(index, 1)
748 }
749
750 // 获取账号显示名称
751 const getAccountDisplayName = (accountId) => {
752 const account = accountStore.accounts.find(acc => acc.id === accountId)
753 return account ? account.name : accountId
754 }
755
756 // 取消发布
757 const cancelPublish = (tab) => {
758 ElMessage.info('已取消发布')
759 }
760
761 // 确认发布
762 const confirmPublish = async (tab) => {
763 // 防止重复点击
764 if (tab.publishing) {
765 throw new Error('正在发布中,请稍候...')
766 }
767
768 tab.publishing = true // 设置发布状态为进行中
769
770 // 数据验证
771 if (tab.fileList.length === 0) {
772 ElMessage.error('请先上传视频文件')
773 tab.publishing = false
774 throw new Error('请先上传视频文件')
775 }
776 if (!tab.title.trim()) {
777 ElMessage.error('请输入标题')
778 tab.publishing = false
779 throw new Error('请输入标题')
780 }
781 if (!tab.selectedPlatform) {
782 ElMessage.error('请选择发布平台')
783 tab.publishing = false
784 throw new Error('请选择发布平台')
785 }
786 if (tab.selectedAccounts.length === 0) {
787 ElMessage.error('请选择发布账号')
788 tab.publishing = false
789 throw new Error('请选择发布账号')
790 }
791
792 // 构造发布数据,符合后端API格式
793 const publishData = {
794 type: tab.selectedPlatform,
795 title: tab.title,
796 tags: tab.selectedTopics, // 不带#号的话题列表
797 fileList: tab.fileList.map(file => file.path), // 只发送文件路径
798 accountList: tab.selectedAccounts.map(accountId => {
799 const account = accountStore.accounts.find(acc => acc.id === accountId)
800 return account ? account.filePath : accountId
801 }), // 发送账号的文件路径
802 enableTimer: tab.scheduleEnabled ? 1 : 0,
803 videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1,
804 dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'],
805 startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0,
806 category: tab.isOriginal ? 1 : 0, // 1表示原创,0表示非原创
807 productLink: tab.productLink.trim() || '',
808 productTitle: tab.productTitle.trim() || '',
809 isDraft: tab.isDraft
810 }
811
812 // 调用后端发布API(使用统一的http封装)
813 try {
814 const data = await http.post('/postVideo', publishData)
815 tab.publishStatus = {
816 message: '发布成功',
817 type: 'success'
818 }
819 // 清空当前tab的数据
820 tab.fileList = []
821 tab.displayFileList = []
822 tab.title = ''
823 tab.selectedTopics = []
824 tab.selectedAccounts = []
825 tab.scheduleEnabled = false
826 } catch (error) {
827 console.error('发布错误:', error)
828 tab.publishStatus = {
829 message: `发布失败:${error.message || '请检查网络连接'}`,
830 type: 'error'
831 }
832 throw error
833 } finally {
834 tab.publishing = false
835 }
836 }
837
838 // 显示上传选项
839 const showUploadOptions = (tab) => {
840 currentUploadTab.value = tab
841 uploadOptionsVisible.value = true
842 }
843
844 // 选择本地上传
845 const selectLocalUpload = () => {
846 uploadOptionsVisible.value = false
847 localUploadVisible.value = true
848 }
849
850 // 选择素材库
851 const selectMaterialLibrary = async () => {
852 uploadOptionsVisible.value = false
853
854 // 如果素材库为空,先获取素材数据
855 if (materials.value.length === 0) {
856 try {
857 const response = await materialApi.getAllMaterials()
858 if (response.code === 200) {
859 appStore.setMaterials(response.data)
860 } else {
861 ElMessage.error('获取素材列表失败')
862 return
863 }
864 } catch (error) {
865 console.error('获取素材列表出错:', error)
866 ElMessage.error('获取素材列表失败')
867 return
868 }
869 }
870
871 selectedMaterials.value = []
872 materialLibraryVisible.value = true
873 }
874
875 // 确认素材选择
876 const confirmMaterialSelection = () => {
877 if (selectedMaterials.value.length === 0) {
878 ElMessage.warning('请选择至少一个素材')
879 return
880 }
881
882 if (currentUploadTab.value) {
883 // 将选中的素材添加到当前tab的文件列表
884 selectedMaterials.value.forEach(materialId => {
885 const material = materials.value.find(m => m.id === materialId)
886 if (material) {
887 const fileInfo = {
888 name: material.filename,
889 url: materialApi.getMaterialPreviewUrl(material.file_path.split('/').pop()),
890 path: material.file_path,
891 size: material.filesize * 1024 * 1024, // 转换为字节
892 type: 'video/mp4'
893 }
894
895 // 检查是否已存在相同文件
896 const exists = currentUploadTab.value.fileList.some(file => file.path === fileInfo.path)
897 if (!exists) {
898 currentUploadTab.value.fileList.push(fileInfo)
899 }
900 }
901 })
902
903 // 更新显示列表
904 currentUploadTab.value.displayFileList = [...currentUploadTab.value.fileList.map(item => ({
905 name: item.name,
906 url: item.url
907 }))]
908 }
909
910 const addedCount = selectedMaterials.value.length
911 materialLibraryVisible.value = false
912 selectedMaterials.value = []
913 currentUploadTab.value = null
914 ElMessage.success(`已添加 ${addedCount} 个素材`)
915 }
916
917 // 批量发布对话框状态
918 const batchPublishDialogVisible = ref(false)
919 const currentPublishingTab = ref(null)
920 const publishProgress = ref(0)
921 const publishResults = ref([])
922 const isCancelled = ref(false)
923
924 // 取消批量发布
925 const cancelBatchPublish = () => {
926 isCancelled.value = true
927 ElMessage.info('正在取消发布...')
928 }
929
930 // 批量发布方法
931 const batchPublish = async () => {
932 if (batchPublishing.value) return
933
934 batchPublishing.value = true
935 currentPublishingTab.value = null
936 publishProgress.value = 0
937 publishResults.value = []
938 isCancelled.value = false
939 batchPublishDialogVisible.value = true
940
941 try {
942 for (let i = 0; i < tabs.length; i++) {
943 if (isCancelled.value) {
944 publishResults.value.push({
945 label: tabs[i].label,
946 status: 'cancelled',
947 message: '已取消'
948 })
949 continue
950 }
951
952 const tab = tabs[i]
953 currentPublishingTab.value = tab
954 publishProgress.value = Math.floor((i / tabs.length) * 100)
955
956 try {
957 await confirmPublish(tab)
958 publishResults.value.push({
959 label: tab.label,
960 status: 'success',
961 message: '发布成功'
962 })
963 } catch (error) {
964 publishResults.value.push({
965 label: tab.label,
966 status: 'error',
967 message: error.message
968 })
969 // 不立即返回,继续显示发布结果
970 }
971 }
972
973 publishProgress.value = 100
974
975 // 统计发布结果
976 const successCount = publishResults.value.filter(r => r.status === 'success').length
977 const failCount = publishResults.value.filter(r => r.status === 'error').length
978 const cancelCount = publishResults.value.filter(r => r.status === 'cancelled').length
979
980 if (isCancelled.value) {
981 ElMessage.warning(`发布已取消:${successCount}个成功,${failCount}个失败,${cancelCount}个未执行`)
982 } else if (failCount > 0) {
983 ElMessage.error(`发布完成:${successCount}个成功,${failCount}个失败`)
984 } else {
985 ElMessage.success('所有Tab发布成功')
986 setTimeout(() => {
987 batchPublishDialogVisible.value = false
988 }, 1000)
989 }
990
991 } catch (error) {
992 console.error('批量发布出错:', error)
993 ElMessage.error('批量发布出错,请重试')
994 } finally {
995 batchPublishing.value = false
996 isCancelled.value = false
997 }
998 }
999 </script>
1000
1001 <style lang="scss" scoped>
1002 @use '@/styles/variables.scss' as *;
1003
1004 .publish-center {
1005 display: flex;
1006 flex-direction: column;
1007 height: 100%;
1008
1009 // Tab管理区域
1010 .tab-management {
1011 background-color: #fff;
1012 border-radius: 4px;
1013 box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
1014 margin-bottom: 20px;
1015 padding: 15px 20px;
1016
1017 .tab-header {
1018 display: flex;
1019 align-items: flex-start;
1020 gap: 15px;
1021
1022 .tab-list {
1023 display: flex;
1024 flex-wrap: wrap;
1025 gap: 10px;
1026 flex: 1;
1027 min-width: 0;
1028
1029 .tab-item {
1030 display: flex;
1031 align-items: center;
1032 gap: 6px;
1033 padding: 6px 12px;
1034 background-color: #f5f7fa;
1035 border: 1px solid #dcdfe6;
1036 border-radius: 4px;
1037 cursor: pointer;
1038 transition: all 0.3s;
1039 font-size: 14px;
1040 height: 32px;
1041
1042 &:hover {
1043 background-color: #ecf5ff;
1044 border-color: #b3d8ff;
1045 }
1046
1047 &.active {
1048 background-color: #409eff;
1049 border-color: #409eff;
1050 color: #fff;
1051
1052 .close-icon {
1053 color: #fff;
1054
1055 &:hover {
1056 background-color: rgba(255, 255, 255, 0.2);
1057 }
1058 }
1059 }
1060
1061 .close-icon {
1062 padding: 2px;
1063 border-radius: 2px;
1064 cursor: pointer;
1065 transition: background-color 0.3s;
1066 font-size: 12px;
1067
1068 &:hover {
1069 background-color: rgba(0, 0, 0, 0.1);
1070 }
1071 }
1072 }
1073 }
1074
1075 .tab-actions {
1076 display: flex;
1077 gap: 10px;
1078 flex-shrink: 0;
1079
1080 .add-tab-btn,
1081 .batch-publish-btn {
1082 display: flex;
1083 align-items: center;
1084 gap: 4px;
1085 height: 32px;
1086 padding: 6px 12px;
1087 font-size: 14px;
1088 white-space: nowrap;
1089 }
1090 }
1091 }
1092 }
1093
1094 // 批量发布进度对话框样式
1095 .publish-progress {
1096 padding: 20px;
1097
1098 .current-publishing {
1099 margin: 15px 0;
1100 text-align: center;
1101 color: #606266;
1102 }
1103
1104 .publish-results {
1105 margin-top: 20px;
1106 border-top: 1px solid #EBEEF5;
1107 padding-top: 15px;
1108 max-height: 300px;
1109 overflow-y: auto;
1110
1111 .result-item {
1112 display: flex;
1113 align-items: center;
1114 padding: 8px 0;
1115 color: #606266;
1116
1117 .el-icon {
1118 margin-right: 8px;
1119 }
1120
1121 .label {
1122 margin-right: 10px;
1123 font-weight: 500;
1124 }
1125
1126 .message {
1127 color: #909399;
1128 }
1129
1130 &.success {
1131 color: #67C23A;
1132 }
1133
1134 &.error {
1135 color: #F56C6C;
1136 }
1137
1138 &.cancelled {
1139 color: #909399;
1140 }
1141 }
1142 }
1143 }
1144
1145 .dialog-footer {
1146 text-align: right;
1147 }
1148
1149 // 内容区域
1150 .publish-content {
1151 flex: 1;
1152 background-color: #fff;
1153 border-radius: 4px;
1154 box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
1155 padding: 20px;
1156
1157 .tab-content-wrapper {
1158 display: flex;
1159 justify-content: center;
1160
1161 .tab-content {
1162 width: 100%;
1163 max-width: 800px;
1164
1165 h3 {
1166 font-size: 16px;
1167 font-weight: 500;
1168 color: $text-primary;
1169 margin: 0 0 10px 0;
1170 }
1171
1172 .upload-section,
1173 .account-section,
1174 .platform-section,
1175 .title-section,
1176 .product-section,
1177 .topic-section,
1178 .schedule-section {
1179 margin-bottom: 30px;
1180 }
1181
1182 .product-section {
1183 .product-name-input,
1184 .product-link-input {
1185 margin-bottom: 5px;
1186 }
1187 }
1188
1189 .video-upload {
1190 width: 100%;
1191
1192 :deep(.el-upload-dragger) {
1193 width: 100%;
1194 height: 180px;
1195 }
1196 }
1197
1198 .account-input {
1199 max-width: 400px;
1200 }
1201
1202 .platform-buttons {
1203 display: flex;
1204 gap: 10px;
1205 flex-wrap: wrap;
1206
1207 .platform-btn {
1208 min-width: 80px;
1209 }
1210 }
1211
1212 .title-input {
1213 max-width: 600px;
1214 }
1215
1216 .topic-display {
1217 display: flex;
1218 flex-direction: column;
1219 gap: 12px;
1220
1221 .selected-topics {
1222 display: flex;
1223 flex-wrap: wrap;
1224 gap: 8px;
1225 min-height: 32px;
1226
1227 .topic-tag {
1228 font-size: 14px;
1229 }
1230 }
1231
1232 .select-topic-btn {
1233 align-self: flex-start;
1234 }
1235 }
1236
1237 .schedule-controls {
1238 display: flex;
1239 flex-direction: column;
1240 gap: 15px;
1241
1242 .schedule-settings {
1243 margin-top: 15px;
1244 padding: 15px;
1245 background-color: #f5f7fa;
1246 border-radius: 4px;
1247
1248 .schedule-item {
1249 display: flex;
1250 align-items: center;
1251 margin-bottom: 15px;
1252
1253 &:last-child {
1254 margin-bottom: 0;
1255 }
1256
1257 .label {
1258 min-width: 120px;
1259 margin-right: 10px;
1260 }
1261
1262 .el-time-select {
1263 margin-right: 10px;
1264 }
1265
1266 .el-button {
1267 margin-left: 10px;
1268 }
1269 }
1270 }
1271 }
1272
1273 .action-buttons {
1274 display: flex;
1275 justify-content: flex-end;
1276 gap: 10px;
1277 margin-top: 30px;
1278 padding-top: 20px;
1279 border-top: 1px solid #ebeef5;
1280 }
1281
1282 .draft-section {
1283 margin: 20px 0;
1284
1285 .draft-checkbox {
1286 display: block;
1287 margin: 10px 0;
1288 }
1289 }
1290
1291 .original-section {
1292 margin: 10px 0 20px;
1293
1294 .original-checkbox {
1295 display: block;
1296 margin: 10px 0;
1297 }
1298 }
1299 }
1300 }
1301 }
1302
1303 // 已上传文件列表样式
1304 .uploaded-files {
1305 margin-top: 20px;
1306
1307 h4 {
1308 font-size: 16px;
1309 font-weight: 500;
1310 margin-bottom: 12px;
1311 color: #303133;
1312 }
1313
1314 .file-list {
1315 display: flex;
1316 flex-direction: column;
1317 gap: 10px;
1318
1319 .file-item {
1320 display: flex;
1321 align-items: center;
1322 padding: 10px 15px;
1323 background-color: #f5f7fa;
1324 border-radius: 4px;
1325
1326 .el-link {
1327 margin-right: 10px;
1328 max-width: 300px;
1329 overflow: hidden;
1330 text-overflow: ellipsis;
1331 white-space: nowrap;
1332 }
1333
1334 .file-size {
1335 color: #909399;
1336 font-size: 13px;
1337 margin-right: auto;
1338 }
1339 }
1340 }
1341 }
1342
1343 // 添加话题弹窗样式
1344 .topic-dialog {
1345 .topic-dialog-content {
1346 .custom-topic-input {
1347 display: flex;
1348 gap: 12px;
1349 margin-bottom: 24px;
1350
1351 .custom-input {
1352 flex: 1;
1353 }
1354 }
1355
1356 .recommended-topics {
1357 h4 {
1358 margin: 0 0 16px 0;
1359 font-size: 16px;
1360 font-weight: 500;
1361 color: #303133;
1362 }
1363
1364 .topic-grid {
1365 display: grid;
1366 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
1367 gap: 12px;
1368
1369 .topic-btn {
1370 height: 36px;
1371 font-size: 14px;
1372 border-radius: 6px;
1373 min-width: 100px;
1374 padding: 0 12px;
1375 white-space: nowrap;
1376 text-align: center;
1377 display: flex;
1378 align-items: center;
1379 justify-content: center;
1380
1381 &.el-button--primary {
1382 background-color: #409eff;
1383 border-color: #409eff;
1384 color: white;
1385 }
1386 }
1387 }
1388 }
1389 }
1390
1391 .dialog-footer {
1392 display: flex;
1393 justify-content: flex-end;
1394 gap: 12px;
1395 }
1396 }
1397 }
1398 </style>
1399
1399 lines Plain Text