1399 lines
39 KiB
Vue
1399 lines
39 KiB
Vue
<template>
|
||
<div class="publish-center">
|
||
<!-- Tab管理区域 -->
|
||
<div class="tab-management">
|
||
<div class="tab-header">
|
||
<div class="tab-list">
|
||
<div
|
||
v-for="tab in tabs"
|
||
:key="tab.name"
|
||
:class="['tab-item', { active: activeTab === tab.name }]"
|
||
@click="activeTab = tab.name"
|
||
>
|
||
<span>{{ tab.label }}</span>
|
||
<el-icon
|
||
v-if="tabs.length > 1"
|
||
class="close-icon"
|
||
@click.stop="removeTab(tab.name)"
|
||
>
|
||
<Close />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
<div class="tab-actions">
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
@click="addTab"
|
||
class="add-tab-btn"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
添加Tab
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
size="small"
|
||
@click="batchPublish"
|
||
:loading="batchPublishing"
|
||
class="batch-publish-btn"
|
||
>
|
||
批量发布
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域 -->
|
||
<div class="publish-content">
|
||
<div class="tab-content-wrapper">
|
||
<div
|
||
v-for="tab in tabs"
|
||
:key="tab.name"
|
||
v-show="activeTab === tab.name"
|
||
class="tab-content"
|
||
>
|
||
<!-- 发布状态提示 -->
|
||
<div v-if="tab.publishStatus" class="publish-status">
|
||
<el-alert
|
||
:title="tab.publishStatus.message"
|
||
:type="tab.publishStatus.type"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
</div>
|
||
|
||
<!-- 视频上传区域 -->
|
||
<div class="upload-section">
|
||
<h3>视频</h3>
|
||
<div class="upload-options">
|
||
<el-button type="primary" @click="showUploadOptions(tab)" class="upload-btn">
|
||
<el-icon><Upload /></el-icon>
|
||
上传视频
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 已上传文件列表 -->
|
||
<div v-if="tab.fileList.length > 0" class="uploaded-files">
|
||
<h4>已上传文件:</h4>
|
||
<div class="file-list">
|
||
<div v-for="(file, index) in tab.fileList" :key="index" class="file-item">
|
||
<el-link :href="file.url" target="_blank" type="primary">{{ file.name }}</el-link>
|
||
<span class="file-size">{{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
|
||
<el-button type="danger" size="small" @click="removeFile(tab, index)">删除</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传选项弹窗 -->
|
||
<el-dialog
|
||
v-model="uploadOptionsVisible"
|
||
title="选择上传方式"
|
||
width="400px"
|
||
class="upload-options-dialog"
|
||
>
|
||
<div class="upload-options-content">
|
||
<el-button type="primary" @click="selectLocalUpload" class="option-btn">
|
||
<el-icon><Upload /></el-icon>
|
||
本地上传
|
||
</el-button>
|
||
<el-button type="success" @click="selectMaterialLibrary" class="option-btn">
|
||
<el-icon><Folder /></el-icon>
|
||
素材库
|
||
</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 本地上传弹窗 -->
|
||
<el-dialog
|
||
v-model="localUploadVisible"
|
||
title="本地上传"
|
||
width="600px"
|
||
class="local-upload-dialog"
|
||
>
|
||
<el-upload
|
||
class="video-upload"
|
||
drag
|
||
:auto-upload="true"
|
||
:action="`${apiBaseUrl}/upload`"
|
||
:on-success="(response, file) => handleUploadSuccess(response, file, currentUploadTab)"
|
||
:on-error="handleUploadError"
|
||
multiple
|
||
accept="video/*"
|
||
:headers="authHeaders"
|
||
>
|
||
<el-icon class="el-icon--upload"><Upload /></el-icon>
|
||
<div class="el-upload__text">
|
||
将视频文件拖到此处,或<em>点击上传</em>
|
||
</div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">
|
||
支持MP4、AVI等视频格式,可上传多个文件
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
</el-dialog>
|
||
|
||
<!-- 批量发布进度对话框 -->
|
||
<el-dialog
|
||
v-model="batchPublishDialogVisible"
|
||
title="批量发布进度"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
:close-on-press-escape="false"
|
||
:show-close="false"
|
||
>
|
||
<div class="publish-progress">
|
||
<el-progress
|
||
:percentage="publishProgress"
|
||
:status="publishProgress === 100 ? 'success' : ''"
|
||
/>
|
||
<div v-if="currentPublishingTab" class="current-publishing">
|
||
正在发布:{{ currentPublishingTab.label }}
|
||
</div>
|
||
|
||
<!-- 发布结果列表 -->
|
||
<div class="publish-results" v-if="publishResults.length > 0">
|
||
<div
|
||
v-for="(result, index) in publishResults"
|
||
:key="index"
|
||
:class="['result-item', result.status]"
|
||
>
|
||
<el-icon v-if="result.status === 'success'"><Check /></el-icon>
|
||
<el-icon v-else-if="result.status === 'error'"><Close /></el-icon>
|
||
<el-icon v-else><InfoFilled /></el-icon>
|
||
<span class="label">{{ result.label }}</span>
|
||
<span class="message">{{ result.message }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button
|
||
@click="cancelBatchPublish"
|
||
:disabled="publishProgress === 100"
|
||
>
|
||
取消发布
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
@click="batchPublishDialogVisible = false"
|
||
v-if="publishProgress === 100"
|
||
>
|
||
关闭
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 素材库选择弹窗 -->
|
||
<el-dialog
|
||
v-model="materialLibraryVisible"
|
||
title="选择素材"
|
||
width="800px"
|
||
class="material-library-dialog"
|
||
>
|
||
<div class="material-library-content">
|
||
<el-checkbox-group v-model="selectedMaterials">
|
||
<div class="material-list">
|
||
<div
|
||
v-for="material in materials"
|
||
:key="material.id"
|
||
class="material-item"
|
||
>
|
||
<el-checkbox :label="material.id" class="material-checkbox">
|
||
<div class="material-info">
|
||
<div class="material-name">{{ material.filename }}</div>
|
||
<div class="material-details">
|
||
<span class="file-size">{{ material.filesize }}MB</span>
|
||
<span class="upload-time">{{ material.upload_time }}</span>
|
||
</div>
|
||
</div>
|
||
</el-checkbox>
|
||
</div>
|
||
</div>
|
||
</el-checkbox-group>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="materialLibraryVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmMaterialSelection">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 账号选择 -->
|
||
<div class="account-section">
|
||
<h3>账号</h3>
|
||
<div class="account-display">
|
||
<div class="selected-accounts">
|
||
<el-tag
|
||
v-for="(account, index) in tab.selectedAccounts"
|
||
:key="index"
|
||
closable
|
||
@close="removeAccount(tab, index)"
|
||
class="account-tag"
|
||
>
|
||
{{ getAccountDisplayName(account) }}
|
||
</el-tag>
|
||
</div>
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
@click="openAccountDialog(tab)"
|
||
class="select-account-btn"
|
||
>
|
||
选择账号
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号选择弹窗 -->
|
||
<el-dialog
|
||
v-model="accountDialogVisible"
|
||
title="选择账号"
|
||
width="600px"
|
||
class="account-dialog"
|
||
>
|
||
<div class="account-dialog-content">
|
||
<el-checkbox-group v-model="tempSelectedAccounts">
|
||
<div class="account-list">
|
||
<el-checkbox
|
||
v-for="account in availableAccounts"
|
||
:key="account.id"
|
||
:label="account.id"
|
||
class="account-item"
|
||
>
|
||
<div class="account-info">
|
||
<span class="account-name">{{ account.name }}</span>
|
||
</div>
|
||
</el-checkbox>
|
||
</div>
|
||
</el-checkbox-group>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="accountDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmAccountSelection">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 平台选择 -->
|
||
<div class="platform-section">
|
||
<h3>平台</h3>
|
||
<el-radio-group v-model="tab.selectedPlatform" class="platform-radios">
|
||
<el-radio
|
||
v-for="platform in platforms"
|
||
:key="platform.key"
|
||
:label="platform.key"
|
||
class="platform-radio"
|
||
>
|
||
{{ platform.name }}
|
||
</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
|
||
<!-- 原创声明 -->
|
||
<div class="original-section">
|
||
<el-checkbox
|
||
v-model="tab.isOriginal"
|
||
label="声明原创"
|
||
class="original-checkbox"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 草稿选项 (仅在视频号可见) -->
|
||
<div v-if="tab.selectedPlatform === 2" class="draft-section">
|
||
<el-checkbox
|
||
v-model="tab.isDraft"
|
||
label="视频号仅保存草稿(用手机发布)"
|
||
class="draft-checkbox"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 标签 (仅在抖音可见) -->
|
||
<div v-if="tab.selectedPlatform === 3" class="product-section">
|
||
<h3>商品链接</h3>
|
||
<el-input
|
||
v-model="tab.productTitle"
|
||
type="text"
|
||
:rows="1"
|
||
placeholder="请输入商品名称"
|
||
maxlength="200"
|
||
class="product-name-input"
|
||
/>
|
||
<el-input
|
||
v-model="tab.productLink"
|
||
type="text"
|
||
:rows="1"
|
||
placeholder="请输入商品链接"
|
||
maxlength="200"
|
||
class="product-link-input"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 标题输入 -->
|
||
<div class="title-section">
|
||
<h3>标题</h3>
|
||
<el-input
|
||
v-model="tab.title"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入标题"
|
||
maxlength="100"
|
||
show-word-limit
|
||
class="title-input"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 话题输入 -->
|
||
<div class="topic-section">
|
||
<h3>话题</h3>
|
||
<div class="topic-display">
|
||
<div class="selected-topics">
|
||
<el-tag
|
||
v-for="(topic, index) in tab.selectedTopics"
|
||
:key="index"
|
||
closable
|
||
@close="removeTopic(tab, index)"
|
||
class="topic-tag"
|
||
>
|
||
#{{ topic }}
|
||
</el-tag>
|
||
</div>
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
@click="openTopicDialog(tab)"
|
||
class="select-topic-btn"
|
||
>
|
||
添加话题
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加话题弹窗 -->
|
||
<el-dialog
|
||
v-model="topicDialogVisible"
|
||
title="添加话题"
|
||
width="600px"
|
||
class="topic-dialog"
|
||
>
|
||
<div class="topic-dialog-content">
|
||
<!-- 自定义话题输入 -->
|
||
<div class="custom-topic-input">
|
||
<el-input
|
||
v-model="customTopic"
|
||
placeholder="输入自定义话题"
|
||
class="custom-input"
|
||
>
|
||
<template #prepend>#</template>
|
||
</el-input>
|
||
<el-button type="primary" @click="addCustomTopic">添加</el-button>
|
||
</div>
|
||
|
||
<!-- 推荐话题 -->
|
||
<div class="recommended-topics">
|
||
<h4>推荐话题</h4>
|
||
<div class="topic-grid">
|
||
<el-button
|
||
v-for="topic in recommendedTopics"
|
||
:key="topic"
|
||
:type="currentTab?.selectedTopics?.includes(topic) ? 'primary' : 'default'"
|
||
@click="toggleRecommendedTopic(topic)"
|
||
class="topic-btn"
|
||
>
|
||
{{ topic }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="topicDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmTopicSelection">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 定时发布 -->
|
||
<div class="schedule-section">
|
||
<h3>定时发布</h3>
|
||
<div class="schedule-controls">
|
||
<el-switch
|
||
v-model="tab.scheduleEnabled"
|
||
active-text="定时发布"
|
||
inactive-text="立即发布"
|
||
/>
|
||
<div v-if="tab.scheduleEnabled" class="schedule-settings">
|
||
<div class="schedule-item">
|
||
<span class="label">每天发布视频数:</span>
|
||
<el-select v-model="tab.videosPerDay" placeholder="选择发布数量">
|
||
<el-option
|
||
v-for="num in 55"
|
||
:key="num"
|
||
:label="num"
|
||
:value="num"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<div class="schedule-item">
|
||
<span class="label">每天发布时间:</span>
|
||
<el-time-select
|
||
v-for="(time, index) in tab.dailyTimes"
|
||
:key="index"
|
||
v-model="tab.dailyTimes[index]"
|
||
start="00:00"
|
||
step="00:30"
|
||
end="23:30"
|
||
placeholder="选择时间"
|
||
/>
|
||
<el-button
|
||
v-if="tab.dailyTimes.length < tab.videosPerDay"
|
||
type="primary"
|
||
size="small"
|
||
@click="tab.dailyTimes.push('10:00')"
|
||
>
|
||
添加时间
|
||
</el-button>
|
||
</div>
|
||
<div class="schedule-item">
|
||
<span class="label">开始天数:</span>
|
||
<el-select v-model="tab.startDays" placeholder="选择开始天数">
|
||
<el-option :label="'明天'" :value="0" />
|
||
<el-option :label="'后天'" :value="1" />
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-buttons">
|
||
<el-button size="small" @click="cancelPublish(tab)">取消</el-button>
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
@click="confirmPublish(tab)"
|
||
:loading="tab.publishing || false"
|
||
>
|
||
{{ tab.publishing ? '发布中...' : '发布' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { Upload, Plus, Close, Folder } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useAccountStore } from '@/stores/account'
|
||
import { useAppStore } from '@/stores/app'
|
||
import { materialApi } from '@/api/material'
|
||
import { http } from '@/utils/request'
|
||
|
||
// API base URL
|
||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
|
||
|
||
// Authorization headers
|
||
const authHeaders = computed(() => ({
|
||
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
|
||
}))
|
||
|
||
// 当前激活的tab
|
||
const activeTab = ref('tab1')
|
||
|
||
// tab计数器
|
||
let tabCounter = 1
|
||
|
||
// 获取应用状态管理
|
||
const appStore = useAppStore()
|
||
|
||
// 上传相关状态
|
||
const uploadOptionsVisible = ref(false)
|
||
const localUploadVisible = ref(false)
|
||
const materialLibraryVisible = ref(false)
|
||
const currentUploadTab = ref(null)
|
||
const selectedMaterials = ref([])
|
||
const materials = computed(() => appStore.materials)
|
||
|
||
// 批量发布相关状态
|
||
const batchPublishing = ref(false)
|
||
const batchPublishMessage = ref('')
|
||
const batchPublishType = ref('info')
|
||
|
||
// 平台列表 - 对应后端type字段
|
||
const platforms = [
|
||
{ key: 3, name: '抖音' },
|
||
{ key: 4, name: '快手' },
|
||
{ key: 2, name: '视频号' },
|
||
{ key: 1, name: '小红书' }
|
||
]
|
||
|
||
const defaultTabInit = {
|
||
name: 'tab1',
|
||
label: '发布1',
|
||
fileList: [], // 后端返回的文件名列表
|
||
displayFileList: [], // 用于显示的文件列表
|
||
selectedAccounts: [], // 选中的账号ID列表
|
||
selectedPlatform: 1, // 选中的平台(单选)
|
||
title: '',
|
||
productLink: '', // 商品链接
|
||
productTitle: '', // 商品名称
|
||
selectedTopics: [], // 话题列表(不带#号)
|
||
scheduleEnabled: false, // 定时发布开关
|
||
videosPerDay: 1, // 每天发布视频数量
|
||
dailyTimes: ['10:00'], // 每天发布时间点列表
|
||
startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
|
||
publishStatus: null, // 发布状态,包含message和type
|
||
publishing: false, // 发布状态,用于控制按钮loading效果
|
||
isDraft: false, // 是否保存为草稿,仅视频号平台可见
|
||
isOriginal: false // 是否标记为原创
|
||
}
|
||
|
||
// helper to create a fresh deep-copied tab from defaultTabInit
|
||
const makeNewTab = () => {
|
||
// prefer structuredClone when available (newer browsers/node), fallback to JSON
|
||
try {
|
||
return typeof structuredClone === 'function' ? structuredClone(defaultTabInit) : JSON.parse(JSON.stringify(defaultTabInit))
|
||
} catch (e) {
|
||
return JSON.parse(JSON.stringify(defaultTabInit))
|
||
}
|
||
}
|
||
|
||
// tab页数据 - 默认只有一个tab (use deep copy to avoid shared refs)
|
||
const tabs = reactive([
|
||
makeNewTab()
|
||
])
|
||
|
||
// 账号相关状态
|
||
const accountDialogVisible = ref(false)
|
||
const tempSelectedAccounts = ref([])
|
||
const currentTab = ref(null)
|
||
|
||
// 获取账号状态管理
|
||
const accountStore = useAccountStore()
|
||
|
||
// 根据选择的平台获取可用账号列表
|
||
const availableAccounts = computed(() => {
|
||
const platformMap = {
|
||
3: '抖音',
|
||
2: '视频号',
|
||
1: '小红书',
|
||
4: '快手'
|
||
}
|
||
const currentPlatform = currentTab.value ? platformMap[currentTab.value.selectedPlatform] : null
|
||
return currentPlatform ? accountStore.accounts.filter(acc => acc.platform === currentPlatform) : []
|
||
})
|
||
|
||
// 话题相关状态
|
||
const topicDialogVisible = ref(false)
|
||
const customTopic = ref('')
|
||
|
||
// 推荐话题列表
|
||
const recommendedTopics = [
|
||
'游戏', '电影', '音乐', '美食', '旅行', '文化',
|
||
'科技', '生活', '娱乐', '体育', '教育', '艺术',
|
||
'健康', '时尚', '美妆', '摄影', '宠物', '汽车'
|
||
]
|
||
|
||
// 添加新tab
|
||
const addTab = () => {
|
||
tabCounter++
|
||
const newTab = makeNewTab()
|
||
newTab.name = `tab${tabCounter}`
|
||
newTab.label = `发布${tabCounter}`
|
||
tabs.push(newTab)
|
||
activeTab.value = newTab.name
|
||
}
|
||
|
||
// 删除tab
|
||
const removeTab = (tabName) => {
|
||
const index = tabs.findIndex(tab => tab.name === tabName)
|
||
if (index > -1) {
|
||
tabs.splice(index, 1)
|
||
// 如果删除的是当前激活的tab,切换到第一个tab
|
||
if (activeTab.value === tabName && tabs.length > 0) {
|
||
activeTab.value = tabs[0].name
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理文件上传成功
|
||
const handleUploadSuccess = (response, file, tab) => {
|
||
if (response.code === 200) {
|
||
// 获取文件路径
|
||
const filePath = response.data.path || response.data
|
||
// 从路径中提取文件名
|
||
const filename = filePath.split('/').pop()
|
||
|
||
// 保存文件信息到fileList,包含文件路径和其他信息
|
||
const fileInfo = {
|
||
name: file.name,
|
||
url: materialApi.getMaterialPreviewUrl(filename), // 使用getMaterialPreviewUrl生成预览URL
|
||
path: filePath,
|
||
size: file.size,
|
||
type: file.type
|
||
}
|
||
|
||
// 添加到文件列表
|
||
tab.fileList.push(fileInfo)
|
||
|
||
// 更新显示列表
|
||
tab.displayFileList = [...tab.fileList.map(item => ({
|
||
name: item.name,
|
||
url: item.url
|
||
}))]
|
||
|
||
ElMessage.success('文件上传成功')
|
||
} else {
|
||
ElMessage.error(response.msg || '上传失败')
|
||
}
|
||
}
|
||
|
||
// 处理文件上传失败
|
||
const handleUploadError = (error) => {
|
||
ElMessage.error('文件上传失败')
|
||
}
|
||
|
||
// 删除已上传文件
|
||
const removeFile = (tab, index) => {
|
||
// 从文件列表中删除
|
||
tab.fileList.splice(index, 1)
|
||
|
||
// 更新显示列表
|
||
tab.displayFileList = [...tab.fileList.map(item => ({
|
||
name: item.name,
|
||
url: item.url
|
||
}))]
|
||
|
||
ElMessage.success('文件删除成功')
|
||
}
|
||
|
||
// 话题相关方法
|
||
// 打开添加话题弹窗
|
||
const openTopicDialog = (tab) => {
|
||
currentTab.value = tab
|
||
topicDialogVisible.value = true
|
||
}
|
||
|
||
// 添加自定义话题
|
||
const addCustomTopic = () => {
|
||
if (!customTopic.value.trim()) {
|
||
ElMessage.warning('请输入话题内容')
|
||
return
|
||
}
|
||
if (currentTab.value && !currentTab.value.selectedTopics.includes(customTopic.value.trim())) {
|
||
currentTab.value.selectedTopics.push(customTopic.value.trim())
|
||
customTopic.value = ''
|
||
ElMessage.success('话题添加成功')
|
||
} else {
|
||
ElMessage.warning('话题已存在')
|
||
}
|
||
}
|
||
|
||
// 切换推荐话题
|
||
const toggleRecommendedTopic = (topic) => {
|
||
if (!currentTab.value) return
|
||
|
||
const index = currentTab.value.selectedTopics.indexOf(topic)
|
||
if (index > -1) {
|
||
currentTab.value.selectedTopics.splice(index, 1)
|
||
} else {
|
||
currentTab.value.selectedTopics.push(topic)
|
||
}
|
||
}
|
||
|
||
// 删除话题
|
||
const removeTopic = (tab, index) => {
|
||
tab.selectedTopics.splice(index, 1)
|
||
}
|
||
|
||
// 确认添加话题
|
||
const confirmTopicSelection = () => {
|
||
topicDialogVisible.value = false
|
||
customTopic.value = ''
|
||
currentTab.value = null
|
||
ElMessage.success('添加话题完成')
|
||
}
|
||
|
||
// 账号选择相关方法
|
||
// 打开账号选择弹窗
|
||
const openAccountDialog = (tab) => {
|
||
currentTab.value = tab
|
||
tempSelectedAccounts.value = [...tab.selectedAccounts]
|
||
accountDialogVisible.value = true
|
||
}
|
||
|
||
// 确认账号选择
|
||
const confirmAccountSelection = () => {
|
||
if (currentTab.value) {
|
||
currentTab.value.selectedAccounts = [...tempSelectedAccounts.value]
|
||
}
|
||
accountDialogVisible.value = false
|
||
currentTab.value = null
|
||
ElMessage.success('账号选择完成')
|
||
}
|
||
|
||
// 删除选中的账号
|
||
const removeAccount = (tab, index) => {
|
||
tab.selectedAccounts.splice(index, 1)
|
||
}
|
||
|
||
// 获取账号显示名称
|
||
const getAccountDisplayName = (accountId) => {
|
||
const account = accountStore.accounts.find(acc => acc.id === accountId)
|
||
return account ? account.name : accountId
|
||
}
|
||
|
||
// 取消发布
|
||
const cancelPublish = (tab) => {
|
||
ElMessage.info('已取消发布')
|
||
}
|
||
|
||
// 确认发布
|
||
const confirmPublish = async (tab) => {
|
||
// 防止重复点击
|
||
if (tab.publishing) {
|
||
throw new Error('正在发布中,请稍候...')
|
||
}
|
||
|
||
tab.publishing = true // 设置发布状态为进行中
|
||
|
||
// 数据验证
|
||
if (tab.fileList.length === 0) {
|
||
ElMessage.error('请先上传视频文件')
|
||
tab.publishing = false
|
||
throw new Error('请先上传视频文件')
|
||
}
|
||
if (!tab.title.trim()) {
|
||
ElMessage.error('请输入标题')
|
||
tab.publishing = false
|
||
throw new Error('请输入标题')
|
||
}
|
||
if (!tab.selectedPlatform) {
|
||
ElMessage.error('请选择发布平台')
|
||
tab.publishing = false
|
||
throw new Error('请选择发布平台')
|
||
}
|
||
if (tab.selectedAccounts.length === 0) {
|
||
ElMessage.error('请选择发布账号')
|
||
tab.publishing = false
|
||
throw new Error('请选择发布账号')
|
||
}
|
||
|
||
// 构造发布数据,符合后端API格式
|
||
const publishData = {
|
||
type: tab.selectedPlatform,
|
||
title: tab.title,
|
||
tags: tab.selectedTopics, // 不带#号的话题列表
|
||
fileList: tab.fileList.map(file => file.path), // 只发送文件路径
|
||
accountList: tab.selectedAccounts.map(accountId => {
|
||
const account = accountStore.accounts.find(acc => acc.id === accountId)
|
||
return account ? account.filePath : accountId
|
||
}), // 发送账号的文件路径
|
||
enableTimer: tab.scheduleEnabled ? 1 : 0,
|
||
videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1,
|
||
dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'],
|
||
startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0,
|
||
category: tab.isOriginal ? 1 : 0, // 1表示原创,0表示非原创
|
||
productLink: tab.productLink.trim() || '',
|
||
productTitle: tab.productTitle.trim() || '',
|
||
isDraft: tab.isDraft
|
||
}
|
||
|
||
// 调用后端发布API(使用统一的http封装)
|
||
try {
|
||
const data = await http.post('/postVideo', publishData)
|
||
tab.publishStatus = {
|
||
message: '发布成功',
|
||
type: 'success'
|
||
}
|
||
// 清空当前tab的数据
|
||
tab.fileList = []
|
||
tab.displayFileList = []
|
||
tab.title = ''
|
||
tab.selectedTopics = []
|
||
tab.selectedAccounts = []
|
||
tab.scheduleEnabled = false
|
||
} catch (error) {
|
||
console.error('发布错误:', error)
|
||
tab.publishStatus = {
|
||
message: `发布失败:${error.message || '请检查网络连接'}`,
|
||
type: 'error'
|
||
}
|
||
throw error
|
||
} finally {
|
||
tab.publishing = false
|
||
}
|
||
}
|
||
|
||
// 显示上传选项
|
||
const showUploadOptions = (tab) => {
|
||
currentUploadTab.value = tab
|
||
uploadOptionsVisible.value = true
|
||
}
|
||
|
||
// 选择本地上传
|
||
const selectLocalUpload = () => {
|
||
uploadOptionsVisible.value = false
|
||
localUploadVisible.value = true
|
||
}
|
||
|
||
// 选择素材库
|
||
const selectMaterialLibrary = async () => {
|
||
uploadOptionsVisible.value = false
|
||
|
||
// 如果素材库为空,先获取素材数据
|
||
if (materials.value.length === 0) {
|
||
try {
|
||
const response = await materialApi.getAllMaterials()
|
||
if (response.code === 200) {
|
||
appStore.setMaterials(response.data)
|
||
} else {
|
||
ElMessage.error('获取素材列表失败')
|
||
return
|
||
}
|
||
} catch (error) {
|
||
console.error('获取素材列表出错:', error)
|
||
ElMessage.error('获取素材列表失败')
|
||
return
|
||
}
|
||
}
|
||
|
||
selectedMaterials.value = []
|
||
materialLibraryVisible.value = true
|
||
}
|
||
|
||
// 确认素材选择
|
||
const confirmMaterialSelection = () => {
|
||
if (selectedMaterials.value.length === 0) {
|
||
ElMessage.warning('请选择至少一个素材')
|
||
return
|
||
}
|
||
|
||
if (currentUploadTab.value) {
|
||
// 将选中的素材添加到当前tab的文件列表
|
||
selectedMaterials.value.forEach(materialId => {
|
||
const material = materials.value.find(m => m.id === materialId)
|
||
if (material) {
|
||
const fileInfo = {
|
||
name: material.filename,
|
||
url: materialApi.getMaterialPreviewUrl(material.file_path.split('/').pop()),
|
||
path: material.file_path,
|
||
size: material.filesize * 1024 * 1024, // 转换为字节
|
||
type: 'video/mp4'
|
||
}
|
||
|
||
// 检查是否已存在相同文件
|
||
const exists = currentUploadTab.value.fileList.some(file => file.path === fileInfo.path)
|
||
if (!exists) {
|
||
currentUploadTab.value.fileList.push(fileInfo)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 更新显示列表
|
||
currentUploadTab.value.displayFileList = [...currentUploadTab.value.fileList.map(item => ({
|
||
name: item.name,
|
||
url: item.url
|
||
}))]
|
||
}
|
||
|
||
const addedCount = selectedMaterials.value.length
|
||
materialLibraryVisible.value = false
|
||
selectedMaterials.value = []
|
||
currentUploadTab.value = null
|
||
ElMessage.success(`已添加 ${addedCount} 个素材`)
|
||
}
|
||
|
||
// 批量发布对话框状态
|
||
const batchPublishDialogVisible = ref(false)
|
||
const currentPublishingTab = ref(null)
|
||
const publishProgress = ref(0)
|
||
const publishResults = ref([])
|
||
const isCancelled = ref(false)
|
||
|
||
// 取消批量发布
|
||
const cancelBatchPublish = () => {
|
||
isCancelled.value = true
|
||
ElMessage.info('正在取消发布...')
|
||
}
|
||
|
||
// 批量发布方法
|
||
const batchPublish = async () => {
|
||
if (batchPublishing.value) return
|
||
|
||
batchPublishing.value = true
|
||
currentPublishingTab.value = null
|
||
publishProgress.value = 0
|
||
publishResults.value = []
|
||
isCancelled.value = false
|
||
batchPublishDialogVisible.value = true
|
||
|
||
try {
|
||
for (let i = 0; i < tabs.length; i++) {
|
||
if (isCancelled.value) {
|
||
publishResults.value.push({
|
||
label: tabs[i].label,
|
||
status: 'cancelled',
|
||
message: '已取消'
|
||
})
|
||
continue
|
||
}
|
||
|
||
const tab = tabs[i]
|
||
currentPublishingTab.value = tab
|
||
publishProgress.value = Math.floor((i / tabs.length) * 100)
|
||
|
||
try {
|
||
await confirmPublish(tab)
|
||
publishResults.value.push({
|
||
label: tab.label,
|
||
status: 'success',
|
||
message: '发布成功'
|
||
})
|
||
} catch (error) {
|
||
publishResults.value.push({
|
||
label: tab.label,
|
||
status: 'error',
|
||
message: error.message
|
||
})
|
||
// 不立即返回,继续显示发布结果
|
||
}
|
||
}
|
||
|
||
publishProgress.value = 100
|
||
|
||
// 统计发布结果
|
||
const successCount = publishResults.value.filter(r => r.status === 'success').length
|
||
const failCount = publishResults.value.filter(r => r.status === 'error').length
|
||
const cancelCount = publishResults.value.filter(r => r.status === 'cancelled').length
|
||
|
||
if (isCancelled.value) {
|
||
ElMessage.warning(`发布已取消:${successCount}个成功,${failCount}个失败,${cancelCount}个未执行`)
|
||
} else if (failCount > 0) {
|
||
ElMessage.error(`发布完成:${successCount}个成功,${failCount}个失败`)
|
||
} else {
|
||
ElMessage.success('所有Tab发布成功')
|
||
setTimeout(() => {
|
||
batchPublishDialogVisible.value = false
|
||
}, 1000)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('批量发布出错:', error)
|
||
ElMessage.error('批量发布出错,请重试')
|
||
} finally {
|
||
batchPublishing.value = false
|
||
isCancelled.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@use '@/styles/variables.scss' as *;
|
||
|
||
.publish-center {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
|
||
// Tab管理区域
|
||
.tab-management {
|
||
background-color: #fff;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 20px;
|
||
padding: 15px 20px;
|
||
|
||
.tab-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 15px;
|
||
|
||
.tab-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.tab-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
background-color: #f5f7fa;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-size: 14px;
|
||
height: 32px;
|
||
|
||
&:hover {
|
||
background-color: #ecf5ff;
|
||
border-color: #b3d8ff;
|
||
}
|
||
|
||
&.active {
|
||
background-color: #409eff;
|
||
border-color: #409eff;
|
||
color: #fff;
|
||
|
||
.close-icon {
|
||
color: #fff;
|
||
|
||
&:hover {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
}
|
||
}
|
||
}
|
||
|
||
.close-icon {
|
||
padding: 2px;
|
||
border-radius: 2px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
font-size: 12px;
|
||
|
||
&:hover {
|
||
background-color: rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.tab-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-shrink: 0;
|
||
|
||
.add-tab-btn,
|
||
.batch-publish-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
height: 32px;
|
||
padding: 6px 12px;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量发布进度对话框样式
|
||
.publish-progress {
|
||
padding: 20px;
|
||
|
||
.current-publishing {
|
||
margin: 15px 0;
|
||
text-align: center;
|
||
color: #606266;
|
||
}
|
||
|
||
.publish-results {
|
||
margin-top: 20px;
|
||
border-top: 1px solid #EBEEF5;
|
||
padding-top: 15px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
|
||
.result-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
color: #606266;
|
||
|
||
.el-icon {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.label {
|
||
margin-right: 10px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.message {
|
||
color: #909399;
|
||
}
|
||
|
||
&.success {
|
||
color: #67C23A;
|
||
}
|
||
|
||
&.error {
|
||
color: #F56C6C;
|
||
}
|
||
|
||
&.cancelled {
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
text-align: right;
|
||
}
|
||
|
||
// 内容区域
|
||
.publish-content {
|
||
flex: 1;
|
||
background-color: #fff;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||
padding: 20px;
|
||
|
||
.tab-content-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
|
||
.tab-content {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
|
||
h3 {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: $text-primary;
|
||
margin: 0 0 10px 0;
|
||
}
|
||
|
||
.upload-section,
|
||
.account-section,
|
||
.platform-section,
|
||
.title-section,
|
||
.product-section,
|
||
.topic-section,
|
||
.schedule-section {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.product-section {
|
||
.product-name-input,
|
||
.product-link-input {
|
||
margin-bottom: 5px;
|
||
}
|
||
}
|
||
|
||
.video-upload {
|
||
width: 100%;
|
||
|
||
:deep(.el-upload-dragger) {
|
||
width: 100%;
|
||
height: 180px;
|
||
}
|
||
}
|
||
|
||
.account-input {
|
||
max-width: 400px;
|
||
}
|
||
|
||
.platform-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
|
||
.platform-btn {
|
||
min-width: 80px;
|
||
}
|
||
}
|
||
|
||
.title-input {
|
||
max-width: 600px;
|
||
}
|
||
|
||
.topic-display {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.selected-topics {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
min-height: 32px;
|
||
|
||
.topic-tag {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.select-topic-btn {
|
||
align-self: flex-start;
|
||
}
|
||
}
|
||
|
||
.schedule-controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
|
||
.schedule-settings {
|
||
margin-top: 15px;
|
||
padding: 15px;
|
||
background-color: #f5f7fa;
|
||
border-radius: 4px;
|
||
|
||
.schedule-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.label {
|
||
min-width: 120px;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.el-time-select {
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.el-button {
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
margin-top: 30px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
|
||
.draft-section {
|
||
margin: 20px 0;
|
||
|
||
.draft-checkbox {
|
||
display: block;
|
||
margin: 10px 0;
|
||
}
|
||
}
|
||
|
||
.original-section {
|
||
margin: 10px 0 20px;
|
||
|
||
.original-checkbox {
|
||
display: block;
|
||
margin: 10px 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 已上传文件列表样式
|
||
.uploaded-files {
|
||
margin-top: 20px;
|
||
|
||
h4 {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
margin-bottom: 12px;
|
||
color: #303133;
|
||
}
|
||
|
||
.file-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 15px;
|
||
background-color: #f5f7fa;
|
||
border-radius: 4px;
|
||
|
||
.el-link {
|
||
margin-right: 10px;
|
||
max-width: 300px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-size {
|
||
color: #909399;
|
||
font-size: 13px;
|
||
margin-right: auto;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加话题弹窗样式
|
||
.topic-dialog {
|
||
.topic-dialog-content {
|
||
.custom-topic-input {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 24px;
|
||
|
||
.custom-input {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.recommended-topics {
|
||
h4 {
|
||
margin: 0 0 16px 0;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.topic-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||
gap: 12px;
|
||
|
||
.topic-btn {
|
||
height: 36px;
|
||
font-size: 14px;
|
||
border-radius: 6px;
|
||
min-width: 100px;
|
||
padding: 0 12px;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&.el-button--primary {
|
||
background-color: #409eff;
|
||
border-color: #409eff;
|
||
color: white;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|