Merge pull request #172 from wjj9868/feat/system-optimization
Browse files- myUtils/login.py +19 -4
- sau_backend.py +80 -33
- sau_frontend/src/App.vue +4 -8
- sau_frontend/src/api/user.js +3 -47
- sau_frontend/src/components/helloworld.vue +0 -66
- sau_frontend/src/stores/account.js +2 -3
- sau_frontend/src/utils/request.js +2 -2
- sau_frontend/src/views/About.vue +99 -13
- sau_frontend/src/views/AccountManagement.vue +12 -28
- sau_frontend/src/views/Dashboard.vue +145 -262
- sau_frontend/src/views/Home.vue +0 -106
- sau_frontend/src/views/PublishCenter.vue +86 -114
- utils/browser_hook.py +17 -0
myUtils/login.py
CHANGED
|
@@ -7,7 +7,24 @@ from myUtils.auth import check_cookie
|
|
| 7 |
from utils.base_social_media import set_init_script
|
| 8 |
import uuid
|
| 9 |
from pathlib import Path
|
| 10 |
-
from conf import BASE_DIR, LOCAL_CHROME_HEADLESS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# 抖音登录
|
| 13 |
async def douyin_cookie_gen(id,status_queue):
|
|
@@ -17,9 +34,7 @@ async def douyin_cookie_gen(id,status_queue):
|
|
| 17 |
if page.url != original_url:
|
| 18 |
url_changed_event.set()
|
| 19 |
async with async_playwright() as playwright:
|
| 20 |
-
options =
|
| 21 |
-
'headless': LOCAL_CHROME_HEADLESS
|
| 22 |
-
}
|
| 23 |
# Make sure to run headed.
|
| 24 |
browser = await playwright.chromium.launch(**options)
|
| 25 |
# Setup context however you like.
|
|
|
|
| 7 |
from utils.base_social_media import set_init_script
|
| 8 |
import uuid
|
| 9 |
from pathlib import Path
|
| 10 |
+
from conf import BASE_DIR, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH
|
| 11 |
+
|
| 12 |
+
# 统一获取浏览器启动配置(防风控+引入本地浏览器)
|
| 13 |
+
def get_browser_options():
|
| 14 |
+
options = {
|
| 15 |
+
'headless': LOCAL_CHROME_HEADLESS,
|
| 16 |
+
'args': [
|
| 17 |
+
'--disable-blink-features=AutomationControlled', # 核心防爬屏蔽:去掉 window.navigator.webdriver 标签
|
| 18 |
+
'--lang=zh-CN',
|
| 19 |
+
'--disable-infobars',
|
| 20 |
+
'--start-maximized'
|
| 21 |
+
]
|
| 22 |
+
}
|
| 23 |
+
# 如果用户在 conf.py 里配置了本地 Chrome,就用本地的,这样成功率极高
|
| 24 |
+
if LOCAL_CHROME_PATH:
|
| 25 |
+
options['executable_path'] = LOCAL_CHROME_PATH
|
| 26 |
+
|
| 27 |
+
return options
|
| 28 |
|
| 29 |
# 抖音登录
|
| 30 |
async def douyin_cookie_gen(id,status_queue):
|
|
|
|
| 34 |
if page.url != original_url:
|
| 35 |
url_changed_event.set()
|
| 36 |
async with async_playwright() as playwright:
|
| 37 |
+
options = get_browser_options()
|
|
|
|
|
|
|
| 38 |
# Make sure to run headed.
|
| 39 |
browser = await playwright.chromium.launch(**options)
|
| 40 |
# Setup context however you like.
|
sau_backend.py
CHANGED
|
@@ -48,14 +48,14 @@ def index(): # put application's code here
|
|
| 48 |
def upload_file():
|
| 49 |
if 'file' not in request.files:
|
| 50 |
return jsonify({
|
| 51 |
-
"code":
|
| 52 |
"data": None,
|
| 53 |
"msg": "No file part in the request"
|
| 54 |
}), 400
|
| 55 |
file = request.files['file']
|
| 56 |
if file.filename == '':
|
| 57 |
return jsonify({
|
| 58 |
-
"code":
|
| 59 |
"data": None,
|
| 60 |
"msg": "No selected file"
|
| 61 |
}), 400
|
|
@@ -67,7 +67,7 @@ def upload_file():
|
|
| 67 |
file.save(filepath)
|
| 68 |
return jsonify({"code":200,"msg": "File uploaded successfully", "data": f"{uuid_v1}_{file.filename}"}), 200
|
| 69 |
except Exception as e:
|
| 70 |
-
return jsonify({"code":
|
| 71 |
|
| 72 |
@app.route('/getFile', methods=['GET'])
|
| 73 |
def get_file():
|
|
@@ -75,11 +75,11 @@ def get_file():
|
|
| 75 |
filename = request.args.get('filename')
|
| 76 |
|
| 77 |
if not filename:
|
| 78 |
-
return {"
|
| 79 |
|
| 80 |
# 防止路径穿越攻击
|
| 81 |
if '..' in filename or filename.startswith('/'):
|
| 82 |
-
return {"
|
| 83 |
|
| 84 |
# 拼接完整路径
|
| 85 |
file_path = str(Path(BASE_DIR / "videoFile"))
|
|
@@ -316,7 +316,16 @@ def delete_file():
|
|
| 316 |
|
| 317 |
@app.route('/deleteAccount', methods=['GET'])
|
| 318 |
def delete_account():
|
| 319 |
-
account_id =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
try:
|
| 322 |
# 获取数据库连接
|
|
@@ -337,6 +346,16 @@ def delete_account():
|
|
| 337 |
|
| 338 |
record = dict(record)
|
| 339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
# 删除数据库记录
|
| 341 |
cursor.execute("DELETE FROM user_info WHERE id = ?", (account_id,))
|
| 342 |
conn.commit()
|
|
@@ -350,7 +369,7 @@ def delete_account():
|
|
| 350 |
except Exception as e:
|
| 351 |
return jsonify({
|
| 352 |
"code": 500,
|
| 353 |
-
"msg":
|
| 354 |
"data": None
|
| 355 |
}), 500
|
| 356 |
|
|
@@ -385,6 +404,9 @@ def postVideo():
|
|
| 385 |
# 获取JSON数据
|
| 386 |
data = request.get_json()
|
| 387 |
|
|
|
|
|
|
|
|
|
|
| 388 |
# 从JSON数据中提取fileList和accountList
|
| 389 |
file_list = data.get('fileList', [])
|
| 390 |
account_list = data.get('accountList', [])
|
|
@@ -403,29 +425,52 @@ def postVideo():
|
|
| 403 |
videos_per_day = data.get('videosPerDay')
|
| 404 |
daily_times = data.get('dailyTimes')
|
| 405 |
start_days = data.get('startDays')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
# 打印获取到的数据(仅作为示例)
|
| 407 |
print("File List:", file_list)
|
| 408 |
print("Account List:", account_list)
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
"data": None
|
| 428 |
-
}),
|
| 429 |
|
| 430 |
|
| 431 |
@app.route('/updateUserinfo', methods=['POST'])
|
|
@@ -470,7 +515,7 @@ def postVideoBatch():
|
|
| 470 |
data_list = request.get_json()
|
| 471 |
|
| 472 |
if not isinstance(data_list, list):
|
| 473 |
-
return jsonify({"
|
| 474 |
for data in data_list:
|
| 475 |
# 从JSON数据中提取fileList和accountList
|
| 476 |
file_list = data.get('fileList', [])
|
|
@@ -484,6 +529,7 @@ def postVideoBatch():
|
|
| 484 |
category = None
|
| 485 |
productLink = data.get('productLink', '')
|
| 486 |
productTitle = data.get('productTitle', '')
|
|
|
|
| 487 |
|
| 488 |
videos_per_day = data.get('videosPerDay')
|
| 489 |
daily_times = data.get('dailyTimes')
|
|
@@ -493,10 +539,11 @@ def postVideoBatch():
|
|
| 493 |
print("Account List:", account_list)
|
| 494 |
match type:
|
| 495 |
case 1:
|
| 496 |
-
|
|
|
|
| 497 |
case 2:
|
| 498 |
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 499 |
-
start_days)
|
| 500 |
case 3:
|
| 501 |
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 502 |
start_days, productLink, productTitle)
|
|
@@ -517,7 +564,7 @@ def upload_cookie():
|
|
| 517 |
try:
|
| 518 |
if 'file' not in request.files:
|
| 519 |
return jsonify({
|
| 520 |
-
"code":
|
| 521 |
"msg": "没有找到Cookie文件",
|
| 522 |
"data": None
|
| 523 |
}), 400
|
|
@@ -525,14 +572,14 @@ def upload_cookie():
|
|
| 525 |
file = request.files['file']
|
| 526 |
if file.filename == '':
|
| 527 |
return jsonify({
|
| 528 |
-
"code":
|
| 529 |
"msg": "Cookie文件名不能为空",
|
| 530 |
"data": None
|
| 531 |
}), 400
|
| 532 |
|
| 533 |
if not file.filename.endswith('.json'):
|
| 534 |
return jsonify({
|
| 535 |
-
"code":
|
| 536 |
"msg": "Cookie文件必须是JSON格式",
|
| 537 |
"data": None
|
| 538 |
}), 400
|
|
@@ -543,7 +590,7 @@ def upload_cookie():
|
|
| 543 |
|
| 544 |
if not account_id or not platform:
|
| 545 |
return jsonify({
|
| 546 |
-
"code":
|
| 547 |
"msg": "缺少账号ID或平台信息",
|
| 548 |
"data": None
|
| 549 |
}), 400
|
|
|
|
| 48 |
def upload_file():
|
| 49 |
if 'file' not in request.files:
|
| 50 |
return jsonify({
|
| 51 |
+
"code": 400,
|
| 52 |
"data": None,
|
| 53 |
"msg": "No file part in the request"
|
| 54 |
}), 400
|
| 55 |
file = request.files['file']
|
| 56 |
if file.filename == '':
|
| 57 |
return jsonify({
|
| 58 |
+
"code": 400,
|
| 59 |
"data": None,
|
| 60 |
"msg": "No selected file"
|
| 61 |
}), 400
|
|
|
|
| 67 |
file.save(filepath)
|
| 68 |
return jsonify({"code":200,"msg": "File uploaded successfully", "data": f"{uuid_v1}_{file.filename}"}), 200
|
| 69 |
except Exception as e:
|
| 70 |
+
return jsonify({"code":500,"msg": str(e),"data":None}), 500
|
| 71 |
|
| 72 |
@app.route('/getFile', methods=['GET'])
|
| 73 |
def get_file():
|
|
|
|
| 75 |
filename = request.args.get('filename')
|
| 76 |
|
| 77 |
if not filename:
|
| 78 |
+
return jsonify({"code": 400, "msg": "filename is required", "data": None}), 400
|
| 79 |
|
| 80 |
# 防止路径穿越攻击
|
| 81 |
if '..' in filename or filename.startswith('/'):
|
| 82 |
+
return jsonify({"code": 400, "msg": "Invalid filename", "data": None}), 400
|
| 83 |
|
| 84 |
# 拼接完整路径
|
| 85 |
file_path = str(Path(BASE_DIR / "videoFile"))
|
|
|
|
| 316 |
|
| 317 |
@app.route('/deleteAccount', methods=['GET'])
|
| 318 |
def delete_account():
|
| 319 |
+
account_id = request.args.get('id')
|
| 320 |
+
|
| 321 |
+
if not account_id or not account_id.isdigit():
|
| 322 |
+
return jsonify({
|
| 323 |
+
"code": 400,
|
| 324 |
+
"msg": "Invalid or missing account ID",
|
| 325 |
+
"data": None
|
| 326 |
+
}), 400
|
| 327 |
+
|
| 328 |
+
account_id = int(account_id)
|
| 329 |
|
| 330 |
try:
|
| 331 |
# 获取数据库连接
|
|
|
|
| 346 |
|
| 347 |
record = dict(record)
|
| 348 |
|
| 349 |
+
# 删除关联的cookie文件
|
| 350 |
+
if record.get('filePath'):
|
| 351 |
+
cookie_file_path = Path(BASE_DIR / "cookiesFile" / record['filePath'])
|
| 352 |
+
if cookie_file_path.exists():
|
| 353 |
+
try:
|
| 354 |
+
cookie_file_path.unlink()
|
| 355 |
+
print(f"✅ Cookie文件已删除: {cookie_file_path}")
|
| 356 |
+
except Exception as e:
|
| 357 |
+
print(f"⚠️ 删除Cookie文件失败: {e}")
|
| 358 |
+
|
| 359 |
# 删除数据库记录
|
| 360 |
cursor.execute("DELETE FROM user_info WHERE id = ?", (account_id,))
|
| 361 |
conn.commit()
|
|
|
|
| 369 |
except Exception as e:
|
| 370 |
return jsonify({
|
| 371 |
"code": 500,
|
| 372 |
+
"msg": f"delete failed: {str(e)}",
|
| 373 |
"data": None
|
| 374 |
}), 500
|
| 375 |
|
|
|
|
| 404 |
# 获取JSON数据
|
| 405 |
data = request.get_json()
|
| 406 |
|
| 407 |
+
if not data:
|
| 408 |
+
return jsonify({"code": 400, "msg": "请求数据不能为空", "data": None}), 400
|
| 409 |
+
|
| 410 |
# 从JSON数据中提取fileList和accountList
|
| 411 |
file_list = data.get('fileList', [])
|
| 412 |
account_list = data.get('accountList', [])
|
|
|
|
| 425 |
videos_per_day = data.get('videosPerDay')
|
| 426 |
daily_times = data.get('dailyTimes')
|
| 427 |
start_days = data.get('startDays')
|
| 428 |
+
|
| 429 |
+
# 参数校验
|
| 430 |
+
if not file_list:
|
| 431 |
+
return jsonify({"code": 400, "msg": "文件列表不能为空", "data": None}), 400
|
| 432 |
+
if not account_list:
|
| 433 |
+
return jsonify({"code": 400, "msg": "账号列表不能为空", "data": None}), 400
|
| 434 |
+
if not type:
|
| 435 |
+
return jsonify({"code": 400, "msg": "平台类型不能为空", "data": None}), 400
|
| 436 |
+
if not title:
|
| 437 |
+
return jsonify({"code": 400, "msg": "标题不能为空", "data": None}), 400
|
| 438 |
+
|
| 439 |
# 打印获取到的数据(仅作为示例)
|
| 440 |
print("File List:", file_list)
|
| 441 |
print("Account List:", account_list)
|
| 442 |
+
|
| 443 |
+
try:
|
| 444 |
+
match type:
|
| 445 |
+
case 1:
|
| 446 |
+
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 447 |
+
start_days)
|
| 448 |
+
case 2:
|
| 449 |
+
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 450 |
+
start_days, is_draft)
|
| 451 |
+
case 3:
|
| 452 |
+
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 453 |
+
start_days, thumbnail_path, productLink, productTitle)
|
| 454 |
+
case 4:
|
| 455 |
+
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 456 |
+
start_days)
|
| 457 |
+
case _:
|
| 458 |
+
return jsonify({"code": 400, "msg": f"不支持的平台类型: {type}", "data": None}), 400
|
| 459 |
+
|
| 460 |
+
# 返回响应给客户端
|
| 461 |
+
return jsonify(
|
| 462 |
+
{
|
| 463 |
+
"code": 200,
|
| 464 |
+
"msg": "发布任务已提交",
|
| 465 |
+
"data": None
|
| 466 |
+
}), 200
|
| 467 |
+
except Exception as e:
|
| 468 |
+
print(f"发布视频时出错: {str(e)}")
|
| 469 |
+
return jsonify({
|
| 470 |
+
"code": 500,
|
| 471 |
+
"msg": f"发布失败: {str(e)}",
|
| 472 |
"data": None
|
| 473 |
+
}), 500
|
| 474 |
|
| 475 |
|
| 476 |
@app.route('/updateUserinfo', methods=['POST'])
|
|
|
|
| 515 |
data_list = request.get_json()
|
| 516 |
|
| 517 |
if not isinstance(data_list, list):
|
| 518 |
+
return jsonify({"code": 400, "msg": "Expected a JSON array", "data": None}), 400
|
| 519 |
for data in data_list:
|
| 520 |
# 从JSON数据中提取fileList和accountList
|
| 521 |
file_list = data.get('fileList', [])
|
|
|
|
| 529 |
category = None
|
| 530 |
productLink = data.get('productLink', '')
|
| 531 |
productTitle = data.get('productTitle', '')
|
| 532 |
+
is_draft = data.get('isDraft', False)
|
| 533 |
|
| 534 |
videos_per_day = data.get('videosPerDay')
|
| 535 |
daily_times = data.get('dailyTimes')
|
|
|
|
| 539 |
print("Account List:", account_list)
|
| 540 |
match type:
|
| 541 |
case 1:
|
| 542 |
+
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 543 |
+
start_days)
|
| 544 |
case 2:
|
| 545 |
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 546 |
+
start_days, is_draft)
|
| 547 |
case 3:
|
| 548 |
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
|
| 549 |
start_days, productLink, productTitle)
|
|
|
|
| 564 |
try:
|
| 565 |
if 'file' not in request.files:
|
| 566 |
return jsonify({
|
| 567 |
+
"code": 400,
|
| 568 |
"msg": "没有找到Cookie文件",
|
| 569 |
"data": None
|
| 570 |
}), 400
|
|
|
|
| 572 |
file = request.files['file']
|
| 573 |
if file.filename == '':
|
| 574 |
return jsonify({
|
| 575 |
+
"code": 400,
|
| 576 |
"msg": "Cookie文件名不能为空",
|
| 577 |
"data": None
|
| 578 |
}), 400
|
| 579 |
|
| 580 |
if not file.filename.endswith('.json'):
|
| 581 |
return jsonify({
|
| 582 |
+
"code": 400,
|
| 583 |
"msg": "Cookie文件必须是JSON格式",
|
| 584 |
"data": None
|
| 585 |
}), 400
|
|
|
|
| 590 |
|
| 591 |
if not account_id or not platform:
|
| 592 |
return jsonify({
|
| 593 |
+
"code": 400,
|
| 594 |
"msg": "缺少账号ID或平台信息",
|
| 595 |
"data": None
|
| 596 |
}), 400
|
sau_frontend/src/App.vue
CHANGED
|
@@ -32,13 +32,9 @@
|
|
| 32 |
<el-icon><Upload /></el-icon>
|
| 33 |
<span>发布中心</span>
|
| 34 |
</el-menu-item>
|
| 35 |
-
<el-menu-item index="/
|
| 36 |
-
<el-icon><Monitor /></el-icon>
|
| 37 |
-
<span>网站</span>
|
| 38 |
-
</el-menu-item>
|
| 39 |
-
<el-menu-item index="/data">
|
| 40 |
<el-icon><DataAnalysis /></el-icon>
|
| 41 |
-
<span>
|
| 42 |
</el-menu-item>
|
| 43 |
</el-menu>
|
| 44 |
</div>
|
|
@@ -65,8 +61,8 @@
|
|
| 65 |
<script setup>
|
| 66 |
import { ref, computed } from 'vue'
|
| 67 |
import { useRoute } from 'vue-router'
|
| 68 |
-
import {
|
| 69 |
-
HomeFilled, User,
|
| 70 |
Fold, Picture, Upload
|
| 71 |
} from '@element-plus/icons-vue'
|
| 72 |
|
|
|
|
| 32 |
<el-icon><Upload /></el-icon>
|
| 33 |
<span>发布中心</span>
|
| 34 |
</el-menu-item>
|
| 35 |
+
<el-menu-item index="/about">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
<el-icon><DataAnalysis /></el-icon>
|
| 37 |
+
<span>关于</span>
|
| 38 |
</el-menu-item>
|
| 39 |
</el-menu>
|
| 40 |
</div>
|
|
|
|
| 61 |
<script setup>
|
| 62 |
import { ref, computed } from 'vue'
|
| 63 |
import { useRoute } from 'vue-router'
|
| 64 |
+
import {
|
| 65 |
+
HomeFilled, User, DataAnalysis,
|
| 66 |
Fold, Picture, Upload
|
| 67 |
} from '@element-plus/icons-vue'
|
| 68 |
|
sau_frontend/src/api/user.js
CHANGED
|
@@ -1,49 +1,5 @@
|
|
| 1 |
import { http } from '@/utils/request'
|
| 2 |
|
| 3 |
-
// 用户相关API
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
getUserInfo(id) {
|
| 7 |
-
return http.get(`/user/${id}`)
|
| 8 |
-
},
|
| 9 |
-
|
| 10 |
-
// 获取用户列表
|
| 11 |
-
getUserList(params) {
|
| 12 |
-
return http.get('/user/list', params)
|
| 13 |
-
},
|
| 14 |
-
|
| 15 |
-
// 创建用户
|
| 16 |
-
createUser(data) {
|
| 17 |
-
return http.post('/user', data)
|
| 18 |
-
},
|
| 19 |
-
|
| 20 |
-
// 更新用户信息
|
| 21 |
-
updateUser(id, data) {
|
| 22 |
-
return http.put(`/user/${id}`, data)
|
| 23 |
-
},
|
| 24 |
-
|
| 25 |
-
// 删除用户
|
| 26 |
-
deleteUser(id) {
|
| 27 |
-
return http.delete(`/user/${id}`)
|
| 28 |
-
},
|
| 29 |
-
|
| 30 |
-
// 用户登录
|
| 31 |
-
login(data) {
|
| 32 |
-
return http.post('/auth/login', data)
|
| 33 |
-
},
|
| 34 |
-
|
| 35 |
-
// 用户注册
|
| 36 |
-
register(data) {
|
| 37 |
-
return http.post('/auth/register', data)
|
| 38 |
-
},
|
| 39 |
-
|
| 40 |
-
// 用户登出
|
| 41 |
-
logout() {
|
| 42 |
-
return http.post('/auth/logout')
|
| 43 |
-
},
|
| 44 |
-
|
| 45 |
-
// 刷新token
|
| 46 |
-
refreshToken() {
|
| 47 |
-
return http.post('/auth/refresh')
|
| 48 |
-
}
|
| 49 |
-
}
|
|
|
|
| 1 |
import { http } from '@/utils/request'
|
| 2 |
|
| 3 |
+
// 用户相关API(预留)
|
| 4 |
+
// 注意:当前后端暂无用户认证接口,以下为预留定义
|
| 5 |
+
export const userApi = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sau_frontend/src/components/helloworld.vue
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
<template>
|
| 2 |
-
<div class="hello-world">
|
| 3 |
-
<h1>{{ msg }}</h1>
|
| 4 |
-
<div class="card">
|
| 5 |
-
<el-button type="primary" @click="count++">
|
| 6 |
-
点击次数: {{ count }}
|
| 7 |
-
</el-button>
|
| 8 |
-
<p class="mt-3">
|
| 9 |
-
这是一个使用 Element Plus 的示例组件
|
| 10 |
-
</p>
|
| 11 |
-
</div>
|
| 12 |
-
<div class="links mt-4">
|
| 13 |
-
<el-link href="https://vuejs.org/" target="_blank" type="primary">
|
| 14 |
-
Vue.js 官网
|
| 15 |
-
</el-link>
|
| 16 |
-
<el-link href="https://element-plus.org/" target="_blank" type="success">
|
| 17 |
-
Element Plus 官网
|
| 18 |
-
</el-link>
|
| 19 |
-
</div>
|
| 20 |
-
</div>
|
| 21 |
-
</template>
|
| 22 |
-
|
| 23 |
-
<script setup>
|
| 24 |
-
import { ref } from 'vue'
|
| 25 |
-
|
| 26 |
-
defineProps({
|
| 27 |
-
msg: {
|
| 28 |
-
type: String,
|
| 29 |
-
default: 'Hello Vue 3 + Vite'
|
| 30 |
-
}
|
| 31 |
-
})
|
| 32 |
-
|
| 33 |
-
const count = ref(0)
|
| 34 |
-
</script>
|
| 35 |
-
|
| 36 |
-
<style lang="scss" scoped>
|
| 37 |
-
@use '@/styles/variables.scss' as *;
|
| 38 |
-
|
| 39 |
-
.hello-world {
|
| 40 |
-
text-align: center;
|
| 41 |
-
padding: 2rem;
|
| 42 |
-
|
| 43 |
-
h1 {
|
| 44 |
-
color: #2c3e50;
|
| 45 |
-
margin-bottom: 2rem;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
.card {
|
| 49 |
-
background: white;
|
| 50 |
-
padding: 2rem;
|
| 51 |
-
border-radius: 8px;
|
| 52 |
-
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
| 53 |
-
margin-bottom: 2rem;
|
| 54 |
-
|
| 55 |
-
p {
|
| 56 |
-
color: #666;
|
| 57 |
-
}
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
.links {
|
| 61 |
-
display: flex;
|
| 62 |
-
justify-content: center;
|
| 63 |
-
gap: 1rem;
|
| 64 |
-
}
|
| 65 |
-
}
|
| 66 |
-
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sau_frontend/src/stores/account.js
CHANGED
|
@@ -22,9 +22,8 @@ export const useAccountStore = defineStore('account', () => {
|
|
| 22 |
type: item[1],
|
| 23 |
filePath: item[2],
|
| 24 |
name: item[3],
|
| 25 |
-
status: item[4] === 1 ? '正常' : '异常',
|
| 26 |
-
platform: platformTypes[item[1]] || '未知'
|
| 27 |
-
avatar: '/vite.svg' // 默认使用vite.svg作为头像
|
| 28 |
}
|
| 29 |
})
|
| 30 |
}
|
|
|
|
| 22 |
type: item[1],
|
| 23 |
filePath: item[2],
|
| 24 |
name: item[3],
|
| 25 |
+
status: item[4] === -1 ? '验证中' : (item[4] === 1 ? '正常' : '异常'),
|
| 26 |
+
platform: platformTypes[item[1]] || '未知'
|
|
|
|
| 27 |
}
|
| 28 |
})
|
| 29 |
}
|
sau_frontend/src/utils/request.js
CHANGED
|
@@ -34,8 +34,8 @@ request.interceptors.response.use(
|
|
| 34 |
if (data.code === 200 || data.success) {
|
| 35 |
return data
|
| 36 |
} else {
|
| 37 |
-
ElMessage.error(data.message || '请求失败')
|
| 38 |
-
return Promise.reject(new Error(data.message || '请求失败'))
|
| 39 |
}
|
| 40 |
},
|
| 41 |
(error) => {
|
|
|
|
| 34 |
if (data.code === 200 || data.success) {
|
| 35 |
return data
|
| 36 |
} else {
|
| 37 |
+
ElMessage.error(data.msg || data.message || '请求失败')
|
| 38 |
+
return Promise.reject(new Error(data.msg || data.message || '请求失败'))
|
| 39 |
}
|
| 40 |
},
|
| 41 |
(error) => {
|
sau_frontend/src/views/About.vue
CHANGED
|
@@ -1,7 +1,54 @@
|
|
| 1 |
<template>
|
| 2 |
<div class="about">
|
| 3 |
-
<
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
</div>
|
| 6 |
</template>
|
| 7 |
|
|
@@ -13,16 +60,55 @@
|
|
| 13 |
@use '@/styles/variables.scss' as *;
|
| 14 |
|
| 15 |
.about {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
}
|
| 28 |
-
</style>
|
|
|
|
| 1 |
<template>
|
| 2 |
<div class="about">
|
| 3 |
+
<el-card class="about-card">
|
| 4 |
+
<div class="about-header">
|
| 5 |
+
<h1>自媒体自动化运营系统</h1>
|
| 6 |
+
<p class="version">social-auto-upload</p>
|
| 7 |
+
</div>
|
| 8 |
+
|
| 9 |
+
<el-divider />
|
| 10 |
+
|
| 11 |
+
<div class="about-section">
|
| 12 |
+
<h3>系统简介</h3>
|
| 13 |
+
<p>
|
| 14 |
+
本系统是一款强大的自动化工具,帮助内容创作者和运营人员一键将视频内容高效发布到多个国内外主流社交媒体平台。
|
| 15 |
+
支持视频上传、定时发布等功能。
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div class="about-section">
|
| 20 |
+
<h3>支持平台</h3>
|
| 21 |
+
<div class="platform-tags">
|
| 22 |
+
<el-tag type="danger">抖音</el-tag>
|
| 23 |
+
<el-tag type="success">快手</el-tag>
|
| 24 |
+
<el-tag type="warning">视频号</el-tag>
|
| 25 |
+
<el-tag type="info">小红书</el-tag>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<div class="about-section">
|
| 30 |
+
<h3>核心功能</h3>
|
| 31 |
+
<ul class="feature-list">
|
| 32 |
+
<li>多平台账号管理与登录状态维护</li>
|
| 33 |
+
<li>视频素材上传与管理</li>
|
| 34 |
+
<li>一键多平台发布</li>
|
| 35 |
+
<li>定时发布与批量发布</li>
|
| 36 |
+
<li>Cookie 导入导出</li>
|
| 37 |
+
</ul>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<div class="about-section">
|
| 41 |
+
<h3>技术栈</h3>
|
| 42 |
+
<div class="tech-tags">
|
| 43 |
+
<el-tag effect="plain">Vue 3</el-tag>
|
| 44 |
+
<el-tag effect="plain">Element Plus</el-tag>
|
| 45 |
+
<el-tag effect="plain">Pinia</el-tag>
|
| 46 |
+
<el-tag effect="plain">Flask</el-tag>
|
| 47 |
+
<el-tag effect="plain">Playwright</el-tag>
|
| 48 |
+
<el-tag effect="plain">SQLite</el-tag>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</el-card>
|
| 52 |
</div>
|
| 53 |
</template>
|
| 54 |
|
|
|
|
| 60 |
@use '@/styles/variables.scss' as *;
|
| 61 |
|
| 62 |
.about {
|
| 63 |
+
max-width: 700px;
|
| 64 |
+
margin: 0 auto;
|
| 65 |
+
|
| 66 |
+
.about-card {
|
| 67 |
+
.about-header {
|
| 68 |
+
text-align: center;
|
| 69 |
+
|
| 70 |
+
h1 {
|
| 71 |
+
color: $text-primary;
|
| 72 |
+
margin: 0 0 8px 0;
|
| 73 |
+
font-size: 24px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.version {
|
| 77 |
+
color: $text-secondary;
|
| 78 |
+
font-size: 14px;
|
| 79 |
+
margin: 0;
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.about-section {
|
| 84 |
+
margin-bottom: 24px;
|
| 85 |
+
|
| 86 |
+
h3 {
|
| 87 |
+
font-size: 16px;
|
| 88 |
+
color: $text-primary;
|
| 89 |
+
margin: 0 0 12px 0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
p {
|
| 93 |
+
color: $text-secondary;
|
| 94 |
+
line-height: 1.8;
|
| 95 |
+
margin: 0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.platform-tags,
|
| 99 |
+
.tech-tags {
|
| 100 |
+
display: flex;
|
| 101 |
+
flex-wrap: wrap;
|
| 102 |
+
gap: 10px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.feature-list {
|
| 106 |
+
margin: 0;
|
| 107 |
+
padding-left: 20px;
|
| 108 |
+
color: $text-secondary;
|
| 109 |
+
line-height: 2;
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
}
|
| 113 |
}
|
| 114 |
+
</style>
|
sau_frontend/src/views/AccountManagement.vue
CHANGED
|
@@ -433,6 +433,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|
| 433 |
import { accountApi } from '@/api/account'
|
| 434 |
import { useAccountStore } from '@/stores/account'
|
| 435 |
import { useAppStore } from '@/stores/app'
|
|
|
|
| 436 |
|
| 437 |
// 获取账号状态管理
|
| 438 |
const accountStore = useAccountStore()
|
|
@@ -452,9 +453,8 @@ const fetchAccountsQuick = async () => {
|
|
| 452 |
if (res.code === 200 && res.data) {
|
| 453 |
// 将所有账号的状态暂时设为"验证中"
|
| 454 |
const accountsWithPendingStatus = res.data.map(account => {
|
| 455 |
-
// account[4] 是状态字段,暂时设为"验证中"
|
| 456 |
const updatedAccount = [...account];
|
| 457 |
-
updatedAccount[4] =
|
| 458 |
return updatedAccount;
|
| 459 |
});
|
| 460 |
accountStore.setAccounts(accountsWithPendingStatus);
|
|
@@ -713,24 +713,13 @@ const handleUploadCookie = (row) => {
|
|
| 713 |
formData.append('id', row.id)
|
| 714 |
formData.append('platform', row.platform)
|
| 715 |
|
| 716 |
-
// 发送上传请求
|
| 717 |
-
const
|
| 718 |
-
const response = await fetch(`${baseUrl}/uploadCookie`, {
|
| 719 |
-
method: 'POST',
|
| 720 |
-
body: formData
|
| 721 |
-
})
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
ElMessage.success('Cookie文件上传成功')
|
| 727 |
-
// 刷新账号列表以显示更新
|
| 728 |
-
fetchAccounts()
|
| 729 |
-
} else {
|
| 730 |
-
ElMessage.error(result.msg || 'Cookie文件上传失败')
|
| 731 |
-
}
|
| 732 |
} catch (error) {
|
| 733 |
-
console.error('上传Cookie文件失败:', error)
|
| 734 |
ElMessage.error('Cookie文件上传失败')
|
| 735 |
} finally {
|
| 736 |
document.body.removeChild(input)
|
|
@@ -811,22 +800,17 @@ const connectSSE = (platform, name) => {
|
|
| 811 |
// 监听消息
|
| 812 |
eventSource.onmessage = (event) => {
|
| 813 |
const data = event.data
|
| 814 |
-
console.log('SSE消息:', data)
|
| 815 |
|
| 816 |
// 如果还没有二维码数据,且数据长度较长,认为是二维码
|
| 817 |
if (!qrCodeData.value && data.length > 100) {
|
| 818 |
try {
|
| 819 |
-
// 确保数据是有效的base64编码
|
| 820 |
-
// 如果数据已经包含了data:image前缀,直接使用
|
| 821 |
if (data.startsWith('data:image')) {
|
| 822 |
qrCodeData.value = data
|
| 823 |
} else {
|
| 824 |
-
// 否则添加前缀
|
| 825 |
qrCodeData.value = `data:image/png;base64,${data}`
|
| 826 |
}
|
| 827 |
-
console.log('设置二维码数据,长度:', data.length)
|
| 828 |
} catch (error) {
|
| 829 |
-
|
| 830 |
}
|
| 831 |
}
|
| 832 |
// 如果收到状态码
|
|
@@ -897,10 +881,10 @@ const submitAccountForm = () => {
|
|
| 897 |
try {
|
| 898 |
// 将平台名称转换为类型数字
|
| 899 |
const platformTypeMap = {
|
| 900 |
-
'
|
| 901 |
-
'
|
| 902 |
-
'
|
| 903 |
-
'
|
| 904 |
};
|
| 905 |
const type = platformTypeMap[accountForm.platform] || 1;
|
| 906 |
|
|
|
|
| 433 |
import { accountApi } from '@/api/account'
|
| 434 |
import { useAccountStore } from '@/stores/account'
|
| 435 |
import { useAppStore } from '@/stores/app'
|
| 436 |
+
import { http } from '@/utils/request'
|
| 437 |
|
| 438 |
// 获取账号状态管理
|
| 439 |
const accountStore = useAccountStore()
|
|
|
|
| 453 |
if (res.code === 200 && res.data) {
|
| 454 |
// 将所有账号的状态暂时设为"验证中"
|
| 455 |
const accountsWithPendingStatus = res.data.map(account => {
|
|
|
|
| 456 |
const updatedAccount = [...account];
|
| 457 |
+
updatedAccount[4] = -1; // -1 表示验证中的临时状态
|
| 458 |
return updatedAccount;
|
| 459 |
});
|
| 460 |
accountStore.setAccounts(accountsWithPendingStatus);
|
|
|
|
| 713 |
formData.append('id', row.id)
|
| 714 |
formData.append('platform', row.platform)
|
| 715 |
|
| 716 |
+
// 使用统一的http封装发送上传请求
|
| 717 |
+
const result = await http.upload('/uploadCookie', formData)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
|
| 719 |
+
ElMessage.success('Cookie文件上传成功')
|
| 720 |
+
// 刷新账号列表以显示更新
|
| 721 |
+
fetchAccounts()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
} catch (error) {
|
|
|
|
| 723 |
ElMessage.error('Cookie文件上传失败')
|
| 724 |
} finally {
|
| 725 |
document.body.removeChild(input)
|
|
|
|
| 800 |
// 监听消息
|
| 801 |
eventSource.onmessage = (event) => {
|
| 802 |
const data = event.data
|
|
|
|
| 803 |
|
| 804 |
// 如果还没有二维码数据,且数据长度较长,认为是二维码
|
| 805 |
if (!qrCodeData.value && data.length > 100) {
|
| 806 |
try {
|
|
|
|
|
|
|
| 807 |
if (data.startsWith('data:image')) {
|
| 808 |
qrCodeData.value = data
|
| 809 |
} else {
|
|
|
|
| 810 |
qrCodeData.value = `data:image/png;base64,${data}`
|
| 811 |
}
|
|
|
|
| 812 |
} catch (error) {
|
| 813 |
+
// 处理二维码数据出错
|
| 814 |
}
|
| 815 |
}
|
| 816 |
// 如果收到状态码
|
|
|
|
| 881 |
try {
|
| 882 |
// 将平台名称转换为类型数字
|
| 883 |
const platformTypeMap = {
|
| 884 |
+
'小红书': 1,
|
| 885 |
+
'视频号': 2,
|
| 886 |
+
'抖音': 3,
|
| 887 |
+
'快手': 4
|
| 888 |
};
|
| 889 |
const type = platformTypeMap[accountForm.platform] || 1;
|
| 890 |
|
sau_frontend/src/views/Dashboard.vue
CHANGED
|
@@ -3,11 +3,11 @@
|
|
| 3 |
<div class="page-header">
|
| 4 |
<h1>自媒体自动化运营系统</h1>
|
| 5 |
</div>
|
| 6 |
-
|
| 7 |
<div class="dashboard-content">
|
| 8 |
<el-row :gutter="20">
|
| 9 |
<!-- 账号统计卡片 -->
|
| 10 |
-
<el-col :span="
|
| 11 |
<el-card class="stat-card">
|
| 12 |
<div class="stat-card-content">
|
| 13 |
<div class="stat-icon">
|
|
@@ -26,9 +26,9 @@
|
|
| 26 |
</div>
|
| 27 |
</el-card>
|
| 28 |
</el-col>
|
| 29 |
-
|
| 30 |
<!-- 平台统计卡片 -->
|
| 31 |
-
<el-col :span="
|
| 32 |
<el-card class="stat-card">
|
| 33 |
<div class="stat-card-content">
|
| 34 |
<div class="stat-icon platform-icon">
|
|
@@ -36,7 +36,7 @@
|
|
| 36 |
</div>
|
| 37 |
<div class="stat-info">
|
| 38 |
<div class="stat-value">{{ platformStats.total }}</div>
|
| 39 |
-
<div class="stat-label">平台
|
| 40 |
</div>
|
| 41 |
</div>
|
| 42 |
<div class="stat-footer">
|
|
@@ -57,31 +57,9 @@
|
|
| 57 |
</div>
|
| 58 |
</el-card>
|
| 59 |
</el-col>
|
| 60 |
-
|
| 61 |
-
<!--
|
| 62 |
-
<el-col :span="
|
| 63 |
-
<el-card class="stat-card">
|
| 64 |
-
<div class="stat-card-content">
|
| 65 |
-
<div class="stat-icon task-icon">
|
| 66 |
-
<el-icon><List /></el-icon>
|
| 67 |
-
</div>
|
| 68 |
-
<div class="stat-info">
|
| 69 |
-
<div class="stat-value">{{ taskStats.total }}</div>
|
| 70 |
-
<div class="stat-label">任务总数</div>
|
| 71 |
-
</div>
|
| 72 |
-
</div>
|
| 73 |
-
<div class="stat-footer">
|
| 74 |
-
<div class="stat-detail">
|
| 75 |
-
<span>完成: {{ taskStats.completed }}</span>
|
| 76 |
-
<span>进行中: {{ taskStats.inProgress }}</span>
|
| 77 |
-
<span>失败: {{ taskStats.failed }}</span>
|
| 78 |
-
</div>
|
| 79 |
-
</div>
|
| 80 |
-
</el-card>
|
| 81 |
-
</el-col>
|
| 82 |
-
|
| 83 |
-
<!-- 内容统计卡片 -->
|
| 84 |
-
<el-col :span="6">
|
| 85 |
<el-card class="stat-card">
|
| 86 |
<div class="stat-card-content">
|
| 87 |
<div class="stat-icon content-icon">
|
|
@@ -89,19 +67,20 @@
|
|
| 89 |
</div>
|
| 90 |
<div class="stat-info">
|
| 91 |
<div class="stat-value">{{ contentStats.total }}</div>
|
| 92 |
-
<div class="stat-label">
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
<div class="stat-footer">
|
| 96 |
<div class="stat-detail">
|
| 97 |
-
<span>
|
| 98 |
-
<span>
|
|
|
|
| 99 |
</div>
|
| 100 |
</div>
|
| 101 |
</el-card>
|
| 102 |
</el-col>
|
| 103 |
</el-row>
|
| 104 |
-
|
| 105 |
<!-- 快捷操作区域 -->
|
| 106 |
<div class="quick-actions">
|
| 107 |
<h2>快捷操作</h2>
|
|
@@ -116,199 +95,144 @@
|
|
| 116 |
</el-card>
|
| 117 |
</el-col>
|
| 118 |
<el-col :span="6">
|
| 119 |
-
<el-card class="action-card">
|
| 120 |
<div class="action-icon">
|
| 121 |
<el-icon><Upload /></el-icon>
|
| 122 |
</div>
|
| 123 |
-
<div class="action-title">
|
| 124 |
-
<div class="action-desc">上传视频
|
| 125 |
</el-card>
|
| 126 |
</el-col>
|
| 127 |
<el-col :span="6">
|
| 128 |
-
<el-card class="action-card">
|
| 129 |
<div class="action-icon">
|
| 130 |
<el-icon><Timer /></el-icon>
|
| 131 |
</div>
|
| 132 |
-
<div class="action-title">
|
| 133 |
-
<div class="action-desc">
|
| 134 |
</el-card>
|
| 135 |
</el-col>
|
| 136 |
<el-col :span="6">
|
| 137 |
-
<el-card class="action-card">
|
| 138 |
<div class="action-icon">
|
| 139 |
<el-icon><DataAnalysis /></el-icon>
|
| 140 |
</div>
|
| 141 |
-
<div class="action-title">
|
| 142 |
-
<div class="action-desc">查看
|
| 143 |
</el-card>
|
| 144 |
</el-col>
|
| 145 |
</el-row>
|
| 146 |
</div>
|
| 147 |
-
|
| 148 |
-
<!--
|
| 149 |
<div class="recent-tasks">
|
| 150 |
<div class="section-header">
|
| 151 |
-
<h2>最近
|
| 152 |
-
<el-button text>查看全部</el-button>
|
| 153 |
</div>
|
| 154 |
-
|
| 155 |
-
<el-table :data="
|
| 156 |
-
<el-table-column prop="
|
| 157 |
-
<el-table-column prop="
|
| 158 |
<template #default="scope">
|
| 159 |
-
|
| 160 |
-
:type="getPlatformTagType(scope.row.platform)"
|
| 161 |
-
effect="plain"
|
| 162 |
-
>
|
| 163 |
-
{{ scope.row.platform }}
|
| 164 |
-
</el-tag>
|
| 165 |
</template>
|
| 166 |
</el-table-column>
|
| 167 |
-
<el-table-column prop="
|
| 168 |
-
<el-table-column
|
| 169 |
-
<el-table-column prop="status" label="状态" width="120">
|
| 170 |
<template #default="scope">
|
| 171 |
<el-tag
|
| 172 |
-
:type="
|
| 173 |
effect="plain"
|
|
|
|
| 174 |
>
|
| 175 |
-
{{ scope.row.
|
| 176 |
</el-tag>
|
| 177 |
</template>
|
| 178 |
</el-table-column>
|
| 179 |
-
<el-table-column label="操作">
|
| 180 |
-
<template #default="scope">
|
| 181 |
-
<el-button size="small" @click="viewTaskDetail(scope.row)">查看</el-button>
|
| 182 |
-
<el-button
|
| 183 |
-
size="small"
|
| 184 |
-
type="primary"
|
| 185 |
-
v-if="scope.row.status === '待执行'"
|
| 186 |
-
@click="executeTask(scope.row)"
|
| 187 |
-
>
|
| 188 |
-
执行
|
| 189 |
-
</el-button>
|
| 190 |
-
<el-button
|
| 191 |
-
size="small"
|
| 192 |
-
type="danger"
|
| 193 |
-
v-if="scope.row.status !== '已完成' && scope.row.status !== '已失败'"
|
| 194 |
-
@click="cancelTask(scope.row)"
|
| 195 |
-
>
|
| 196 |
-
取消
|
| 197 |
-
</el-button>
|
| 198 |
-
</template>
|
| 199 |
-
</el-table-column>
|
| 200 |
</el-table>
|
|
|
|
|
|
|
| 201 |
</div>
|
| 202 |
</div>
|
| 203 |
</div>
|
| 204 |
</template>
|
| 205 |
|
| 206 |
<script setup>
|
| 207 |
-
import { ref, reactive } from 'vue'
|
| 208 |
import { useRouter } from 'vue-router'
|
| 209 |
-
import {
|
| 210 |
-
User, UserFilled, Platform,
|
| 211 |
-
Upload, Timer, DataAnalysis
|
| 212 |
} from '@element-plus/icons-vue'
|
| 213 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
-
// 账号统计数据
|
| 218 |
-
const accountStats =
|
| 219 |
-
|
| 220 |
-
normal
|
| 221 |
-
abnormal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
})
|
| 223 |
|
| 224 |
-
// 平台统计数据
|
| 225 |
-
const platformStats =
|
| 226 |
-
|
| 227 |
-
kuaishou
|
| 228 |
-
douyin
|
| 229 |
-
channels
|
| 230 |
-
xiaohongshu
|
|
|
|
|
|
|
|
|
|
| 231 |
})
|
| 232 |
|
| 233 |
-
//
|
| 234 |
-
const
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
})
|
| 240 |
|
| 241 |
-
//
|
| 242 |
-
const
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
})
|
| 247 |
|
| 248 |
-
//
|
| 249 |
-
const
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
platform: '快手',
|
| 254 |
-
account: '快手账号1',
|
| 255 |
-
createTime: '2024-05-01 10:30:00',
|
| 256 |
-
status: '已完成'
|
| 257 |
-
},
|
| 258 |
-
{
|
| 259 |
-
id: 2,
|
| 260 |
-
title: '抖音视频定时发布',
|
| 261 |
-
platform: '抖音',
|
| 262 |
-
account: '抖音账号1',
|
| 263 |
-
createTime: '2024-05-01 11:15:00',
|
| 264 |
-
status: '进行中'
|
| 265 |
-
},
|
| 266 |
-
{
|
| 267 |
-
id: 3,
|
| 268 |
-
title: '视频号内容上传',
|
| 269 |
-
platform: '视频号',
|
| 270 |
-
account: '视频号账号1',
|
| 271 |
-
createTime: '2024-05-01 14:20:00',
|
| 272 |
-
status: '待执行'
|
| 273 |
-
},
|
| 274 |
-
{
|
| 275 |
-
id: 4,
|
| 276 |
-
title: '小红书图文发布',
|
| 277 |
-
platform: '小红书',
|
| 278 |
-
account: '小红书账号1',
|
| 279 |
-
createTime: '2024-05-01 16:45:00',
|
| 280 |
-
status: '已失败'
|
| 281 |
-
},
|
| 282 |
-
{
|
| 283 |
-
id: 5,
|
| 284 |
-
title: '快手短视频批量上传',
|
| 285 |
-
platform: '快手',
|
| 286 |
-
account: '快手账号2',
|
| 287 |
-
createTime: '2024-05-02 09:10:00',
|
| 288 |
-
status: '待执行'
|
| 289 |
-
}
|
| 290 |
-
])
|
| 291 |
-
|
| 292 |
-
// 根据平台获取标签类型
|
| 293 |
-
const getPlatformTagType = (platform) => {
|
| 294 |
-
const typeMap = {
|
| 295 |
-
'快手': 'success',
|
| 296 |
-
'抖音': 'danger',
|
| 297 |
-
'视频号': 'warning',
|
| 298 |
-
'小红书': 'info'
|
| 299 |
-
}
|
| 300 |
-
return typeMap[platform] || 'info'
|
| 301 |
}
|
| 302 |
|
| 303 |
-
//
|
| 304 |
-
const
|
| 305 |
-
const
|
| 306 |
-
|
| 307 |
-
'进行中': 'warning',
|
| 308 |
-
'待执行': 'info',
|
| 309 |
-
'已失败': 'danger'
|
| 310 |
-
}
|
| 311 |
-
return typeMap[status] || 'info'
|
| 312 |
}
|
| 313 |
|
| 314 |
// 导航到指定路由
|
|
@@ -316,65 +240,32 @@ const navigateTo = (path) => {
|
|
| 316 |
router.push(path)
|
| 317 |
}
|
| 318 |
|
| 319 |
-
//
|
| 320 |
-
const
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
ElMessageBox.confirm(
|
| 328 |
-
`确定要执行任务 ${task.title} 吗?`,
|
| 329 |
-
'提示',
|
| 330 |
-
{
|
| 331 |
-
confirmButtonText: '确定',
|
| 332 |
-
cancelButtonText: '取消',
|
| 333 |
-
type: 'info',
|
| 334 |
}
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
// 更新任务状态
|
| 338 |
-
const index = recentTasks.value.findIndex(t => t.id === task.id)
|
| 339 |
-
if (index !== -1) {
|
| 340 |
-
recentTasks.value[index].status = '进行中'
|
| 341 |
-
}
|
| 342 |
-
ElMessage({
|
| 343 |
-
type: 'success',
|
| 344 |
-
message: '任务已开始执行',
|
| 345 |
-
})
|
| 346 |
-
})
|
| 347 |
-
.catch(() => {
|
| 348 |
-
// 取消执行
|
| 349 |
-
})
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
// 取消任务
|
| 353 |
-
const cancelTask = (task) => {
|
| 354 |
-
ElMessageBox.confirm(
|
| 355 |
-
`确定要取消任务 ${task.title} 吗?`,
|
| 356 |
-
'警告',
|
| 357 |
-
{
|
| 358 |
-
confirmButtonText: '确定',
|
| 359 |
-
cancelButtonText: '取消',
|
| 360 |
-
type: 'warning',
|
| 361 |
}
|
| 362 |
-
)
|
| 363 |
-
.
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
recentTasks.value[index].status = '已取消'
|
| 368 |
-
}
|
| 369 |
-
ElMessage({
|
| 370 |
-
type: 'success',
|
| 371 |
-
message: '任务已取消',
|
| 372 |
-
})
|
| 373 |
-
})
|
| 374 |
-
.catch(() => {
|
| 375 |
-
// 取消操作
|
| 376 |
-
})
|
| 377 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
</script>
|
| 379 |
|
| 380 |
<style lang="scss" scoped>
|
|
@@ -383,24 +274,24 @@ const cancelTask = (task) => {
|
|
| 383 |
.dashboard {
|
| 384 |
.page-header {
|
| 385 |
margin-bottom: 20px;
|
| 386 |
-
|
| 387 |
h1 {
|
| 388 |
font-size: 24px;
|
| 389 |
color: $text-primary;
|
| 390 |
margin: 0;
|
| 391 |
}
|
| 392 |
}
|
| 393 |
-
|
| 394 |
.dashboard-content {
|
| 395 |
.stat-card {
|
| 396 |
height: 140px;
|
| 397 |
margin-bottom: 20px;
|
| 398 |
-
|
| 399 |
.stat-card-content {
|
| 400 |
display: flex;
|
| 401 |
align-items: center;
|
| 402 |
margin-bottom: 15px;
|
| 403 |
-
|
| 404 |
.stat-icon {
|
| 405 |
width: 60px;
|
| 406 |
height: 60px;
|
|
@@ -410,37 +301,29 @@ const cancelTask = (task) => {
|
|
| 410 |
justify-content: center;
|
| 411 |
align-items: center;
|
| 412 |
margin-right: 15px;
|
| 413 |
-
|
| 414 |
.el-icon {
|
| 415 |
font-size: 30px;
|
| 416 |
color: $primary-color;
|
| 417 |
}
|
| 418 |
-
|
| 419 |
&.platform-icon {
|
| 420 |
background-color: rgba($success-color, 0.1);
|
| 421 |
-
|
| 422 |
.el-icon {
|
| 423 |
color: $success-color;
|
| 424 |
}
|
| 425 |
}
|
| 426 |
-
|
| 427 |
-
&.task-icon {
|
| 428 |
-
background-color: rgba($warning-color, 0.1);
|
| 429 |
-
|
| 430 |
-
.el-icon {
|
| 431 |
-
color: $warning-color;
|
| 432 |
-
}
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
&.content-icon {
|
| 436 |
background-color: rgba($info-color, 0.1);
|
| 437 |
-
|
| 438 |
.el-icon {
|
| 439 |
color: $info-color;
|
| 440 |
}
|
| 441 |
}
|
| 442 |
}
|
| 443 |
-
|
| 444 |
.stat-info {
|
| 445 |
.stat-value {
|
| 446 |
font-size: 24px;
|
|
@@ -448,40 +331,40 @@ const cancelTask = (task) => {
|
|
| 448 |
color: $text-primary;
|
| 449 |
line-height: 1.2;
|
| 450 |
}
|
| 451 |
-
|
| 452 |
.stat-label {
|
| 453 |
font-size: 14px;
|
| 454 |
color: $text-secondary;
|
| 455 |
}
|
| 456 |
}
|
| 457 |
}
|
| 458 |
-
|
| 459 |
.stat-footer {
|
| 460 |
border-top: 1px solid $border-lighter;
|
| 461 |
padding-top: 10px;
|
| 462 |
-
|
| 463 |
.stat-detail {
|
| 464 |
display: flex;
|
| 465 |
justify-content: space-between;
|
| 466 |
color: $text-secondary;
|
| 467 |
font-size: 13px;
|
| 468 |
-
|
| 469 |
.el-tag {
|
| 470 |
margin-right: 5px;
|
| 471 |
}
|
| 472 |
}
|
| 473 |
}
|
| 474 |
}
|
| 475 |
-
|
| 476 |
.quick-actions {
|
| 477 |
margin: 20px 0 30px;
|
| 478 |
-
|
| 479 |
h2 {
|
| 480 |
font-size: 18px;
|
| 481 |
margin-bottom: 15px;
|
| 482 |
color: $text-primary;
|
| 483 |
}
|
| 484 |
-
|
| 485 |
.action-card {
|
| 486 |
height: 160px;
|
| 487 |
display: flex;
|
|
@@ -490,12 +373,12 @@ const cancelTask = (task) => {
|
|
| 490 |
justify-content: center;
|
| 491 |
cursor: pointer;
|
| 492 |
transition: all 0.3s;
|
| 493 |
-
|
| 494 |
&:hover {
|
| 495 |
transform: translateY(-5px);
|
| 496 |
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
| 497 |
}
|
| 498 |
-
|
| 499 |
.action-icon {
|
| 500 |
width: 50px;
|
| 501 |
height: 50px;
|
|
@@ -505,20 +388,20 @@ const cancelTask = (task) => {
|
|
| 505 |
justify-content: center;
|
| 506 |
align-items: center;
|
| 507 |
margin-bottom: 15px;
|
| 508 |
-
|
| 509 |
.el-icon {
|
| 510 |
font-size: 24px;
|
| 511 |
color: $primary-color;
|
| 512 |
}
|
| 513 |
}
|
| 514 |
-
|
| 515 |
.action-title {
|
| 516 |
font-size: 16px;
|
| 517 |
font-weight: bold;
|
| 518 |
color: $text-primary;
|
| 519 |
margin-bottom: 5px;
|
| 520 |
}
|
| 521 |
-
|
| 522 |
.action-desc {
|
| 523 |
font-size: 13px;
|
| 524 |
color: $text-secondary;
|
|
@@ -526,16 +409,16 @@ const cancelTask = (task) => {
|
|
| 526 |
}
|
| 527 |
}
|
| 528 |
}
|
| 529 |
-
|
| 530 |
.recent-tasks {
|
| 531 |
margin-top: 30px;
|
| 532 |
-
|
| 533 |
.section-header {
|
| 534 |
display: flex;
|
| 535 |
justify-content: space-between;
|
| 536 |
align-items: center;
|
| 537 |
margin-bottom: 15px;
|
| 538 |
-
|
| 539 |
h2 {
|
| 540 |
font-size: 18px;
|
| 541 |
color: $text-primary;
|
|
@@ -545,4 +428,4 @@ const cancelTask = (task) => {
|
|
| 545 |
}
|
| 546 |
}
|
| 547 |
}
|
| 548 |
-
</style>
|
|
|
|
| 3 |
<div class="page-header">
|
| 4 |
<h1>自媒体自动化运营系统</h1>
|
| 5 |
</div>
|
| 6 |
+
|
| 7 |
<div class="dashboard-content">
|
| 8 |
<el-row :gutter="20">
|
| 9 |
<!-- 账号统计卡片 -->
|
| 10 |
+
<el-col :span="8">
|
| 11 |
<el-card class="stat-card">
|
| 12 |
<div class="stat-card-content">
|
| 13 |
<div class="stat-icon">
|
|
|
|
| 26 |
</div>
|
| 27 |
</el-card>
|
| 28 |
</el-col>
|
| 29 |
+
|
| 30 |
<!-- 平台统计卡片 -->
|
| 31 |
+
<el-col :span="8">
|
| 32 |
<el-card class="stat-card">
|
| 33 |
<div class="stat-card-content">
|
| 34 |
<div class="stat-icon platform-icon">
|
|
|
|
| 36 |
</div>
|
| 37 |
<div class="stat-info">
|
| 38 |
<div class="stat-value">{{ platformStats.total }}</div>
|
| 39 |
+
<div class="stat-label">已接入平台</div>
|
| 40 |
</div>
|
| 41 |
</div>
|
| 42 |
<div class="stat-footer">
|
|
|
|
| 57 |
</div>
|
| 58 |
</el-card>
|
| 59 |
</el-col>
|
| 60 |
+
|
| 61 |
+
<!-- 素材统计卡片 -->
|
| 62 |
+
<el-col :span="8">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
<el-card class="stat-card">
|
| 64 |
<div class="stat-card-content">
|
| 65 |
<div class="stat-icon content-icon">
|
|
|
|
| 67 |
</div>
|
| 68 |
<div class="stat-info">
|
| 69 |
<div class="stat-value">{{ contentStats.total }}</div>
|
| 70 |
+
<div class="stat-label">素材总数</div>
|
| 71 |
</div>
|
| 72 |
</div>
|
| 73 |
<div class="stat-footer">
|
| 74 |
<div class="stat-detail">
|
| 75 |
+
<span>视频: {{ contentStats.videos }}</span>
|
| 76 |
+
<span>图片: {{ contentStats.images }}</span>
|
| 77 |
+
<span>其他: {{ contentStats.others }}</span>
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
</el-card>
|
| 81 |
</el-col>
|
| 82 |
</el-row>
|
| 83 |
+
|
| 84 |
<!-- 快捷操作区域 -->
|
| 85 |
<div class="quick-actions">
|
| 86 |
<h2>快捷操作</h2>
|
|
|
|
| 95 |
</el-card>
|
| 96 |
</el-col>
|
| 97 |
<el-col :span="6">
|
| 98 |
+
<el-card class="action-card" @click="navigateTo('/material-management')">
|
| 99 |
<div class="action-icon">
|
| 100 |
<el-icon><Upload /></el-icon>
|
| 101 |
</div>
|
| 102 |
+
<div class="action-title">素材管理</div>
|
| 103 |
+
<div class="action-desc">上传和管理视频素材</div>
|
| 104 |
</el-card>
|
| 105 |
</el-col>
|
| 106 |
<el-col :span="6">
|
| 107 |
+
<el-card class="action-card" @click="navigateTo('/publish-center')">
|
| 108 |
<div class="action-icon">
|
| 109 |
<el-icon><Timer /></el-icon>
|
| 110 |
</div>
|
| 111 |
+
<div class="action-title">发布中心</div>
|
| 112 |
+
<div class="action-desc">发布内容到各平台</div>
|
| 113 |
</el-card>
|
| 114 |
</el-col>
|
| 115 |
<el-col :span="6">
|
| 116 |
+
<el-card class="action-card" @click="navigateTo('/about')">
|
| 117 |
<div class="action-icon">
|
| 118 |
<el-icon><DataAnalysis /></el-icon>
|
| 119 |
</div>
|
| 120 |
+
<div class="action-title">关于系统</div>
|
| 121 |
+
<div class="action-desc">查看系统信息</div>
|
| 122 |
</el-card>
|
| 123 |
</el-col>
|
| 124 |
</el-row>
|
| 125 |
</div>
|
| 126 |
+
|
| 127 |
+
<!-- 素材列表 -->
|
| 128 |
<div class="recent-tasks">
|
| 129 |
<div class="section-header">
|
| 130 |
+
<h2>最近上传素材</h2>
|
| 131 |
+
<el-button text @click="navigateTo('/material-management')">查看全部</el-button>
|
| 132 |
</div>
|
| 133 |
+
|
| 134 |
+
<el-table :data="recentMaterials" style="width: 100%" v-loading="loading">
|
| 135 |
+
<el-table-column prop="filename" label="文件名" width="300" />
|
| 136 |
+
<el-table-column prop="filesize" label="文件大小" width="120">
|
| 137 |
<template #default="scope">
|
| 138 |
+
{{ scope.row.filesize }} MB
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
</template>
|
| 140 |
</el-table-column>
|
| 141 |
+
<el-table-column prop="upload_time" label="上传时间" width="200" />
|
| 142 |
+
<el-table-column label="类型" width="100">
|
|
|
|
| 143 |
<template #default="scope">
|
| 144 |
<el-tag
|
| 145 |
+
:type="getFileTypeTag(scope.row.filename)"
|
| 146 |
effect="plain"
|
| 147 |
+
size="small"
|
| 148 |
>
|
| 149 |
+
{{ getFileType(scope.row.filename) }}
|
| 150 |
</el-tag>
|
| 151 |
</template>
|
| 152 |
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
</el-table>
|
| 154 |
+
|
| 155 |
+
<el-empty v-if="!loading && recentMaterials.length === 0" description="暂无素材数据" />
|
| 156 |
</div>
|
| 157 |
</div>
|
| 158 |
</div>
|
| 159 |
</template>
|
| 160 |
|
| 161 |
<script setup>
|
| 162 |
+
import { ref, reactive, computed, onMounted } from 'vue'
|
| 163 |
import { useRouter } from 'vue-router'
|
| 164 |
+
import {
|
| 165 |
+
User, UserFilled, Platform, Document,
|
| 166 |
+
Upload, Timer, DataAnalysis
|
| 167 |
} from '@element-plus/icons-vue'
|
| 168 |
+
import { accountApi } from '@/api/account'
|
| 169 |
+
import { materialApi } from '@/api/material'
|
| 170 |
+
import { useAccountStore } from '@/stores/account'
|
| 171 |
+
import { useAppStore } from '@/stores/app'
|
| 172 |
|
| 173 |
const router = useRouter()
|
| 174 |
+
const accountStore = useAccountStore()
|
| 175 |
+
const appStore = useAppStore()
|
| 176 |
+
const loading = ref(false)
|
| 177 |
|
| 178 |
+
// 账号统计数据 - 从真实数据计算
|
| 179 |
+
const accountStats = computed(() => {
|
| 180 |
+
const accounts = accountStore.accounts
|
| 181 |
+
const normal = accounts.filter(a => a.status === '正常').length
|
| 182 |
+
const abnormal = accounts.filter(a => a.status !== '正常' && a.status !== '验证中').length
|
| 183 |
+
return {
|
| 184 |
+
total: accounts.length,
|
| 185 |
+
normal,
|
| 186 |
+
abnormal
|
| 187 |
+
}
|
| 188 |
})
|
| 189 |
|
| 190 |
+
// 平台统计数据 - 从真实数据计算
|
| 191 |
+
const platformStats = computed(() => {
|
| 192 |
+
const accounts = accountStore.accounts
|
| 193 |
+
const kuaishou = accounts.filter(a => a.platform === '快手').length
|
| 194 |
+
const douyin = accounts.filter(a => a.platform === '抖音').length
|
| 195 |
+
const channels = accounts.filter(a => a.platform === '视频号').length
|
| 196 |
+
const xiaohongshu = accounts.filter(a => a.platform === '小红书').length
|
| 197 |
+
// 统计有账号的平台数量
|
| 198 |
+
const total = [kuaishou, douyin, channels, xiaohongshu].filter(n => n > 0).length
|
| 199 |
+
return { total, kuaishou, douyin, channels, xiaohongshu }
|
| 200 |
})
|
| 201 |
|
| 202 |
+
// 素材统计数据 - 从真实数据计算
|
| 203 |
+
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
| 204 |
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
| 205 |
+
|
| 206 |
+
const contentStats = computed(() => {
|
| 207 |
+
const materials = appStore.materials
|
| 208 |
+
const videos = materials.filter(m => videoExtensions.some(ext => m.filename.toLowerCase().endsWith(ext))).length
|
| 209 |
+
const images = materials.filter(m => imageExtensions.some(ext => m.filename.toLowerCase().endsWith(ext))).length
|
| 210 |
+
return {
|
| 211 |
+
total: materials.length,
|
| 212 |
+
videos,
|
| 213 |
+
images,
|
| 214 |
+
others: materials.length - videos - images
|
| 215 |
+
}
|
| 216 |
})
|
| 217 |
|
| 218 |
+
// 最近上传的素材(最多显示5条)
|
| 219 |
+
const recentMaterials = computed(() => {
|
| 220 |
+
return [...appStore.materials]
|
| 221 |
+
.sort((a, b) => new Date(b.upload_time) - new Date(a.upload_time))
|
| 222 |
+
.slice(0, 5)
|
| 223 |
})
|
| 224 |
|
| 225 |
+
// 获取文件类型
|
| 226 |
+
const getFileType = (filename) => {
|
| 227 |
+
if (videoExtensions.some(ext => filename.toLowerCase().endsWith(ext))) return '视频'
|
| 228 |
+
if (imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))) return '图片'
|
| 229 |
+
return '其他'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
+
// 获取文件类型标签颜色
|
| 233 |
+
const getFileTypeTag = (filename) => {
|
| 234 |
+
const type = getFileType(filename)
|
| 235 |
+
return { '视频': 'success', '图片': 'warning', '其他': 'info' }[type] || 'info'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
|
| 238 |
// 导航到指定路由
|
|
|
|
| 240 |
router.push(path)
|
| 241 |
}
|
| 242 |
|
| 243 |
+
// 加载数据
|
| 244 |
+
const fetchDashboardData = async () => {
|
| 245 |
+
loading.value = true
|
| 246 |
+
try {
|
| 247 |
+
// 并行获取账号和素材数据
|
| 248 |
+
const [accountRes, materialRes] = await Promise.allSettled([
|
| 249 |
+
accountApi.getAccounts(),
|
| 250 |
+
materialApi.getAllMaterials()
|
| 251 |
+
])
|
| 252 |
|
| 253 |
+
if (accountRes.status === 'fulfilled' && accountRes.value.code === 200) {
|
| 254 |
+
accountStore.setAccounts(accountRes.value.data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
+
if (materialRes.status === 'fulfilled' && materialRes.value.code === 200) {
|
| 257 |
+
appStore.setMaterials(materialRes.value.data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
}
|
| 259 |
+
} catch (error) {
|
| 260 |
+
console.error('获取仪表盘数据失败:', error)
|
| 261 |
+
} finally {
|
| 262 |
+
loading.value = false
|
| 263 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
}
|
| 265 |
+
|
| 266 |
+
onMounted(() => {
|
| 267 |
+
fetchDashboardData()
|
| 268 |
+
})
|
| 269 |
</script>
|
| 270 |
|
| 271 |
<style lang="scss" scoped>
|
|
|
|
| 274 |
.dashboard {
|
| 275 |
.page-header {
|
| 276 |
margin-bottom: 20px;
|
| 277 |
+
|
| 278 |
h1 {
|
| 279 |
font-size: 24px;
|
| 280 |
color: $text-primary;
|
| 281 |
margin: 0;
|
| 282 |
}
|
| 283 |
}
|
| 284 |
+
|
| 285 |
.dashboard-content {
|
| 286 |
.stat-card {
|
| 287 |
height: 140px;
|
| 288 |
margin-bottom: 20px;
|
| 289 |
+
|
| 290 |
.stat-card-content {
|
| 291 |
display: flex;
|
| 292 |
align-items: center;
|
| 293 |
margin-bottom: 15px;
|
| 294 |
+
|
| 295 |
.stat-icon {
|
| 296 |
width: 60px;
|
| 297 |
height: 60px;
|
|
|
|
| 301 |
justify-content: center;
|
| 302 |
align-items: center;
|
| 303 |
margin-right: 15px;
|
| 304 |
+
|
| 305 |
.el-icon {
|
| 306 |
font-size: 30px;
|
| 307 |
color: $primary-color;
|
| 308 |
}
|
| 309 |
+
|
| 310 |
&.platform-icon {
|
| 311 |
background-color: rgba($success-color, 0.1);
|
| 312 |
+
|
| 313 |
.el-icon {
|
| 314 |
color: $success-color;
|
| 315 |
}
|
| 316 |
}
|
| 317 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
&.content-icon {
|
| 319 |
background-color: rgba($info-color, 0.1);
|
| 320 |
+
|
| 321 |
.el-icon {
|
| 322 |
color: $info-color;
|
| 323 |
}
|
| 324 |
}
|
| 325 |
}
|
| 326 |
+
|
| 327 |
.stat-info {
|
| 328 |
.stat-value {
|
| 329 |
font-size: 24px;
|
|
|
|
| 331 |
color: $text-primary;
|
| 332 |
line-height: 1.2;
|
| 333 |
}
|
| 334 |
+
|
| 335 |
.stat-label {
|
| 336 |
font-size: 14px;
|
| 337 |
color: $text-secondary;
|
| 338 |
}
|
| 339 |
}
|
| 340 |
}
|
| 341 |
+
|
| 342 |
.stat-footer {
|
| 343 |
border-top: 1px solid $border-lighter;
|
| 344 |
padding-top: 10px;
|
| 345 |
+
|
| 346 |
.stat-detail {
|
| 347 |
display: flex;
|
| 348 |
justify-content: space-between;
|
| 349 |
color: $text-secondary;
|
| 350 |
font-size: 13px;
|
| 351 |
+
|
| 352 |
.el-tag {
|
| 353 |
margin-right: 5px;
|
| 354 |
}
|
| 355 |
}
|
| 356 |
}
|
| 357 |
}
|
| 358 |
+
|
| 359 |
.quick-actions {
|
| 360 |
margin: 20px 0 30px;
|
| 361 |
+
|
| 362 |
h2 {
|
| 363 |
font-size: 18px;
|
| 364 |
margin-bottom: 15px;
|
| 365 |
color: $text-primary;
|
| 366 |
}
|
| 367 |
+
|
| 368 |
.action-card {
|
| 369 |
height: 160px;
|
| 370 |
display: flex;
|
|
|
|
| 373 |
justify-content: center;
|
| 374 |
cursor: pointer;
|
| 375 |
transition: all 0.3s;
|
| 376 |
+
|
| 377 |
&:hover {
|
| 378 |
transform: translateY(-5px);
|
| 379 |
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
| 380 |
}
|
| 381 |
+
|
| 382 |
.action-icon {
|
| 383 |
width: 50px;
|
| 384 |
height: 50px;
|
|
|
|
| 388 |
justify-content: center;
|
| 389 |
align-items: center;
|
| 390 |
margin-bottom: 15px;
|
| 391 |
+
|
| 392 |
.el-icon {
|
| 393 |
font-size: 24px;
|
| 394 |
color: $primary-color;
|
| 395 |
}
|
| 396 |
}
|
| 397 |
+
|
| 398 |
.action-title {
|
| 399 |
font-size: 16px;
|
| 400 |
font-weight: bold;
|
| 401 |
color: $text-primary;
|
| 402 |
margin-bottom: 5px;
|
| 403 |
}
|
| 404 |
+
|
| 405 |
.action-desc {
|
| 406 |
font-size: 13px;
|
| 407 |
color: $text-secondary;
|
|
|
|
| 409 |
}
|
| 410 |
}
|
| 411 |
}
|
| 412 |
+
|
| 413 |
.recent-tasks {
|
| 414 |
margin-top: 30px;
|
| 415 |
+
|
| 416 |
.section-header {
|
| 417 |
display: flex;
|
| 418 |
justify-content: space-between;
|
| 419 |
align-items: center;
|
| 420 |
margin-bottom: 15px;
|
| 421 |
+
|
| 422 |
h2 {
|
| 423 |
font-size: 18px;
|
| 424 |
color: $text-primary;
|
|
|
|
| 428 |
}
|
| 429 |
}
|
| 430 |
}
|
| 431 |
+
</style>
|
sau_frontend/src/views/Home.vue
DELETED
|
@@ -1,106 +0,0 @@
|
|
| 1 |
-
<template>
|
| 2 |
-
<div class="home">
|
| 3 |
-
<div class="welcome-section">
|
| 4 |
-
<h1>欢迎使用 Vue3 + Vite 项目</h1>
|
| 5 |
-
<p>这是一个集成了 Vue3、Vite、Element Plus、Pinia、Vue Router 和 Axios 的现代化前端项目</p>
|
| 6 |
-
<div class="features">
|
| 7 |
-
<el-row :gutter="20">
|
| 8 |
-
<el-col :span="8">
|
| 9 |
-
<el-card class="feature-card">
|
| 10 |
-
<template #header>
|
| 11 |
-
<div class="card-header">
|
| 12 |
-
<el-icon><Lightning /></el-icon>
|
| 13 |
-
<span>快速开发</span>
|
| 14 |
-
</div>
|
| 15 |
-
</template>
|
| 16 |
-
<p>基于 Vite 构建,提供极速的开发体验</p>
|
| 17 |
-
</el-card>
|
| 18 |
-
</el-col>
|
| 19 |
-
<el-col :span="8">
|
| 20 |
-
<el-card class="feature-card">
|
| 21 |
-
<template #header>
|
| 22 |
-
<div class="card-header">
|
| 23 |
-
<el-icon><Star /></el-icon>
|
| 24 |
-
<span>现代化</span>
|
| 25 |
-
</div>
|
| 26 |
-
</template>
|
| 27 |
-
<p>使用 Vue3 Composition API 和 setup 语法</p>
|
| 28 |
-
</el-card>
|
| 29 |
-
</el-col>
|
| 30 |
-
<el-col :span="8">
|
| 31 |
-
<el-card class="feature-card">
|
| 32 |
-
<template #header>
|
| 33 |
-
<div class="card-header">
|
| 34 |
-
<el-icon><Setting /></el-icon>
|
| 35 |
-
<span>完整配置</span>
|
| 36 |
-
</div>
|
| 37 |
-
</template>
|
| 38 |
-
<p>集成路由、状态管理、HTTP请求等完整解决方案</p>
|
| 39 |
-
</el-card>
|
| 40 |
-
</el-col>
|
| 41 |
-
</el-row>
|
| 42 |
-
</div>
|
| 43 |
-
</div>
|
| 44 |
-
|
| 45 |
-
<div class="demo-section">
|
| 46 |
-
<HelloWorld msg="Vue3 + Vite + Element Plus" />
|
| 47 |
-
</div>
|
| 48 |
-
</div>
|
| 49 |
-
</template>
|
| 50 |
-
|
| 51 |
-
<script setup>
|
| 52 |
-
import HelloWorld from '../components/HelloWorld.vue'
|
| 53 |
-
import { Lightning, Star, Setting } from '@element-plus/icons-vue'
|
| 54 |
-
</script>
|
| 55 |
-
|
| 56 |
-
<style lang="scss" scoped>
|
| 57 |
-
@use '@/styles/variables.scss' as *;
|
| 58 |
-
|
| 59 |
-
.home {
|
| 60 |
-
.welcome-section {
|
| 61 |
-
text-align: center;
|
| 62 |
-
margin-bottom: 3rem;
|
| 63 |
-
|
| 64 |
-
h1 {
|
| 65 |
-
color: #2c3e50;
|
| 66 |
-
margin-bottom: 1rem;
|
| 67 |
-
font-size: 2.5rem;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
p {
|
| 71 |
-
color: #7f8c8d;
|
| 72 |
-
font-size: 1.2rem;
|
| 73 |
-
margin-bottom: 2rem;
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
.features {
|
| 77 |
-
margin-top: 2rem;
|
| 78 |
-
|
| 79 |
-
.feature-card {
|
| 80 |
-
height: 200px;
|
| 81 |
-
|
| 82 |
-
.card-header {
|
| 83 |
-
display: flex;
|
| 84 |
-
align-items: center;
|
| 85 |
-
gap: 0.5rem;
|
| 86 |
-
font-weight: bold;
|
| 87 |
-
|
| 88 |
-
.el-icon {
|
| 89 |
-
color: #409eff;
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
p {
|
| 94 |
-
color: #666;
|
| 95 |
-
font-size: 1rem;
|
| 96 |
-
line-height: 1.6;
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
.demo-section {
|
| 103 |
-
margin-top: 3rem;
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sau_frontend/src/views/PublishCenter.vue
CHANGED
|
@@ -296,6 +296,15 @@
|
|
| 296 |
</el-radio-group>
|
| 297 |
</div>
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
<!-- 草稿选项 (仅在视频号可见) -->
|
| 300 |
<div v-if="tab.selectedPlatform === 2" class="draft-section">
|
| 301 |
<el-checkbox
|
|
@@ -411,27 +420,6 @@
|
|
| 411 |
</template>
|
| 412 |
</el-dialog>
|
| 413 |
|
| 414 |
-
<!-- 标签 (仅在抖音可见) -->
|
| 415 |
-
<div v-if="tab.selectedPlatform === 3" class="product-section">
|
| 416 |
-
<h3>商品链接</h3>
|
| 417 |
-
<el-input
|
| 418 |
-
v-model="tab.productTitle"
|
| 419 |
-
type="text"
|
| 420 |
-
:rows="1"
|
| 421 |
-
placeholder="请输入商品名称"
|
| 422 |
-
maxlength="200"
|
| 423 |
-
class="product-name-input"
|
| 424 |
-
/>
|
| 425 |
-
<el-input
|
| 426 |
-
v-model="tab.productLink"
|
| 427 |
-
type="text"
|
| 428 |
-
:rows="1"
|
| 429 |
-
placeholder="请输入商品链接"
|
| 430 |
-
maxlength="200"
|
| 431 |
-
class="product-link-input"
|
| 432 |
-
/>
|
| 433 |
-
</div>
|
| 434 |
-
|
| 435 |
<!-- 定时发布 -->
|
| 436 |
<div class="schedule-section">
|
| 437 |
<h3>定时发布</h3>
|
|
@@ -509,6 +497,7 @@ import { ElMessage } from 'element-plus'
|
|
| 509 |
import { useAccountStore } from '@/stores/account'
|
| 510 |
import { useAppStore } from '@/stores/app'
|
| 511 |
import { materialApi } from '@/api/material'
|
|
|
|
| 512 |
|
| 513 |
// API base URL
|
| 514 |
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
|
|
@@ -565,7 +554,8 @@ const defaultTabInit = {
|
|
| 565 |
startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
|
| 566 |
publishStatus: null, // 发布状态,包含message和type
|
| 567 |
publishing: false, // 发布状态,用于控制按钮loading效果
|
| 568 |
-
isDraft: false // 是否保存为草稿,仅视频号平台可见
|
|
|
|
| 569 |
}
|
| 570 |
|
| 571 |
// helper to create a fresh deep-copied tab from defaultTabInit
|
|
@@ -663,7 +653,6 @@ const handleUploadSuccess = (response, file, tab) => {
|
|
| 663 |
}))]
|
| 664 |
|
| 665 |
ElMessage.success('文件上传成功')
|
| 666 |
-
console.log('上传成功:', fileInfo)
|
| 667 |
} else {
|
| 668 |
ElMessage.error(response.msg || '上传失败')
|
| 669 |
}
|
|
@@ -672,7 +661,6 @@ const handleUploadSuccess = (response, file, tab) => {
|
|
| 672 |
// 处理文件上传失败
|
| 673 |
const handleUploadError = (error) => {
|
| 674 |
ElMessage.error('文件上传失败')
|
| 675 |
-
console.error('上传错误:', error)
|
| 676 |
}
|
| 677 |
|
| 678 |
// 删除已上传文件
|
|
@@ -774,102 +762,77 @@ const cancelPublish = (tab) => {
|
|
| 774 |
const confirmPublish = async (tab) => {
|
| 775 |
// 防止重复点击
|
| 776 |
if (tab.publishing) {
|
| 777 |
-
|
| 778 |
}
|
| 779 |
|
| 780 |
tab.publishing = true // 设置发布状态为进行中
|
| 781 |
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
ElMessage.error('请选择发布账号')
|
| 804 |
-
tab.publishing = false // 重置发布状态
|
| 805 |
-
reject(new Error('请选择发布账号'))
|
| 806 |
-
return
|
| 807 |
-
}
|
| 808 |
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
.
|
| 839 |
-
.
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
} else {
|
| 854 |
-
tab.publishStatus = {
|
| 855 |
-
message: `发布失败:${data.msg || '发布失败'}`,
|
| 856 |
-
type: 'error'
|
| 857 |
-
}
|
| 858 |
-
reject(new Error(data.msg || '发布失败'))
|
| 859 |
-
}
|
| 860 |
-
})
|
| 861 |
-
.catch(error => {
|
| 862 |
-
console.error('发布错误:', error)
|
| 863 |
-
tab.publishStatus = {
|
| 864 |
-
message: '发布失败,请检查网络连接',
|
| 865 |
-
type: 'error'
|
| 866 |
-
}
|
| 867 |
-
reject(error)
|
| 868 |
-
})
|
| 869 |
-
.finally(() => {
|
| 870 |
-
tab.publishing = false // 重置发布状态
|
| 871 |
-
})
|
| 872 |
-
})
|
| 873 |
}
|
| 874 |
|
| 875 |
// 显示上传选项
|
|
@@ -1324,6 +1287,15 @@ const batchPublish = async () => {
|
|
| 1324 |
margin: 10px 0;
|
| 1325 |
}
|
| 1326 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1327 |
}
|
| 1328 |
}
|
| 1329 |
}
|
|
|
|
| 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
|
|
|
|
| 420 |
</template>
|
| 421 |
</el-dialog>
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
<!-- 定时发布 -->
|
| 424 |
<div class="schedule-section">
|
| 425 |
<h3>定时发布</h3>
|
|
|
|
| 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'
|
|
|
|
| 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
|
|
|
|
| 653 |
}))]
|
| 654 |
|
| 655 |
ElMessage.success('文件上传成功')
|
|
|
|
| 656 |
} else {
|
| 657 |
ElMessage.error(response.msg || '上传失败')
|
| 658 |
}
|
|
|
|
| 661 |
// 处理文件上传失败
|
| 662 |
const handleUploadError = (error) => {
|
| 663 |
ElMessage.error('文件上传失败')
|
|
|
|
| 664 |
}
|
| 665 |
|
| 666 |
// 删除已上传文件
|
|
|
|
| 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 |
// 显示上传选项
|
|
|
|
| 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 |
}
|
utils/browser_hook.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from conf import LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH
|
| 2 |
+
|
| 3 |
+
def get_browser_options():
|
| 4 |
+
options = {
|
| 5 |
+
'headless': LOCAL_CHROME_HEADLESS,
|
| 6 |
+
'args': [
|
| 7 |
+
'--disable-blink-features=AutomationControlled',
|
| 8 |
+
'--lang=zh-CN',
|
| 9 |
+
'--disable-infobars',
|
| 10 |
+
'--start-maximized',
|
| 11 |
+
'--no-sandbox',
|
| 12 |
+
'--disable-web-security'
|
| 13 |
+
]
|
| 14 |
+
}
|
| 15 |
+
if LOCAL_CHROME_PATH:
|
| 16 |
+
options['executable_path'] = LOCAL_CHROME_PATH
|
| 17 |
+
return options
|