feat: 深度优化自媒体发布系统及修复关键BUG

This commit is contained in:
钟停为止
2026-03-05 10:12:40 +08:00
parent ab2dbb0de6
commit 23ee8d014f
11 changed files with 440 additions and 689 deletions
+80 -33
View File
@@ -48,14 +48,14 @@ def index(): # put application's code here
def upload_file():
if 'file' not in request.files:
return jsonify({
"code": 200,
"code": 400,
"data": None,
"msg": "No file part in the request"
}), 400
file = request.files['file']
if file.filename == '':
return jsonify({
"code": 200,
"code": 400,
"data": None,
"msg": "No selected file"
}), 400
@@ -67,7 +67,7 @@ def upload_file():
file.save(filepath)
return jsonify({"code":200,"msg": "File uploaded successfully", "data": f"{uuid_v1}_{file.filename}"}), 200
except Exception as e:
return jsonify({"code":200,"msg": str(e),"data":None}), 500
return jsonify({"code":500,"msg": str(e),"data":None}), 500
@app.route('/getFile', methods=['GET'])
def get_file():
@@ -75,11 +75,11 @@ def get_file():
filename = request.args.get('filename')
if not filename:
return {"error": "filename is required"}, 400
return jsonify({"code": 400, "msg": "filename is required", "data": None}), 400
# 防止路径穿越攻击
if '..' in filename or filename.startswith('/'):
return {"error": "Invalid filename"}, 400
return jsonify({"code": 400, "msg": "Invalid filename", "data": None}), 400
# 拼接完整路径
file_path = str(Path(BASE_DIR / "videoFile"))
@@ -316,7 +316,16 @@ def delete_file():
@app.route('/deleteAccount', methods=['GET'])
def delete_account():
account_id = int(request.args.get('id'))
account_id = request.args.get('id')
if not account_id or not account_id.isdigit():
return jsonify({
"code": 400,
"msg": "Invalid or missing account ID",
"data": None
}), 400
account_id = int(account_id)
try:
# 获取数据库连接
@@ -337,6 +346,16 @@ def delete_account():
record = dict(record)
# 删除关联的cookie文件
if record.get('filePath'):
cookie_file_path = Path(BASE_DIR / "cookiesFile" / record['filePath'])
if cookie_file_path.exists():
try:
cookie_file_path.unlink()
print(f"✅ Cookie文件已删除: {cookie_file_path}")
except Exception as e:
print(f"⚠️ 删除Cookie文件失败: {e}")
# 删除数据库记录
cursor.execute("DELETE FROM user_info WHERE id = ?", (account_id,))
conn.commit()
@@ -350,7 +369,7 @@ def delete_account():
except Exception as e:
return jsonify({
"code": 500,
"msg": str("delete failed!"),
"msg": f"delete failed: {str(e)}",
"data": None
}), 500
@@ -385,6 +404,9 @@ def postVideo():
# 获取JSON数据
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "请求数据不能为空", "data": None}), 400
# 从JSON数据中提取fileList和accountList
file_list = data.get('fileList', [])
account_list = data.get('accountList', [])
@@ -403,29 +425,52 @@ def postVideo():
videos_per_day = data.get('videosPerDay')
daily_times = data.get('dailyTimes')
start_days = data.get('startDays')
# 参数校验
if not file_list:
return jsonify({"code": 400, "msg": "文件列表不能为空", "data": None}), 400
if not account_list:
return jsonify({"code": 400, "msg": "账号列表不能为空", "data": None}), 400
if not type:
return jsonify({"code": 400, "msg": "平台类型不能为空", "data": None}), 400
if not title:
return jsonify({"code": 400, "msg": "标题不能为空", "data": None}), 400
# 打印获取到的数据(仅作为示例)
print("File List:", file_list)
print("Account List:", account_list)
match type:
case 1:
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case 2:
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, is_draft)
case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, thumbnail_path, productLink, productTitle)
case 4:
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
# 返回响应给客户端
return jsonify(
{
"code": 200,
"msg": None,
try:
match type:
case 1:
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case 2:
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, is_draft)
case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, thumbnail_path, productLink, productTitle)
case 4:
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case _:
return jsonify({"code": 400, "msg": f"不支持的平台类型: {type}", "data": None}), 400
# 返回响应给客户端
return jsonify(
{
"code": 200,
"msg": "发布任务已提交",
"data": None
}), 200
except Exception as e:
print(f"发布视频时出错: {str(e)}")
return jsonify({
"code": 500,
"msg": f"发布失败: {str(e)}",
"data": None
}), 200
}), 500
@app.route('/updateUserinfo', methods=['POST'])
@@ -470,7 +515,7 @@ def postVideoBatch():
data_list = request.get_json()
if not isinstance(data_list, list):
return jsonify({"error": "Expected a JSON array"}), 400
return jsonify({"code": 400, "msg": "Expected a JSON array", "data": None}), 400
for data in data_list:
# 从JSON数据中提取fileList和accountList
file_list = data.get('fileList', [])
@@ -484,6 +529,7 @@ def postVideoBatch():
category = None
productLink = data.get('productLink', '')
productTitle = data.get('productTitle', '')
is_draft = data.get('isDraft', False)
videos_per_day = data.get('videosPerDay')
daily_times = data.get('dailyTimes')
@@ -493,10 +539,11 @@ def postVideoBatch():
print("Account List:", account_list)
match type:
case 1:
return
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case 2:
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
start_days, is_draft)
case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, productLink, productTitle)
@@ -517,7 +564,7 @@ def upload_cookie():
try:
if 'file' not in request.files:
return jsonify({
"code": 500,
"code": 400,
"msg": "没有找到Cookie文件",
"data": None
}), 400
@@ -525,14 +572,14 @@ def upload_cookie():
file = request.files['file']
if file.filename == '':
return jsonify({
"code": 500,
"code": 400,
"msg": "Cookie文件名不能为空",
"data": None
}), 400
if not file.filename.endswith('.json'):
return jsonify({
"code": 500,
"code": 400,
"msg": "Cookie文件必须是JSON格式",
"data": None
}), 400
@@ -543,7 +590,7 @@ def upload_cookie():
if not account_id or not platform:
return jsonify({
"code": 500,
"code": 400,
"msg": "缺少账号ID或平台信息",
"data": None
}), 400
+4 -8
View File
@@ -32,13 +32,9 @@
<el-icon><Upload /></el-icon>
<span>发布中心</span>
</el-menu-item>
<el-menu-item index="/website">
<el-icon><Monitor /></el-icon>
<span>网站</span>
</el-menu-item>
<el-menu-item index="/data">
<el-menu-item index="/about">
<el-icon><DataAnalysis /></el-icon>
<span>数据</span>
<span>关于</span>
</el-menu-item>
</el-menu>
</div>
@@ -65,8 +61,8 @@
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import {
HomeFilled, User, Monitor, DataAnalysis,
import {
HomeFilled, User, DataAnalysis,
Fold, Picture, Upload
} from '@element-plus/icons-vue'
+3 -47
View File
@@ -1,49 +1,5 @@
import { http } from '@/utils/request'
// 用户相关API
export const userApi = {
// 获取用户信息
getUserInfo(id) {
return http.get(`/user/${id}`)
},
// 获取用户列表
getUserList(params) {
return http.get('/user/list', params)
},
// 创建用户
createUser(data) {
return http.post('/user', data)
},
// 更新用户信息
updateUser(id, data) {
return http.put(`/user/${id}`, data)
},
// 删除用户
deleteUser(id) {
return http.delete(`/user/${id}`)
},
// 用户登录
login(data) {
return http.post('/auth/login', data)
},
// 用户注册
register(data) {
return http.post('/auth/register', data)
},
// 用户登出
logout() {
return http.post('/auth/logout')
},
// 刷新token
refreshToken() {
return http.post('/auth/refresh')
}
}
// 用户相关API(预留)
// 注意:当前后端暂无用户认证接口,以下为预留定义
export const userApi = {}
@@ -1,66 +0,0 @@
<template>
<div class="hello-world">
<h1>{{ msg }}</h1>
<div class="card">
<el-button type="primary" @click="count++">
点击次数: {{ count }}
</el-button>
<p class="mt-3">
这是一个使用 Element Plus 的示例组件
</p>
</div>
<div class="links mt-4">
<el-link href="https://vuejs.org/" target="_blank" type="primary">
Vue.js 官网
</el-link>
<el-link href="https://element-plus.org/" target="_blank" type="success">
Element Plus 官网
</el-link>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
defineProps({
msg: {
type: String,
default: 'Hello Vue 3 + Vite'
}
})
const count = ref(0)
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
.hello-world {
text-align: center;
padding: 2rem;
h1 {
color: #2c3e50;
margin-bottom: 2rem;
}
.card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
p {
color: #666;
}
}
.links {
display: flex;
justify-content: center;
gap: 1rem;
}
}
</style>
+2 -3
View File
@@ -22,9 +22,8 @@ export const useAccountStore = defineStore('account', () => {
type: item[1],
filePath: item[2],
name: item[3],
status: item[4] === 1 ? '正常' : '异常',
platform: platformTypes[item[1]] || '未知',
avatar: '/vite.svg' // 默认使用vite.svg作为头像
status: item[4] === -1 ? '验证中' : (item[4] === 1 ? '正常' : '异常'),
platform: platformTypes[item[1]] || '未知'
}
})
}
+2 -2
View File
@@ -34,8 +34,8 @@ request.interceptors.response.use(
if (data.code === 200 || data.success) {
return data
} else {
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
ElMessage.error(data.msg || data.message || '请求失败')
return Promise.reject(new Error(data.msg || data.message || '请求失败'))
}
},
(error) => {
+99 -13
View File
@@ -1,7 +1,54 @@
<template>
<div class="about">
<h1>关于我们</h1>
<p>这是关于页面</p>
<el-card class="about-card">
<div class="about-header">
<h1>自媒体自动化运营系统</h1>
<p class="version">social-auto-upload</p>
</div>
<el-divider />
<div class="about-section">
<h3>系统简介</h3>
<p>
本系统是一款强大的自动化工具帮助内容创作者和运营人员一键将视频内容高效发布到多个国内外主流社交媒体平台
支持视频上传定时发布等功能
</p>
</div>
<div class="about-section">
<h3>支持平台</h3>
<div class="platform-tags">
<el-tag type="danger">抖音</el-tag>
<el-tag type="success">快手</el-tag>
<el-tag type="warning">视频号</el-tag>
<el-tag type="info">小红书</el-tag>
</div>
</div>
<div class="about-section">
<h3>核心功能</h3>
<ul class="feature-list">
<li>多平台账号管理与登录状态维护</li>
<li>视频素材上传与管理</li>
<li>一键多平台发布</li>
<li>定时发布与批量发布</li>
<li>Cookie 导入导出</li>
</ul>
</div>
<div class="about-section">
<h3>技术栈</h3>
<div class="tech-tags">
<el-tag effect="plain">Vue 3</el-tag>
<el-tag effect="plain">Element Plus</el-tag>
<el-tag effect="plain">Pinia</el-tag>
<el-tag effect="plain">Flask</el-tag>
<el-tag effect="plain">Playwright</el-tag>
<el-tag effect="plain">SQLite</el-tag>
</div>
</div>
</el-card>
</div>
</template>
@@ -13,16 +60,55 @@
@use '@/styles/variables.scss' as *;
.about {
text-align: center;
padding: 2rem;
h1 {
color: #2c3e50;
margin-bottom: 1rem;
}
p {
color: #7f8c8d;
max-width: 700px;
margin: 0 auto;
.about-card {
.about-header {
text-align: center;
h1 {
color: $text-primary;
margin: 0 0 8px 0;
font-size: 24px;
}
.version {
color: $text-secondary;
font-size: 14px;
margin: 0;
}
}
.about-section {
margin-bottom: 24px;
h3 {
font-size: 16px;
color: $text-primary;
margin: 0 0 12px 0;
}
p {
color: $text-secondary;
line-height: 1.8;
margin: 0;
}
.platform-tags,
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.feature-list {
margin: 0;
padding-left: 20px;
color: $text-secondary;
line-height: 2;
}
}
}
}
</style>
</style>
+12 -28
View File
@@ -433,6 +433,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { accountApi } from '@/api/account'
import { useAccountStore } from '@/stores/account'
import { useAppStore } from '@/stores/app'
import { http } from '@/utils/request'
// 获取账号状态管理
const accountStore = useAccountStore()
@@ -452,9 +453,8 @@ const fetchAccountsQuick = async () => {
if (res.code === 200 && res.data) {
// 将所有账号的状态暂时设为"验证中"
const accountsWithPendingStatus = res.data.map(account => {
// account[4] 是状态字段,暂时设为"验证中"
const updatedAccount = [...account];
updatedAccount[4] = '验证中'; // 临时状态
updatedAccount[4] = -1; // -1 表示验证中的临时状态
return updatedAccount;
});
accountStore.setAccounts(accountsWithPendingStatus);
@@ -713,24 +713,13 @@ const handleUploadCookie = (row) => {
formData.append('id', row.id)
formData.append('platform', row.platform)
// 发送上传请求
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
const response = await fetch(`${baseUrl}/uploadCookie`, {
method: 'POST',
body: formData
})
// 使用统一的http封装发送上传请求
const result = await http.upload('/uploadCookie', formData)
const result = await response.json()
if (result.code === 200) {
ElMessage.success('Cookie文件上传成功')
// 刷新账号列表以显示更新
fetchAccounts()
} else {
ElMessage.error(result.msg || 'Cookie文件上传失败')
}
ElMessage.success('Cookie文件上传成功')
// 刷新账号列表以显示更新
fetchAccounts()
} catch (error) {
console.error('上传Cookie文件失败:', error)
ElMessage.error('Cookie文件上传失败')
} finally {
document.body.removeChild(input)
@@ -811,22 +800,17 @@ const connectSSE = (platform, name) => {
// 监听消息
eventSource.onmessage = (event) => {
const data = event.data
console.log('SSE消息:', data)
// 如果还没有二维码数据,且数据长度较长,认为是二维码
if (!qrCodeData.value && data.length > 100) {
try {
// 确保数据是有效的base64编码
// 如果数据已经包含了data:image前缀,直接使用
if (data.startsWith('data:image')) {
qrCodeData.value = data
} else {
// 否则添加前缀
qrCodeData.value = `data:image/png;base64,${data}`
}
console.log('设置二维码数据,长度:', data.length)
} catch (error) {
console.error('处理二维码数据出错:', error)
// 处理二维码数据出错
}
}
// 如果收到状态码
@@ -897,10 +881,10 @@ const submitAccountForm = () => {
try {
// 将平台名称转换为类型数字
const platformTypeMap = {
'快手': 1,
'抖音': 2,
'视频号': 3,
'小红书': 4
'小红书': 1,
'视频号': 2,
'抖音': 3,
'快手': 4
};
const type = platformTypeMap[accountForm.platform] || 1;
+152 -269
View File
@@ -3,11 +3,11 @@
<div class="page-header">
<h1>自媒体自动化运营系统</h1>
</div>
<div class="dashboard-content">
<el-row :gutter="20">
<!-- 账号统计卡片 -->
<el-col :span="6">
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon">
@@ -26,9 +26,9 @@
</div>
</el-card>
</el-col>
<!-- 平台统计卡片 -->
<el-col :span="6">
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon platform-icon">
@@ -36,7 +36,7 @@
</div>
<div class="stat-info">
<div class="stat-value">{{ platformStats.total }}</div>
<div class="stat-label">平台总数</div>
<div class="stat-label">已接入平台</div>
</div>
</div>
<div class="stat-footer">
@@ -57,31 +57,9 @@
</div>
</el-card>
</el-col>
<!-- 任务统计卡片 -->
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon task-icon">
<el-icon><List /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ taskStats.total }}</div>
<div class="stat-label">任务总数</div>
</div>
</div>
<div class="stat-footer">
<div class="stat-detail">
<span>完成: {{ taskStats.completed }}</span>
<span>进行中: {{ taskStats.inProgress }}</span>
<span>失败: {{ taskStats.failed }}</span>
</div>
</div>
</el-card>
</el-col>
<!-- 内容统计卡片 -->
<el-col :span="6">
<!-- 素材统计卡片 -->
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon content-icon">
@@ -89,19 +67,20 @@
</div>
<div class="stat-info">
<div class="stat-value">{{ contentStats.total }}</div>
<div class="stat-label">内容总数</div>
<div class="stat-label">素材总数</div>
</div>
</div>
<div class="stat-footer">
<div class="stat-detail">
<span>已发布: {{ contentStats.published }}</span>
<span>草稿: {{ contentStats.draft }}</span>
<span>视频: {{ contentStats.videos }}</span>
<span>图片: {{ contentStats.images }}</span>
<span>其他: {{ contentStats.others }}</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 快捷操作区域 -->
<div class="quick-actions">
<h2>快捷操作</h2>
@@ -116,199 +95,144 @@
</el-card>
</el-col>
<el-col :span="6">
<el-card class="action-card">
<el-card class="action-card" @click="navigateTo('/material-management')">
<div class="action-icon">
<el-icon><Upload /></el-icon>
</div>
<div class="action-title">内容上传</div>
<div class="action-desc">上传视频和图文内容</div>
<div class="action-title">素材管理</div>
<div class="action-desc">上传和管理视频素材</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="action-card">
<el-card class="action-card" @click="navigateTo('/publish-center')">
<div class="action-icon">
<el-icon><Timer /></el-icon>
</div>
<div class="action-title">定时发布</div>
<div class="action-desc">设置内容发布时间</div>
<div class="action-title">发布中心</div>
<div class="action-desc">发布内容到各平台</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="action-card">
<el-card class="action-card" @click="navigateTo('/about')">
<div class="action-icon">
<el-icon><DataAnalysis /></el-icon>
</div>
<div class="action-title">数据分析</div>
<div class="action-desc">查看内容数据分析</div>
<div class="action-title">关于系统</div>
<div class="action-desc">查看系统信息</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 最近任务列表 -->
<!-- 素材列表 -->
<div class="recent-tasks">
<div class="section-header">
<h2>最近任务</h2>
<el-button text>查看全部</el-button>
<h2>最近上传素材</h2>
<el-button text @click="navigateTo('/material-management')">查看全部</el-button>
</div>
<el-table :data="recentTasks" style="width: 100%">
<el-table-column prop="title" label="任务名称" width="250" />
<el-table-column prop="platform" label="平台" width="120">
<el-table :data="recentMaterials" style="width: 100%" v-loading="loading">
<el-table-column prop="filename" label="文件名" width="300" />
<el-table-column prop="filesize" label="文件大小" width="120">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
{{ scope.row.filesize }} MB
</template>
</el-table-column>
<el-table-column prop="account" label="账号" width="150" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="status" label="状态" width="120">
<el-table-column prop="upload_time" label="上传时间" width="200" />
<el-table-column label="类型" width="100">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
:type="getFileTypeTag(scope.row.filename)"
effect="plain"
size="small"
>
{{ scope.row.status }}
{{ getFileType(scope.row.filename) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="viewTaskDetail(scope.row)">查看</el-button>
<el-button
size="small"
type="primary"
v-if="scope.row.status === '待执行'"
@click="executeTask(scope.row)"
>
执行
</el-button>
<el-button
size="small"
type="danger"
v-if="scope.row.status !== '已完成' && scope.row.status !== '已失败'"
@click="cancelTask(scope.row)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && recentMaterials.length === 0" description="暂无素材数据" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
User, UserFilled, Platform, List, Document,
Upload, Timer, DataAnalysis
import {
User, UserFilled, Platform, Document,
Upload, Timer, DataAnalysis
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { accountApi } from '@/api/account'
import { materialApi } from '@/api/material'
import { useAccountStore } from '@/stores/account'
import { useAppStore } from '@/stores/app'
const router = useRouter()
const accountStore = useAccountStore()
const appStore = useAppStore()
const loading = ref(false)
// 账号统计数据
const accountStats = reactive({
total: 12,
normal: 10,
abnormal: 2
})
// 平台统计数据
const platformStats = reactive({
total: 4,
kuaishou: 3,
douyin: 4,
channels: 2,
xiaohongshu: 3
})
// 任务统计数据
const taskStats = reactive({
total: 24,
completed: 18,
inProgress: 5,
failed: 1
})
// 内容统计数据
const contentStats = reactive({
total: 36,
published: 30,
draft: 6
})
// 最近任务数据
const recentTasks = ref([
{
id: 1,
title: '快手视频自动发布',
platform: '快手',
account: '快手账号1',
createTime: '2024-05-01 10:30:00',
status: '已完成'
},
{
id: 2,
title: '抖音视频定时发布',
platform: '抖音',
account: '抖音账号1',
createTime: '2024-05-01 11:15:00',
status: '进行中'
},
{
id: 3,
title: '视频号内容上传',
platform: '视频号',
account: '视频号账号1',
createTime: '2024-05-01 14:20:00',
status: '待执行'
},
{
id: 4,
title: '小红书图文发布',
platform: '小红书',
account: '小红书账号1',
createTime: '2024-05-01 16:45:00',
status: '已失败'
},
{
id: 5,
title: '快手短视频批量上传',
platform: '快手',
account: '快手账号2',
createTime: '2024-05-02 09:10:00',
status: '待执行'
// 账号统计数据 - 从真实数据计算
const accountStats = computed(() => {
const accounts = accountStore.accounts
const normal = accounts.filter(a => a.status === '正常').length
const abnormal = accounts.filter(a => a.status !== '正常' && a.status !== '验证中').length
return {
total: accounts.length,
normal,
abnormal
}
])
})
// 根据平台获取标签类型
const getPlatformTagType = (platform) => {
const typeMap = {
'快手': 'success',
'抖音': 'danger',
'视频号': 'warning',
'小红书': 'info'
// 平台统计数据 - 从真实数据计算
const platformStats = computed(() => {
const accounts = accountStore.accounts
const kuaishou = accounts.filter(a => a.platform === '快手').length
const douyin = accounts.filter(a => a.platform === '抖音').length
const channels = accounts.filter(a => a.platform === '视频号').length
const xiaohongshu = accounts.filter(a => a.platform === '小红书').length
// 统计有账号的平台数量
const total = [kuaishou, douyin, channels, xiaohongshu].filter(n => n > 0).length
return { total, kuaishou, douyin, channels, xiaohongshu }
})
// 素材统计数据 - 从真实数据计算
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const contentStats = computed(() => {
const materials = appStore.materials
const videos = materials.filter(m => videoExtensions.some(ext => m.filename.toLowerCase().endsWith(ext))).length
const images = materials.filter(m => imageExtensions.some(ext => m.filename.toLowerCase().endsWith(ext))).length
return {
total: materials.length,
videos,
images,
others: materials.length - videos - images
}
return typeMap[platform] || 'info'
})
// 最近上传的素材(最多显示5条)
const recentMaterials = computed(() => {
return [...appStore.materials]
.sort((a, b) => new Date(b.upload_time) - new Date(a.upload_time))
.slice(0, 5)
})
// 获取文件类型
const getFileType = (filename) => {
if (videoExtensions.some(ext => filename.toLowerCase().endsWith(ext))) return '视频'
if (imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))) return '图片'
return '其他'
}
// 根据状态获取标签类型
const getStatusTagType = (status) => {
const typeMap = {
'已完成': 'success',
'进行中': 'warning',
'待执行': 'info',
'已失败': 'danger'
}
return typeMap[status] || 'info'
// 获取文件类型标签颜色
const getFileTypeTag = (filename) => {
const type = getFileType(filename)
return { '视频': 'success', '图片': 'warning', '其他': 'info' }[type] || 'info'
}
// 导航到指定路由
@@ -316,65 +240,32 @@ const navigateTo = (path) => {
router.push(path)
}
// 查看任务详情
const viewTaskDetail = (task) => {
ElMessage.info(`查看任务: ${task.title}`)
// 实际应用中应该跳转到任务详情页面
// 加载数据
const fetchDashboardData = async () => {
loading.value = true
try {
// 并行获取账号和素材数据
const [accountRes, materialRes] = await Promise.allSettled([
accountApi.getAccounts(),
materialApi.getAllMaterials()
])
if (accountRes.status === 'fulfilled' && accountRes.value.code === 200) {
accountStore.setAccounts(accountRes.value.data)
}
if (materialRes.status === 'fulfilled' && materialRes.value.code === 200) {
appStore.setMaterials(materialRes.value.data)
}
} catch (error) {
console.error('获取仪表盘数据失败:', error)
} finally {
loading.value = false
}
}
// 执行任务
const executeTask = (task) => {
ElMessageBox.confirm(
`确定要执行任务 ${task.title} 吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
)
.then(() => {
// 更新任务状态
const index = recentTasks.value.findIndex(t => t.id === task.id)
if (index !== -1) {
recentTasks.value[index].status = '进行中'
}
ElMessage({
type: 'success',
message: '任务已开始执行',
})
})
.catch(() => {
// 取消执行
})
}
// 取消任务
const cancelTask = (task) => {
ElMessageBox.confirm(
`确定要取消任务 ${task.title} 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
// 更新任务状态
const index = recentTasks.value.findIndex(t => t.id === task.id)
if (index !== -1) {
recentTasks.value[index].status = '已取消'
}
ElMessage({
type: 'success',
message: '任务已取消',
})
})
.catch(() => {
// 取消操作
})
}
onMounted(() => {
fetchDashboardData()
})
</script>
<style lang="scss" scoped>
@@ -383,24 +274,24 @@ const cancelTask = (task) => {
.dashboard {
.page-header {
margin-bottom: 20px;
h1 {
font-size: 24px;
color: $text-primary;
margin: 0;
}
}
.dashboard-content {
.stat-card {
height: 140px;
margin-bottom: 20px;
.stat-card-content {
display: flex;
align-items: center;
margin-bottom: 15px;
.stat-icon {
width: 60px;
height: 60px;
@@ -410,37 +301,29 @@ const cancelTask = (task) => {
justify-content: center;
align-items: center;
margin-right: 15px;
.el-icon {
font-size: 30px;
color: $primary-color;
}
&.platform-icon {
background-color: rgba($success-color, 0.1);
.el-icon {
color: $success-color;
}
}
&.task-icon {
background-color: rgba($warning-color, 0.1);
.el-icon {
color: $warning-color;
}
}
&.content-icon {
background-color: rgba($info-color, 0.1);
.el-icon {
color: $info-color;
}
}
}
.stat-info {
.stat-value {
font-size: 24px;
@@ -448,40 +331,40 @@ const cancelTask = (task) => {
color: $text-primary;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: $text-secondary;
}
}
}
.stat-footer {
border-top: 1px solid $border-lighter;
padding-top: 10px;
.stat-detail {
display: flex;
justify-content: space-between;
color: $text-secondary;
font-size: 13px;
.el-tag {
margin-right: 5px;
}
}
}
}
.quick-actions {
margin: 20px 0 30px;
h2 {
font-size: 18px;
margin-bottom: 15px;
color: $text-primary;
}
.action-card {
height: 160px;
display: flex;
@@ -490,12 +373,12 @@ const cancelTask = (task) => {
justify-content: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.action-icon {
width: 50px;
height: 50px;
@@ -505,20 +388,20 @@ const cancelTask = (task) => {
justify-content: center;
align-items: center;
margin-bottom: 15px;
.el-icon {
font-size: 24px;
color: $primary-color;
}
}
.action-title {
font-size: 16px;
font-weight: bold;
color: $text-primary;
margin-bottom: 5px;
}
.action-desc {
font-size: 13px;
color: $text-secondary;
@@ -526,16 +409,16 @@ const cancelTask = (task) => {
}
}
}
.recent-tasks {
margin-top: 30px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
h2 {
font-size: 18px;
color: $text-primary;
@@ -545,4 +428,4 @@ const cancelTask = (task) => {
}
}
}
</style>
</style>
-106
View File
@@ -1,106 +0,0 @@
<template>
<div class="home">
<div class="welcome-section">
<h1>欢迎使用 Vue3 + Vite 项目</h1>
<p>这是一个集成了 Vue3ViteElement PlusPiniaVue Router Axios 的现代化前端项目</p>
<div class="features">
<el-row :gutter="20">
<el-col :span="8">
<el-card class="feature-card">
<template #header>
<div class="card-header">
<el-icon><Lightning /></el-icon>
<span>快速开发</span>
</div>
</template>
<p>基于 Vite 构建提供极速的开发体验</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="feature-card">
<template #header>
<div class="card-header">
<el-icon><Star /></el-icon>
<span>现代化</span>
</div>
</template>
<p>使用 Vue3 Composition API setup 语法</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="feature-card">
<template #header>
<div class="card-header">
<el-icon><Setting /></el-icon>
<span>完整配置</span>
</div>
</template>
<p>集成路由状态管理HTTP请求等完整解决方案</p>
</el-card>
</el-col>
</el-row>
</div>
</div>
<div class="demo-section">
<HelloWorld msg="Vue3 + Vite + Element Plus" />
</div>
</div>
</template>
<script setup>
import HelloWorld from '../components/HelloWorld.vue'
import { Lightning, Star, Setting } from '@element-plus/icons-vue'
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
.home {
.welcome-section {
text-align: center;
margin-bottom: 3rem;
h1 {
color: #2c3e50;
margin-bottom: 1rem;
font-size: 2.5rem;
}
p {
color: #7f8c8d;
font-size: 1.2rem;
margin-bottom: 2rem;
}
.features {
margin-top: 2rem;
.feature-card {
height: 200px;
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
.el-icon {
color: #409eff;
}
}
p {
color: #666;
font-size: 1rem;
line-height: 1.6;
}
}
}
}
.demo-section {
margin-top: 3rem;
}
}
</style>
+86 -114
View File
@@ -296,6 +296,15 @@
</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
@@ -411,27 +420,6 @@
</template>
</el-dialog>
<!-- 标签 (仅在抖音可见) -->
<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="schedule-section">
<h3>定时发布</h3>
@@ -509,6 +497,7 @@ 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'
@@ -565,7 +554,8 @@ const defaultTabInit = {
startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
publishStatus: null, // 发布状态,包含message和type
publishing: false, // 发布状态,用于控制按钮loading效果
isDraft: false // 是否保存为草稿,仅视频号平台可见
isDraft: false, // 是否保存为草稿,仅视频号平台可见
isOriginal: false // 是否标记为原创
}
// helper to create a fresh deep-copied tab from defaultTabInit
@@ -663,7 +653,6 @@ const handleUploadSuccess = (response, file, tab) => {
}))]
ElMessage.success('文件上传成功')
console.log('上传成功:', fileInfo)
} else {
ElMessage.error(response.msg || '上传失败')
}
@@ -672,7 +661,6 @@ const handleUploadSuccess = (response, file, tab) => {
// 处理文件上传失败
const handleUploadError = (error) => {
ElMessage.error('文件上传失败')
console.error('上传错误:', error)
}
// 删除已上传文件
@@ -774,102 +762,77 @@ const cancelPublish = (tab) => {
const confirmPublish = async (tab) => {
// 防止重复点击
if (tab.publishing) {
return Promise.reject(new Error('正在发布中,请稍候...'))
throw new Error('正在发布中,请稍候...')
}
tab.publishing = true // 设置发布状态为进行中
return new Promise((resolve, reject) => {
// 数据验证
if (tab.fileList.length === 0) {
ElMessage.error('请先上传视频文件')
tab.publishing = false // 重置发布状态
reject(new Error('请先上传视频文件'))
return
}
if (!tab.title.trim()) {
ElMessage.error('请输入标题')
tab.publishing = false // 重置发布状态
reject(new Error('请输入标题'))
return
}
if (!tab.selectedPlatform) {
ElMessage.error('请选择发布平台')
tab.publishing = false // 重置发布状态
reject(new Error('请选择发布平台'))
return
}
if (tab.selectedAccounts.length === 0) {
ElMessage.error('请选择发布账号')
tab.publishing = false // 重置发布状态
reject(new Error('请选择发布账号'))
return
}
// 数据验证
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, // 是否启用定时发布,开启传1,不开启传0
videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1, // 每天发布视频数量,1-55
dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'], // 每天发布时间点
startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
category: 0, //表示非原创
productLink: tab.productLink.trim() || '', // 商品链接
productTitle: tab.productTitle.trim() || '', // 商品名称
isDraft: tab.isDraft // 是否保存为草稿,仅视频号平台使用
}
// 构造发布数据,符合后端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
fetch(`${apiBaseUrl}/postVideo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authHeaders.value
},
body: JSON.stringify(publishData)
})
.then(response => response.json())
.then(data => {
if (data.code === 200) {
tab.publishStatus = {
message: '发布成功',
type: 'success'
}
// 清空当前tab的数据
tab.fileList = []
tab.displayFileList = []
tab.title = ''
tab.selectedTopics = []
tab.selectedAccounts = []
tab.scheduleEnabled = false
resolve()
} else {
tab.publishStatus = {
message: `发布失败:${data.msg || '发布失败'}`,
type: 'error'
}
reject(new Error(data.msg || '发布失败'))
}
})
.catch(error => {
console.error('发布错误:', error)
tab.publishStatus = {
message: '发布失败,请检查网络连接',
type: 'error'
}
reject(error)
})
.finally(() => {
tab.publishing = false // 重置发布状态
})
})
// 调用后端发布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
}
}
// 显示上传选项
@@ -1324,6 +1287,15 @@ const batchPublish = async () => {
margin: 10px 0;
}
}
.original-section {
margin: 10px 0 20px;
.original-checkbox {
display: block;
margin: 10px 0;
}
}
}
}
}