dreammis commited on
Commit
0cc7405
·
2 Parent(s): f5e7a3e30ec71b

Merge branch 'main' into main

Browse files
CLAUDE.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Project Overview
2
+
3
+ This project, `social-auto-upload`, is a powerful automation tool designed to help content creators and operators efficiently publish video content to multiple domestic and international mainstream social media platforms in one click. The project implements video upload, scheduled release and other functions for platforms such as `Douyin`, `Bilibili`, `Xiaohongshu`, `Kuaishou`, `WeChat Channel`, `Baijiahao` and `TikTok`.
4
+
5
+ The project consists of a Python backend and a Vue.js frontend.
6
+
7
+ **Backend:**
8
+
9
+ * Framework: Flask
10
+ * Core Functionality:
11
+ * Handles file uploads and management.
12
+ * Interacts with a SQLite database to store information about files and user accounts.
13
+ * Uses `playwright` for browser automation to interact with social media platforms.
14
+ * Provides a RESTful API for the frontend to consume.
15
+ * Uses Server-Sent Events (SSE) for real-time communication with the frontend during the login process.
16
+
17
+ **Frontend:**
18
+
19
+ * Framework: Vue.js
20
+ * Build Tool: Vite
21
+ * UI Library: Element Plus
22
+ * State Management: Pinia
23
+ * Routing: Vue Router
24
+ * Core Functionality:
25
+ * Provides a web interface for managing social media accounts, video files, and publishing videos.
26
+ * Communicates with the backend via a RESTful API.
27
+
28
+ **Command-line Interface:**
29
+
30
+ The project also provides a command-line interface (CLI) for users who prefer to work from the terminal. The CLI supports two main actions:
31
+
32
+ * `login`: To log in to a social media platform.
33
+ * `upload`: To upload a video to a social media platform, with an option to schedule the upload.
34
+
35
+ ## Building and Running
36
+
37
+ ### Backend
38
+
39
+ 1. **Install dependencies:**
40
+ ```bash
41
+ pip install -r requirements.txt
42
+ ```
43
+
44
+ 2. **Install Playwright browser drivers:**
45
+ ```bash
46
+ playwright install chromium
47
+ ```
48
+
49
+ 3. **Initialize the database:**
50
+ ```bash
51
+ python db/createTable.py
52
+ ```
53
+
54
+ 4. **Run the backend server:**
55
+ ```bash
56
+ python sau_backend.py
57
+ ```
58
+ The backend server will start on `http://localhost:5409`.
59
+
60
+ ### Frontend
61
+
62
+ 1. **Navigate to the frontend directory:**
63
+ ```bash
64
+ cd sau_frontend
65
+ ```
66
+
67
+ 2. **Install dependencies:**
68
+ ```bash
69
+ npm install
70
+ ```
71
+
72
+ 3. **Run the development server:**
73
+ ```bash
74
+ npm run dev
75
+ ```
76
+ The frontend development server will start on `http://localhost:5173`.
77
+
78
+ ### Command-line Interface
79
+
80
+ To use the CLI, you can run the `cli_main.py` script with the appropriate arguments.
81
+
82
+ **Login:**
83
+
84
+ ```bash
85
+ python cli_main.py <platform> <account_name> login
86
+ ```
87
+
88
+ **Upload:**
89
+
90
+ ```bash
91
+ python cli_main.py <platform> <account_name> upload <video_file> [-pt {0,1}] [-t YYYY-MM-DD HH:MM]
92
+ ```
93
+
94
+ ## Development Conventions
95
+
96
+ * The backend code is located in the root directory and the `myUtils` and `uploader` directories.
97
+ * The frontend code is located in the `sau_frontend` directory.
98
+ * The project uses a SQLite database for data storage. The database file is located at `db/database.db`.
99
+ * The `conf.example.py` file should be copied to `conf.py` and configured with the appropriate settings.
100
+ * The `requirements.txt` file lists the Python dependencies.
101
+ * The `package.json` file in the `sau_frontend` directory lists the frontend dependencies.
myUtils/postVideo.py CHANGED
@@ -10,7 +10,7 @@ from utils.constant import TencentZoneTypes
10
  from utils.files_times import generate_schedule_time_next_day
11
 
12
 
13
- def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0):
14
  # 生成文件的完整路径
15
  account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
16
  files = [Path(BASE_DIR / "videoFile" / file) for file in files]
@@ -25,7 +25,7 @@ def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.L
25
  print(f"视频文件名:{file}")
26
  print(f"标题:{title}")
27
  print(f"Hashtag:{tags}")
28
- app = TencentVideo(title, str(file), tags, publish_datetimes[index], cookie, category)
29
  asyncio.run(app.main(), debug=False)
30
 
31
 
 
10
  from utils.files_times import generate_schedule_time_next_day
11
 
12
 
13
+ def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0, is_draft=False):
14
  # 生成文件的完整路径
15
  account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
16
  files = [Path(BASE_DIR / "videoFile" / file) for file in files]
 
25
  print(f"视频文件名:{file}")
26
  print(f"标题:{title}")
27
  print(f"Hashtag:{tags}")
28
+ app = TencentVideo(title, str(file), tags, publish_datetimes[index], cookie, category, is_draft)
29
  asyncio.run(app.main(), debug=False)
30
 
31
 
sau_backend.py CHANGED
@@ -139,9 +139,10 @@ def upload_save():
139
  }), 200
140
 
141
  except Exception as e:
 
142
  return jsonify({
143
  "code": 500,
144
- "msg": str("upload failed!"),
145
  "data": None
146
  }), 500
147
 
@@ -157,14 +158,26 @@ def get_all_files():
157
  cursor.execute("SELECT * FROM file_records")
158
  rows = cursor.fetchall()
159
 
160
- # 将结果转为字典列表
161
- data = [dict(row) for row in rows]
162
-
163
- return jsonify({
164
- "code": 200,
165
- "msg": "success",
166
- "data": data
167
- }), 200
 
 
 
 
 
 
 
 
 
 
 
 
168
  except Exception as e:
169
  return jsonify({
170
  "code": 500,
@@ -173,6 +186,37 @@ def get_all_files():
173
  }), 500
174
 
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  @app.route("/getValidAccounts",methods=['GET'])
177
  async def getValidAccounts():
178
  with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
@@ -234,6 +278,18 @@ def delete_file():
234
 
235
  record = dict(record)
236
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  # 删除数据库记录
238
  cursor.execute("DELETE FROM file_records WHERE id = ?", (file_id,))
239
  conn.commit()
@@ -338,6 +394,7 @@ def postVideo():
338
  productLink = data.get('productLink', '')
339
  productTitle = data.get('productTitle', '')
340
  thumbnail_path = data.get('thumbnail', '')
 
341
 
342
  videos_per_day = data.get('videosPerDay')
343
  daily_times = data.get('dailyTimes')
@@ -351,7 +408,7 @@ def postVideo():
351
  start_days)
352
  case 2:
353
  post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
354
- start_days)
355
  case 3:
356
  post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
357
  start_days, thumbnail_path, productLink, productTitle)
@@ -450,6 +507,127 @@ def postVideoBatch():
450
  "data": None
451
  }), 200
452
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  # 包装函数:在线程中运行异步函数
454
  def run_async_function(type,id,status_queue):
455
  match type:
 
139
  }), 200
140
 
141
  except Exception as e:
142
+ print(f"Upload failed: {e}")
143
  return jsonify({
144
  "code": 500,
145
+ "msg": f"upload failed: {e}",
146
  "data": None
147
  }), 500
148
 
 
158
  cursor.execute("SELECT * FROM file_records")
159
  rows = cursor.fetchall()
160
 
161
+ # 将结果转为字典列表,并提取UUID
162
+ data = []
163
+ for row in rows:
164
+ row_dict = dict(row)
165
+ # 从 file_path 中提取 UUID (文件名的第一部分,下划线前)
166
+ if row_dict.get('file_path'):
167
+ file_path_parts = row_dict['file_path'].split('_', 1) # 只分割第一个下划线
168
+ if len(file_path_parts) > 0:
169
+ row_dict['uuid'] = file_path_parts[0] # UUID 部分
170
+ else:
171
+ row_dict['uuid'] = ''
172
+ else:
173
+ row_dict['uuid'] = ''
174
+ data.append(row_dict)
175
+
176
+ return jsonify({
177
+ "code": 200,
178
+ "msg": "success",
179
+ "data": data
180
+ }), 200
181
  except Exception as e:
182
  return jsonify({
183
  "code": 500,
 
186
  }), 500
187
 
188
 
189
+ @app.route("/getAccounts", methods=['GET'])
190
+ def getAccounts():
191
+ """快速获取所有账号信息,不进行cookie验证"""
192
+ try:
193
+ with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
194
+ conn.row_factory = sqlite3.Row
195
+ cursor = conn.cursor()
196
+ cursor.execute('''
197
+ SELECT * FROM user_info''')
198
+ rows = cursor.fetchall()
199
+ rows_list = [list(row) for row in rows]
200
+
201
+ print("\n📋 当前数据表内容(快速获取):")
202
+ for row in rows:
203
+ print(row)
204
+
205
+ return jsonify(
206
+ {
207
+ "code": 200,
208
+ "msg": None,
209
+ "data": rows_list
210
+ }), 200
211
+ except Exception as e:
212
+ print(f"获取账号列表时出错: {str(e)}")
213
+ return jsonify({
214
+ "code": 500,
215
+ "msg": f"获取账号列表失败: {str(e)}",
216
+ "data": None
217
+ }), 500
218
+
219
+
220
  @app.route("/getValidAccounts",methods=['GET'])
221
  async def getValidAccounts():
222
  with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
 
278
 
279
  record = dict(record)
280
 
281
+ # 获取文件路径并删除实际文件
282
+ file_path = Path(BASE_DIR / "videoFile" / record['file_path'])
283
+ if file_path.exists():
284
+ try:
285
+ file_path.unlink() # 删除文件
286
+ print(f"✅ 实际文件已删除: {file_path}")
287
+ except Exception as e:
288
+ print(f"⚠️ 删除实际文件失败: {e}")
289
+ # 即使删除文件失败,也要继续删除数据库记录,避免数据不一致
290
+ else:
291
+ print(f"⚠️ 实际文件不存在: {file_path}")
292
+
293
  # 删除数据库记录
294
  cursor.execute("DELETE FROM file_records WHERE id = ?", (file_id,))
295
  conn.commit()
 
394
  productLink = data.get('productLink', '')
395
  productTitle = data.get('productTitle', '')
396
  thumbnail_path = data.get('thumbnail', '')
397
+ is_draft = data.get('isDraft', False) # 新增参数:是否保存为草稿
398
 
399
  videos_per_day = data.get('videosPerDay')
400
  daily_times = data.get('dailyTimes')
 
408
  start_days)
409
  case 2:
410
  post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
411
+ start_days, is_draft)
412
  case 3:
413
  post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
414
  start_days, thumbnail_path, productLink, productTitle)
 
507
  "data": None
508
  }), 200
509
 
510
+ # Cookie文件上传API
511
+ @app.route('/uploadCookie', methods=['POST'])
512
+ def upload_cookie():
513
+ try:
514
+ if 'file' not in request.files:
515
+ return jsonify({
516
+ "code": 500,
517
+ "msg": "没有找到Cookie文件",
518
+ "data": None
519
+ }), 400
520
+
521
+ file = request.files['file']
522
+ if file.filename == '':
523
+ return jsonify({
524
+ "code": 500,
525
+ "msg": "Cookie文件名不能为空",
526
+ "data": None
527
+ }), 400
528
+
529
+ if not file.filename.endswith('.json'):
530
+ return jsonify({
531
+ "code": 500,
532
+ "msg": "Cookie文件必须是JSON格式",
533
+ "data": None
534
+ }), 400
535
+
536
+ # 获取账号信息
537
+ account_id = request.form.get('id')
538
+ platform = request.form.get('platform')
539
+
540
+ if not account_id or not platform:
541
+ return jsonify({
542
+ "code": 500,
543
+ "msg": "缺少账号ID或平台信息",
544
+ "data": None
545
+ }), 400
546
+
547
+ # 从数据库获取账号的文件路径
548
+ with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
549
+ conn.row_factory = sqlite3.Row
550
+ cursor = conn.cursor()
551
+ cursor.execute('SELECT filePath FROM user_info WHERE id = ?', (account_id,))
552
+ result = cursor.fetchone()
553
+
554
+ if not result:
555
+ return jsonify({
556
+ "code": 500,
557
+ "msg": "账号不存在",
558
+ "data": None
559
+ }), 404
560
+
561
+ # 保存上传的Cookie文件到对应路径
562
+ cookie_file_path = Path(BASE_DIR / "cookiesFile" / result['filePath'])
563
+ cookie_file_path.parent.mkdir(parents=True, exist_ok=True)
564
+
565
+ file.save(str(cookie_file_path))
566
+
567
+ # 更新数据库中的账号信息(可选,比如更新更新时间)
568
+ # 这里可以根据需要添加额外的处理逻辑
569
+
570
+ return jsonify({
571
+ "code": 200,
572
+ "msg": "Cookie文件上传成功",
573
+ "data": None
574
+ }), 200
575
+
576
+ except Exception as e:
577
+ print(f"上传Cookie文件时出错: {str(e)}")
578
+ return jsonify({
579
+ "code": 500,
580
+ "msg": f"上传Cookie文件失败: {str(e)}",
581
+ "data": None
582
+ }), 500
583
+
584
+
585
+ # Cookie文件下载API
586
+ @app.route('/downloadCookie', methods=['GET'])
587
+ def download_cookie():
588
+ try:
589
+ file_path = request.args.get('filePath')
590
+ if not file_path:
591
+ return jsonify({
592
+ "code": 500,
593
+ "msg": "缺少文件路径参数",
594
+ "data": None
595
+ }), 400
596
+
597
+ # 验证文件路径的安全性,防止路径遍历攻击
598
+ cookie_file_path = Path(BASE_DIR / "cookiesFile" / file_path).resolve()
599
+ base_path = Path(BASE_DIR / "cookiesFile").resolve()
600
+
601
+ if not cookie_file_path.is_relative_to(base_path):
602
+ return jsonify({
603
+ "code": 500,
604
+ "msg": "非法文件路径",
605
+ "data": None
606
+ }), 400
607
+
608
+ if not cookie_file_path.exists():
609
+ return jsonify({
610
+ "code": 500,
611
+ "msg": "Cookie文件不存在",
612
+ "data": None
613
+ }), 404
614
+
615
+ # 返回文件
616
+ return send_from_directory(
617
+ directory=str(cookie_file_path.parent),
618
+ path=cookie_file_path.name,
619
+ as_attachment=True
620
+ )
621
+
622
+ except Exception as e:
623
+ print(f"下载Cookie文件时出错: {str(e)}")
624
+ return jsonify({
625
+ "code": 500,
626
+ "msg": f"下载Cookie文件失败: {str(e)}",
627
+ "data": None
628
+ }), 500
629
+
630
+
631
  # 包装函数:在线程中运行异步函数
632
  def run_async_function(type,id,status_queue):
633
  match type:
sau_frontend/.env.development CHANGED
@@ -2,7 +2,7 @@
2
  NODE_ENV=development
3
 
4
  # API 基础地址
5
- VITE_API_BASE_URL=http://localhost:5409
6
 
7
  # 应用端口
8
  VITE_PORT=5173
 
2
  NODE_ENV=development
3
 
4
  # API 基础地址
5
+ VITE_API_BASE_URL=/api
6
 
7
  # 应用端口
8
  VITE_PORT=5173
sau_frontend/src/api/account.js CHANGED
@@ -2,21 +2,26 @@ import { http } from '@/utils/request'
2
 
3
  // 账号管理相关API
4
  export const accountApi = {
5
- // 获取有效账号列表
6
  getValidAccounts() {
7
  return http.get('/getValidAccounts')
8
  },
9
-
 
 
 
 
 
10
  // 添加账号
11
  addAccount(data) {
12
  return http.post('/account', data)
13
  },
14
-
15
  // 更新账号
16
  updateAccount(data) {
17
  return http.post('/updateUserinfo', data)
18
  },
19
-
20
  // 删除账号
21
  deleteAccount(id) {
22
  return http.get(`/deleteAccount?id=${id}`)
 
2
 
3
  // 账号管理相关API
4
  export const accountApi = {
5
+ // 获取有效账号列表(带验证)
6
  getValidAccounts() {
7
  return http.get('/getValidAccounts')
8
  },
9
+
10
+ // 获取账号列表(不带验证,快速加载)
11
+ getAccounts() {
12
+ return http.get('/getAccounts')
13
+ },
14
+
15
  // 添加账号
16
  addAccount(data) {
17
  return http.post('/account', data)
18
  },
19
+
20
  // 更新账号
21
  updateAccount(data) {
22
  return http.post('/updateUserinfo', data)
23
  },
24
+
25
  // 删除账号
26
  deleteAccount(id) {
27
  return http.get(`/deleteAccount?id=${id}`)
sau_frontend/src/api/material.js CHANGED
@@ -8,9 +8,9 @@ export const materialApi = {
8
  },
9
 
10
  // 上传素材
11
- uploadMaterial: (formData) => {
12
  // 使用http.upload方法,它已经配置了正确的Content-Type
13
- return http.upload('/uploadSave', formData)
14
  },
15
 
16
  // 删除素材
 
8
  },
9
 
10
  // 上传素材
11
+ uploadMaterial: (formData, onUploadProgress) => {
12
  // 使用http.upload方法,它已经配置了正确的Content-Type
13
+ return http.upload('/uploadSave', formData, onUploadProgress)
14
  },
15
 
16
  // 删除素材
sau_frontend/src/utils/request.js CHANGED
@@ -87,11 +87,12 @@ export const http = {
87
  return request.delete(url, { params })
88
  },
89
 
90
- upload(url, formData) {
91
  return request.post(url, formData, {
92
  headers: {
93
  'Content-Type': 'multipart/form-data'
94
- }
 
95
  })
96
  }
97
  }
 
87
  return request.delete(url, { params })
88
  },
89
 
90
+ upload(url, formData, onUploadProgress) {
91
  return request.post(url, formData, {
92
  headers: {
93
  'Content-Type': 'multipart/form-data'
94
+ },
95
+ onUploadProgress
96
  })
97
  }
98
  }
sau_frontend/src/views/AccountManagement.vue CHANGED
@@ -30,7 +30,7 @@
30
  <el-table :data="filteredAccounts" style="width: 100%">
31
  <el-table-column label="头像" width="80">
32
  <template #default="scope">
33
- <el-avatar :src="scope.row.avatar" :size="40" />
34
  </template>
35
  </el-table-column>
36
  <el-table-column prop="name" label="名称" width="180" />
@@ -47,9 +47,14 @@
47
  <el-table-column prop="status" label="状态">
48
  <template #default="scope">
49
  <el-tag
50
- :type="scope.row.status === '正常' ? 'success' : 'danger'"
51
  effect="plain"
 
 
52
  >
 
 
 
53
  {{ scope.row.status }}
54
  </el-tag>
55
  </template>
@@ -57,6 +62,8 @@
57
  <el-table-column label="操作">
58
  <template #default="scope">
59
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
 
 
60
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
61
  </template>
62
  </el-table-column>
@@ -93,7 +100,7 @@
93
  <el-table :data="filteredKuaishouAccounts" style="width: 100%">
94
  <el-table-column label="头像" width="80">
95
  <template #default="scope">
96
- <el-avatar :src="scope.row.avatar" :size="40" />
97
  </template>
98
  </el-table-column>
99
  <el-table-column prop="name" label="名称" width="180" />
@@ -110,9 +117,14 @@
110
  <el-table-column prop="status" label="状态">
111
  <template #default="scope">
112
  <el-tag
113
- :type="scope.row.status === '正常' ? 'success' : 'danger'"
114
  effect="plain"
 
 
115
  >
 
 
 
116
  {{ scope.row.status }}
117
  </el-tag>
118
  </template>
@@ -120,6 +132,8 @@
120
  <el-table-column label="操作">
121
  <template #default="scope">
122
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
 
 
123
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
124
  </template>
125
  </el-table-column>
@@ -156,7 +170,7 @@
156
  <el-table :data="filteredDouyinAccounts" style="width: 100%">
157
  <el-table-column label="头像" width="80">
158
  <template #default="scope">
159
- <el-avatar :src="scope.row.avatar" :size="40" />
160
  </template>
161
  </el-table-column>
162
  <el-table-column prop="name" label="名称" width="180" />
@@ -173,9 +187,14 @@
173
  <el-table-column prop="status" label="状态">
174
  <template #default="scope">
175
  <el-tag
176
- :type="scope.row.status === '正常' ? 'success' : 'danger'"
177
  effect="plain"
 
 
178
  >
 
 
 
179
  {{ scope.row.status }}
180
  </el-tag>
181
  </template>
@@ -183,6 +202,8 @@
183
  <el-table-column label="操作">
184
  <template #default="scope">
185
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
 
 
186
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
187
  </template>
188
  </el-table-column>
@@ -219,7 +240,7 @@
219
  <el-table :data="filteredChannelsAccounts" style="width: 100%">
220
  <el-table-column label="头像" width="80">
221
  <template #default="scope">
222
- <el-avatar :src="scope.row.avatar" :size="40" />
223
  </template>
224
  </el-table-column>
225
  <el-table-column prop="name" label="名称" width="180" />
@@ -236,9 +257,14 @@
236
  <el-table-column prop="status" label="状态">
237
  <template #default="scope">
238
  <el-tag
239
- :type="scope.row.status === '正常' ? 'success' : 'danger'"
240
  effect="plain"
 
 
241
  >
 
 
 
242
  {{ scope.row.status }}
243
  </el-tag>
244
  </template>
@@ -246,6 +272,8 @@
246
  <el-table-column label="操作">
247
  <template #default="scope">
248
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
 
 
249
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
250
  </template>
251
  </el-table-column>
@@ -282,7 +310,7 @@
282
  <el-table :data="filteredXiaohongshuAccounts" style="width: 100%">
283
  <el-table-column label="头像" width="80">
284
  <template #default="scope">
285
- <el-avatar :src="scope.row.avatar" :size="40" />
286
  </template>
287
  </el-table-column>
288
  <el-table-column prop="name" label="名称" width="180" />
@@ -299,9 +327,14 @@
299
  <el-table-column prop="status" label="状态">
300
  <template #default="scope">
301
  <el-tag
302
- :type="scope.row.status === '正常' ? 'success' : 'danger'"
303
  effect="plain"
 
 
304
  >
 
 
 
305
  {{ scope.row.status }}
306
  </el-tag>
307
  </template>
@@ -309,6 +342,8 @@
309
  <el-table-column label="操作">
310
  <template #default="scope">
311
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
 
 
312
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
313
  </template>
314
  </el-table-column>
@@ -393,7 +428,7 @@
393
 
394
  <script setup>
395
  import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
396
- import { Refresh, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
397
  import { ElMessage, ElMessageBox } from 'element-plus'
398
  import { accountApi } from '@/api/account'
399
  import { useAccountStore } from '@/stores/account'
@@ -410,12 +445,31 @@ const activeTab = ref('all')
410
  // 搜索关键词
411
  const searchKeyword = ref('')
412
 
413
- // 获取账号数据
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  const fetchAccounts = async () => {
415
  if (appStore.isAccountRefreshing) return
416
-
417
  appStore.setAccountRefreshing(true)
418
-
419
  try {
420
  const res = await accountApi.getValidAccounts()
421
  if (res.code === 200 && res.data) {
@@ -436,12 +490,30 @@ const fetchAccounts = async () => {
436
  }
437
  }
438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  // 页面加载时获取账号数据
440
  onMounted(() => {
441
- // 只有第一次进入时才获取数据
442
- if (appStore.isFirstTimeAccountManagement) {
443
- fetchAccounts()
444
- }
 
 
 
445
  })
446
 
447
  // 获取平台标签类型
@@ -455,10 +527,34 @@ const getPlatformTagType = (platform) => {
455
  return typeMap[platform] || 'info'
456
  }
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  // 过滤后的账号列表
459
  const filteredAccounts = computed(() => {
460
  if (!searchKeyword.value) return accountStore.accounts
461
- return accountStore.accounts.filter(account =>
462
  account.name.includes(searchKeyword.value)
463
  )
464
  })
@@ -528,7 +624,12 @@ const handleAddAccount = () => {
528
  // 编辑账号
529
  const handleEdit = (row) => {
530
  dialogType.value = 'edit'
531
- Object.assign(accountForm, { ...row })
 
 
 
 
 
532
  dialogVisible.value = true
533
  }
534
 
@@ -547,7 +648,7 @@ const handleDelete = (row) => {
547
  try {
548
  // 调用API删除账号
549
  const response = await accountApi.deleteAccount(row.id)
550
-
551
  if (response.code === 200) {
552
  // 从状态管理中删除账号
553
  accountStore.deleteAccount(row.id)
@@ -568,6 +669,108 @@ const handleDelete = (row) => {
568
  })
569
  }
570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  // SSE事件源对象
572
  let eventSource = null
573
 
@@ -583,12 +786,12 @@ const closeSSEConnection = () => {
583
  const connectSSE = (platform, name) => {
584
  // 关闭可能存在的连接
585
  closeSSEConnection()
586
-
587
  // 设置连接状态
588
  sseConnecting.value = true
589
  qrCodeData.value = ''
590
  loginStatus.value = ''
591
-
592
  // 获取平台类型编号
593
  const platformTypeMap = {
594
  '小红书': '1',
@@ -596,20 +799,20 @@ const connectSSE = (platform, name) => {
596
  '抖音': '3',
597
  '快手': '4'
598
  }
599
-
600
  const type = platformTypeMap[platform] || '1'
601
-
602
  // 创建SSE连接
603
  const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
604
  const url = `${baseUrl}/login?type=${type}&id=${encodeURIComponent(name)}`
605
-
606
  eventSource = new EventSource(url)
607
-
608
  // 监听消息
609
  eventSource.onmessage = (event) => {
610
  const data = event.data
611
  console.log('SSE消息:', data)
612
-
613
  // 如果还没有二维码数据,且数据长度较长,认为是二维码
614
  if (!qrCodeData.value && data.length > 100) {
615
  try {
@@ -625,30 +828,32 @@ const connectSSE = (platform, name) => {
625
  } catch (error) {
626
  console.error('处理二维码数据出错:', error)
627
  }
628
- }
629
  // 如果收到状态码
630
  else if (data === '200' || data === '500') {
631
  loginStatus.value = data
632
-
633
  // 如果登录成功
634
  if (data === '200') {
635
  setTimeout(() => {
636
  // 关闭连接
637
  closeSSEConnection()
638
-
639
  // 1秒后关闭对话框并开始刷新
640
  setTimeout(() => {
641
  dialogVisible.value = false
642
  sseConnecting.value = false
643
- ElMessage.success('账号添加成功')
644
-
 
 
645
  // 显示更新账号信息提示
646
  ElMessage({
647
  type: 'info',
648
  message: '正在同步账号信息...',
649
  duration: 0
650
  })
651
-
652
  // 触发刷新操作
653
  fetchAccounts().then(() => {
654
  // 刷新完成后关闭提示
@@ -660,7 +865,7 @@ const connectSSE = (platform, name) => {
660
  } else {
661
  // 登录失败,关闭连接
662
  closeSSEConnection()
663
-
664
  // 2秒后重置状态,允许重试
665
  setTimeout(() => {
666
  sseConnecting.value = false
@@ -670,7 +875,7 @@ const connectSSE = (platform, name) => {
670
  }
671
  }
672
  }
673
-
674
  // 监听错误
675
  eventSource.onerror = (error) => {
676
  console.error('SSE连接错误:', error)
@@ -690,14 +895,29 @@ const submitAccountForm = () => {
690
  } else {
691
  // 编辑账号逻辑
692
  try {
 
 
 
 
 
 
 
 
 
693
  const res = await accountApi.updateAccount({
694
  id: accountForm.id,
695
- type: Number(accountForm.platform === '快手' ? 1 : accountForm.platform === '抖音' ? 2 : accountForm.platform === '视频号' ? 3 : 4),
696
  userName: accountForm.name
697
  })
698
  if (res.code === 200) {
699
  // 更新状态管理中的账号
700
- accountStore.updateAccount(accountForm.id, accountForm)
 
 
 
 
 
 
701
  ElMessage.success('更新成功')
702
  dialogVisible.value = false
703
  // 刷新账号列表
@@ -785,6 +1005,16 @@ onBeforeUnmount(() => {
785
  }
786
 
787
  // 二维码容器样式
 
 
 
 
 
 
 
 
 
 
788
  .qrcode-container {
789
  margin-top: 20px;
790
  display: flex;
 
30
  <el-table :data="filteredAccounts" style="width: 100%">
31
  <el-table-column label="头像" width="80">
32
  <template #default="scope">
33
+ <el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
34
  </template>
35
  </el-table-column>
36
  <el-table-column prop="name" label="名称" width="180" />
 
47
  <el-table-column prop="status" label="状态">
48
  <template #default="scope">
49
  <el-tag
50
+ :type="getStatusTagType(scope.row.status)"
51
  effect="plain"
52
+ :class="{'clickable-status': isStatusClickable(scope.row.status)}"
53
+ @click="handleStatusClick(scope.row)"
54
  >
55
+ <el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
56
+ <Loading />
57
+ </el-icon>
58
  {{ scope.row.status }}
59
  </el-tag>
60
  </template>
 
62
  <el-table-column label="操作">
63
  <template #default="scope">
64
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
65
+ <el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
66
+ <el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</el-button>
67
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
68
  </template>
69
  </el-table-column>
 
100
  <el-table :data="filteredKuaishouAccounts" style="width: 100%">
101
  <el-table-column label="头像" width="80">
102
  <template #default="scope">
103
+ <el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
104
  </template>
105
  </el-table-column>
106
  <el-table-column prop="name" label="名称" width="180" />
 
117
  <el-table-column prop="status" label="状态">
118
  <template #default="scope">
119
  <el-tag
120
+ :type="getStatusTagType(scope.row.status)"
121
  effect="plain"
122
+ :class="{'clickable-status': isStatusClickable(scope.row.status)}"
123
+ @click="handleStatusClick(scope.row)"
124
  >
125
+ <el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
126
+ <Loading />
127
+ </el-icon>
128
  {{ scope.row.status }}
129
  </el-tag>
130
  </template>
 
132
  <el-table-column label="操作">
133
  <template #default="scope">
134
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
135
+ <el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
136
+ <el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</el-button>
137
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
138
  </template>
139
  </el-table-column>
 
170
  <el-table :data="filteredDouyinAccounts" style="width: 100%">
171
  <el-table-column label="头像" width="80">
172
  <template #default="scope">
173
+ <el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
174
  </template>
175
  </el-table-column>
176
  <el-table-column prop="name" label="名称" width="180" />
 
187
  <el-table-column prop="status" label="状态">
188
  <template #default="scope">
189
  <el-tag
190
+ :type="getStatusTagType(scope.row.status)"
191
  effect="plain"
192
+ :class="{'clickable-status': isStatusClickable(scope.row.status)}"
193
+ @click="handleStatusClick(scope.row)"
194
  >
195
+ <el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
196
+ <Loading />
197
+ </el-icon>
198
  {{ scope.row.status }}
199
  </el-tag>
200
  </template>
 
202
  <el-table-column label="操作">
203
  <template #default="scope">
204
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
205
+ <el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
206
+ <el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</el-button>
207
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
208
  </template>
209
  </el-table-column>
 
240
  <el-table :data="filteredChannelsAccounts" style="width: 100%">
241
  <el-table-column label="头像" width="80">
242
  <template #default="scope">
243
+ <el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
244
  </template>
245
  </el-table-column>
246
  <el-table-column prop="name" label="名称" width="180" />
 
257
  <el-table-column prop="status" label="状态">
258
  <template #default="scope">
259
  <el-tag
260
+ :type="getStatusTagType(scope.row.status)"
261
  effect="plain"
262
+ :class="{'clickable-status': isStatusClickable(scope.row.status)}"
263
+ @click="handleStatusClick(scope.row)"
264
  >
265
+ <el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
266
+ <Loading />
267
+ </el-icon>
268
  {{ scope.row.status }}
269
  </el-tag>
270
  </template>
 
272
  <el-table-column label="操作">
273
  <template #default="scope">
274
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
275
+ <el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
276
+ <el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</el-button>
277
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
278
  </template>
279
  </el-table-column>
 
310
  <el-table :data="filteredXiaohongshuAccounts" style="width: 100%">
311
  <el-table-column label="头像" width="80">
312
  <template #default="scope">
313
+ <el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
314
  </template>
315
  </el-table-column>
316
  <el-table-column prop="name" label="名称" width="180" />
 
327
  <el-table-column prop="status" label="状态">
328
  <template #default="scope">
329
  <el-tag
330
+ :type="getStatusTagType(scope.row.status)"
331
  effect="plain"
332
+ :class="{'clickable-status': isStatusClickable(scope.row.status)}"
333
+ @click="handleStatusClick(scope.row)"
334
  >
335
+ <el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
336
+ <Loading />
337
+ </el-icon>
338
  {{ scope.row.status }}
339
  </el-tag>
340
  </template>
 
342
  <el-table-column label="操作">
343
  <template #default="scope">
344
  <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
345
+ <el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
346
+ <el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</el-button>
347
  <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
348
  </template>
349
  </el-table-column>
 
428
 
429
  <script setup>
430
  import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
431
+ import { Refresh, CircleCheckFilled, CircleCloseFilled, Download, Upload, Loading } from '@element-plus/icons-vue'
432
  import { ElMessage, ElMessageBox } from 'element-plus'
433
  import { accountApi } from '@/api/account'
434
  import { useAccountStore } from '@/stores/account'
 
445
  // 搜索关键词
446
  const searchKeyword = ref('')
447
 
448
+ // 获取账号数据(快速,不验证)
449
+ const fetchAccountsQuick = async () => {
450
+ try {
451
+ const res = await accountApi.getAccounts()
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);
461
+ }
462
+ } catch (error) {
463
+ console.error('快速获取账号数据失败:', error)
464
+ }
465
+ }
466
+
467
+ // 获取账号数据(带验证)
468
  const fetchAccounts = async () => {
469
  if (appStore.isAccountRefreshing) return
470
+
471
  appStore.setAccountRefreshing(true)
472
+
473
  try {
474
  const res = await accountApi.getValidAccounts()
475
  if (res.code === 200 && res.data) {
 
490
  }
491
  }
492
 
493
+ // 后台验证所有账号(优化版本,使用setTimeout避免阻塞UI)
494
+ const validateAllAccountsInBackground = async () => {
495
+ // 使用setTimeout将验证过程放在下一个事件循环,避免阻塞UI
496
+ setTimeout(async () => {
497
+ try {
498
+ const res = await accountApi.getValidAccounts()
499
+ if (res.code === 200 && res.data) {
500
+ accountStore.setAccounts(res.data)
501
+ }
502
+ } catch (error) {
503
+ console.error('后台验证账号失败:', error)
504
+ }
505
+ }, 0)
506
+ }
507
+
508
  // 页面加载时获取账号数据
509
  onMounted(() => {
510
+ // 快速获取账号列表(不验证),立即显示
511
+ fetchAccountsQuick()
512
+
513
+ // 在后台验证所有账号
514
+ setTimeout(() => {
515
+ validateAllAccountsInBackground()
516
+ }, 100) // 稍微延迟一下,让用户看到快速加载的效果
517
  })
518
 
519
  // 获取平台标签类型
 
527
  return typeMap[platform] || 'info'
528
  }
529
 
530
+ // 判断状态是否可点击(异常状态可点击)
531
+ const isStatusClickable = (status) => {
532
+ return status === '异常'; // 只有异常状态可点击,验证中不可点击
533
+ }
534
+
535
+ // 获取状态标签类型
536
+ const getStatusTagType = (status) => {
537
+ if (status === '验证中') {
538
+ return 'info'; // 验证中使用灰色
539
+ } else if (status === '正常') {
540
+ return 'success'; // 正常使用绿色
541
+ } else {
542
+ return 'danger'; // 无效使用红色
543
+ }
544
+ }
545
+
546
+ // 处理状态点击事件
547
+ const handleStatusClick = (row) => {
548
+ if (isStatusClickable(row.status)) {
549
+ // 触发重新登录流程
550
+ handleReLogin(row)
551
+ }
552
+ }
553
+
554
  // 过滤后的账号列表
555
  const filteredAccounts = computed(() => {
556
  if (!searchKeyword.value) return accountStore.accounts
557
+ return accountStore.accounts.filter(account =>
558
  account.name.includes(searchKeyword.value)
559
  )
560
  })
 
624
  // 编辑账号
625
  const handleEdit = (row) => {
626
  dialogType.value = 'edit'
627
+ Object.assign(accountForm, {
628
+ id: row.id,
629
+ name: row.name,
630
+ platform: row.platform,
631
+ status: row.status
632
+ })
633
  dialogVisible.value = true
634
  }
635
 
 
648
  try {
649
  // 调用API删除账号
650
  const response = await accountApi.deleteAccount(row.id)
651
+
652
  if (response.code === 200) {
653
  // 从状态管理中删除账号
654
  accountStore.deleteAccount(row.id)
 
669
  })
670
  }
671
 
672
+ // 下载Cookie文件
673
+ const handleDownloadCookie = (row) => {
674
+ // 从后端获取Cookie文件
675
+ const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
676
+ const downloadUrl = `${baseUrl}/downloadCookie?filePath=${encodeURIComponent(row.filePath)}`
677
+
678
+ // 创建一个隐藏的链接来触发下载
679
+ const link = document.createElement('a')
680
+ link.href = downloadUrl
681
+ link.download = `${row.name}_cookie.json`
682
+ link.target = '_blank'
683
+ link.style.display = 'none'
684
+ document.body.appendChild(link)
685
+ link.click()
686
+ document.body.removeChild(link)
687
+ }
688
+
689
+ // 上传Cookie文件
690
+ const handleUploadCookie = (row) => {
691
+ // 创建一个隐藏的文件输入框
692
+ const input = document.createElement('input')
693
+ input.type = 'file'
694
+ input.accept = '.json'
695
+ input.style.display = 'none'
696
+ document.body.appendChild(input)
697
+
698
+ input.onchange = async (event) => {
699
+ const file = event.target.files[0]
700
+ if (!file) return
701
+
702
+ // 检查文件类型
703
+ if (!file.name.endsWith('.json')) {
704
+ ElMessage.error('请选择JSON格式的Cookie文件')
705
+ document.body.removeChild(input)
706
+ return
707
+ }
708
+
709
+ try {
710
+ // 创建FormData对象
711
+ const formData = new FormData()
712
+ formData.append('file', file)
713
+ formData.append('id', row.id)
714
+ formData.append('platform', row.platform)
715
+
716
+ // 发送上传请求
717
+ const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
718
+ const response = await fetch(`${baseUrl}/uploadCookie`, {
719
+ method: 'POST',
720
+ body: formData
721
+ })
722
+
723
+ const result = await response.json()
724
+
725
+ if (result.code === 200) {
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)
737
+ }
738
+ }
739
+
740
+ input.click()
741
+ }
742
+
743
+ // 重新登录账号
744
+ const handleReLogin = (row) => {
745
+ // 设置表单信息
746
+ dialogType.value = 'edit'
747
+ Object.assign(accountForm, {
748
+ id: row.id,
749
+ name: row.name,
750
+ platform: row.platform,
751
+ status: row.status
752
+ })
753
+
754
+ // 重置SSE状态
755
+ sseConnecting.value = false
756
+ qrCodeData.value = ''
757
+ loginStatus.value = ''
758
+
759
+ // 显示对话框
760
+ dialogVisible.value = true
761
+
762
+ // 立即开始登录流程
763
+ setTimeout(() => {
764
+ connectSSE(row.platform, row.name)
765
+ }, 300)
766
+ }
767
+
768
+ // 获取默认头像
769
+ const getDefaultAvatar = (name) => {
770
+ // 使用简单的默认头像,可以基于用户名生成不同的颜色
771
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`
772
+ }
773
+
774
  // SSE事件源对象
775
  let eventSource = null
776
 
 
786
  const connectSSE = (platform, name) => {
787
  // 关闭可能存在的连接
788
  closeSSEConnection()
789
+
790
  // 设置连接状态
791
  sseConnecting.value = true
792
  qrCodeData.value = ''
793
  loginStatus.value = ''
794
+
795
  // 获取平台类型编号
796
  const platformTypeMap = {
797
  '小红书': '1',
 
799
  '抖音': '3',
800
  '快手': '4'
801
  }
802
+
803
  const type = platformTypeMap[platform] || '1'
804
+
805
  // 创建SSE连接
806
  const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
807
  const url = `${baseUrl}/login?type=${type}&id=${encodeURIComponent(name)}`
808
+
809
  eventSource = new EventSource(url)
810
+
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 {
 
828
  } catch (error) {
829
  console.error('处理二维码数据出错:', error)
830
  }
831
+ }
832
  // 如果收到状态码
833
  else if (data === '200' || data === '500') {
834
  loginStatus.value = data
835
+
836
  // 如果登录成功
837
  if (data === '200') {
838
  setTimeout(() => {
839
  // 关闭连接
840
  closeSSEConnection()
841
+
842
  // 1秒后关闭对话框并开始刷新
843
  setTimeout(() => {
844
  dialogVisible.value = false
845
  sseConnecting.value = false
846
+
847
+ // 根据是否是重新登录显示不同提示
848
+ ElMessage.success(dialogType.value === 'edit' ? '重新登录成功' : '账号添加成功')
849
+
850
  // 显示更新账号信息提示
851
  ElMessage({
852
  type: 'info',
853
  message: '正在同步账号信息...',
854
  duration: 0
855
  })
856
+
857
  // 触发刷新操作
858
  fetchAccounts().then(() => {
859
  // 刷新完成后关闭提示
 
865
  } else {
866
  // 登录失败,关闭连接
867
  closeSSEConnection()
868
+
869
  // 2秒后重置状态,允许重试
870
  setTimeout(() => {
871
  sseConnecting.value = false
 
875
  }
876
  }
877
  }
878
+
879
  // 监听错误
880
  eventSource.onerror = (error) => {
881
  console.error('SSE连接错误:', error)
 
895
  } else {
896
  // 编辑账号逻辑
897
  try {
898
+ // 将平台名称转换为类型数字
899
+ const platformTypeMap = {
900
+ '快手': 1,
901
+ '抖音': 2,
902
+ '视频号': 3,
903
+ '小红书': 4
904
+ };
905
+ const type = platformTypeMap[accountForm.platform] || 1;
906
+
907
  const res = await accountApi.updateAccount({
908
  id: accountForm.id,
909
+ type: type,
910
  userName: accountForm.name
911
  })
912
  if (res.code === 200) {
913
  // 更新状态管理中的账号
914
+ const updatedAccount = {
915
+ id: accountForm.id,
916
+ name: accountForm.name,
917
+ platform: accountForm.platform,
918
+ status: accountForm.status // Keep the existing status
919
+ };
920
+ accountStore.updateAccount(accountForm.id, updatedAccount)
921
  ElMessage.success('更新成功')
922
  dialogVisible.value = false
923
  // 刷新账号列表
 
1005
  }
1006
 
1007
  // 二维码容器样式
1008
+ .clickable-status {
1009
+ cursor: pointer;
1010
+ transition: all 0.3s;
1011
+
1012
+ &:hover {
1013
+ transform: scale(1.05);
1014
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
1015
+ }
1016
+ }
1017
+
1018
  .qrcode-container {
1019
  margin-top: 20px;
1020
  display: flex;
sau_frontend/src/views/MaterialManagement.vue CHANGED
@@ -25,6 +25,7 @@
25
 
26
  <div v-if="filteredMaterials.length > 0" class="material-list">
27
  <el-table :data="filteredMaterials" style="width: 100%">
 
28
  <el-table-column prop="filename" label="文件名" width="300" />
29
  <el-table-column prop="filesize" label="文件大小" width="120">
30
  <template #default="scope">
@@ -58,7 +59,8 @@
58
  <el-form-item label="文件名称:">
59
  <el-input
60
  v-model="customFilename"
61
- placeholder="选填"
 
62
  clearable
63
  />
64
  </el-form-item>
@@ -66,10 +68,11 @@
66
  <el-upload
67
  class="upload-demo"
68
  drag
 
69
  :auto-upload="false"
70
  :on-change="handleFileChange"
 
71
  :file-list="fileList"
72
- :limit="1"
73
  >
74
  <el-icon class="el-icon--upload"><Upload /></el-icon>
75
  <div class="el-upload__text">
@@ -77,11 +80,26 @@
77
  </div>
78
  <template #tip>
79
  <div class="el-upload__tip">
80
- 支持视频、图片等格式文件,只能上传一个文件
81
  </div>
82
  </template>
83
  </el-upload>
84
  </el-form-item>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  </el-form>
86
  </div>
87
  <template #footer>
@@ -123,7 +141,7 @@
123
  </template>
124
 
125
  <script setup>
126
- import { ref, reactive, computed, onMounted } from 'vue'
127
  import { Refresh, Upload } from '@element-plus/icons-vue'
128
  import { ElMessage, ElMessageBox } from 'element-plus'
129
  import { materialApi } from '@/api/material'
@@ -145,6 +163,17 @@ const currentMaterial = ref(null)
145
  // 文件上传
146
  const fileList = ref([])
147
  const customFilename = ref('')
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  // 获取素材列表
150
  const fetchMaterials = async () => {
@@ -186,6 +215,7 @@ const handleUploadMaterial = () => {
186
  // 清空变量
187
  fileList.value = []
188
  customFilename.value = ''
 
189
  uploadDialogVisible.value = true
190
  }
191
 
@@ -193,16 +223,24 @@ const handleUploadMaterial = () => {
193
  const handleUploadDialogClose = () => {
194
  fileList.value = []
195
  customFilename.value = ''
 
196
  }
197
 
198
  // 文件选择变更
199
  const handleFileChange = (file, uploadFileList) => {
200
- // 只保留最新选择的文件
201
- console.log('选择的文件:', file)
202
- if (file.raw) {
203
- // 确保获取到原始文件对象
204
- fileList.value = [file]
205
  }
 
 
 
 
 
 
 
 
206
  }
207
 
208
  // 提交上传
@@ -212,45 +250,67 @@ const submitUpload = async () => {
212
  return
213
  }
214
 
215
- // 确保文件对象存在
216
- const fileObj = fileList.value[0]
217
- if (!fileObj || !fileObj.raw) {
218
- ElMessage.warning('文件对象无效,请重新选择文件')
219
- return
220
- }
221
-
222
  isUploading.value = true
223
 
224
- try {
225
- // 使用FormData进行表单提交
226
- const formData = new FormData()
227
-
228
- // 添加文件,确保使用正确的文件对象
229
- console.log('上传文件对象:', fileObj.raw)
230
- formData.append('file', fileObj.raw)
231
-
232
- // 如果用户输入了自定义文件名,则添加到表单中
233
- if (customFilename.value.trim()) {
234
- formData.append('filename', customFilename.value.trim())
235
- console.log('自定义文件名:', customFilename.value.trim())
236
- }
237
-
238
- const response = await materialApi.uploadMaterial(formData)
239
-
240
- if (response.code === 200) {
241
- ElMessage.success('上传成功')
242
- uploadDialogVisible.value = false
243
- // 上传成功后直接刷新素材列表
244
- await fetchMaterials()
245
- } else {
246
- ElMessage.error(response.msg || '上传失败')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
- } catch (error) {
249
- console.error('上传素材出错:', error)
250
- ElMessage.error('上传失败: ' + (error.message || '未知错误'))
251
- } finally {
252
- isUploading.value = false
253
  }
 
 
 
 
 
254
  }
255
 
256
  // 预览素材
@@ -430,6 +490,24 @@ onMounted(() => {
430
  justify-content: flex-end;
431
  }
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  /* 覆盖Element Plus对话框样式 */
434
  :deep(.el-dialog__body) {
435
  padding: 20px 0;
@@ -445,4 +523,35 @@ onMounted(() => {
445
  padding-top: 10px;
446
  padding-bottom: 15px;
447
  }
448
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  <div v-if="filteredMaterials.length > 0" class="material-list">
27
  <el-table :data="filteredMaterials" style="width: 100%">
28
+ <el-table-column prop="uuid" label="UUID" width="180" />
29
  <el-table-column prop="filename" label="文件名" width="300" />
30
  <el-table-column prop="filesize" label="文件大小" width="120">
31
  <template #default="scope">
 
59
  <el-form-item label="文件名称:">
60
  <el-input
61
  v-model="customFilename"
62
+ placeholder="选填 (仅单个文件时生效)"
63
+ :disabled="customFilenameDisabled"
64
  clearable
65
  />
66
  </el-form-item>
 
68
  <el-upload
69
  class="upload-demo"
70
  drag
71
+ multiple
72
  :auto-upload="false"
73
  :on-change="handleFileChange"
74
+ :on-remove="handleFileRemove"
75
  :file-list="fileList"
 
76
  >
77
  <el-icon class="el-icon--upload"><Upload /></el-icon>
78
  <div class="el-upload__text">
 
80
  </div>
81
  <template #tip>
82
  <div class="el-upload__tip">
83
+ 支持视频、图片等格式文件,次选择多个文件
84
  </div>
85
  </template>
86
  </el-upload>
87
  </el-form-item>
88
+ <el-form-item label="上传列表" v-if="fileList.length > 0">
89
+ <div class="upload-file-list">
90
+ <div v-for="file in fileList" :key="file.uid" class="upload-file-item">
91
+ <span class="file-name">{{ file.name }}</span>
92
+ <el-progress
93
+ :percentage="uploadProgress[file.uid]?.percentage || 0"
94
+ :text-inside="true"
95
+ :stroke-width="20"
96
+ style="width: 100%; margin-top: 5px;"
97
+ >
98
+ <span>{{ uploadProgress[file.uid]?.speed || '' }}</span>
99
+ </el-progress>
100
+ </div>
101
+ </div>
102
+ </el-form-item>
103
  </el-form>
104
  </div>
105
  <template #footer>
 
141
  </template>
142
 
143
  <script setup>
144
+ import { ref, computed, onMounted, watch } from 'vue'
145
  import { Refresh, Upload } from '@element-plus/icons-vue'
146
  import { ElMessage, ElMessageBox } from 'element-plus'
147
  import { materialApi } from '@/api/material'
 
163
  // 文件上传
164
  const fileList = ref([])
165
  const customFilename = ref('')
166
+ const customFilenameDisabled = computed(() => fileList.value.length > 1)
167
+ const uploadProgress = ref({}); // { [uid]: { percentage: 0, speed: '' } }
168
+
169
+
170
+ watch(fileList, (newList) => {
171
+ if (newList.length <= 1) {
172
+ // If you want to clear the custom name when going back to single file, uncomment below
173
+ // customFilename.value = ''
174
+ }
175
+ });
176
+
177
 
178
  // 获取素材列表
179
  const fetchMaterials = async () => {
 
215
  // 清空变量
216
  fileList.value = []
217
  customFilename.value = ''
218
+ uploadProgress.value = {};
219
  uploadDialogVisible.value = true
220
  }
221
 
 
223
  const handleUploadDialogClose = () => {
224
  fileList.value = []
225
  customFilename.value = ''
226
+ uploadProgress.value = {};
227
  }
228
 
229
  // 文件选择变更
230
  const handleFileChange = (file, uploadFileList) => {
231
+ fileList.value = uploadFileList;
232
+ const newProgress = {};
233
+ for (const f of uploadFileList) {
234
+ newProgress[f.uid] = { percentage: 0, speed: '' };
 
235
  }
236
+ uploadProgress.value = newProgress;
237
+ }
238
+
239
+ const handleFileRemove = (file, uploadFileList) => {
240
+ fileList.value = uploadFileList;
241
+ const newProgress = { ...uploadProgress.value };
242
+ delete newProgress[file.uid];
243
+ uploadProgress.value = newProgress;
244
  }
245
 
246
  // 提交上传
 
250
  return
251
  }
252
 
 
 
 
 
 
 
 
253
  isUploading.value = true
254
 
255
+ for (const file of fileList.value) {
256
+ try {
257
+ // 确保文件对象存在
258
+ if (!file || !file.raw) {
259
+ ElMessage.warning(`文件 ${file.name} 对象无效,已跳过`)
260
+ continue
261
+ }
262
+
263
+ const formData = new FormData()
264
+ formData.append('file', file.raw)
265
+
266
+ // 只有当只有一个文件时,自定义文件名才生效
267
+ if (fileList.value.length === 1 && customFilename.value.trim()) {
268
+ formData.append('filename', customFilename.value.trim())
269
+ }
270
+
271
+ let lastLoaded = 0;
272
+ let lastTime = Date.now();
273
+
274
+ const response = await materialApi.uploadMaterial(formData, (progressEvent) => {
275
+ const progressData = uploadProgress.value[file.uid];
276
+ if (!progressData) return;
277
+
278
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
279
+ progressData.percentage = progress;
280
+
281
+ const currentTime = Date.now();
282
+ const timeDiff = (currentTime - lastTime) / 1000; // in seconds
283
+ const loadedDiff = progressEvent.loaded - lastLoaded;
284
+
285
+ if (timeDiff > 0.5) { // Update speed every 0.5 seconds
286
+ const speed = loadedDiff / timeDiff; // bytes per second
287
+ if (speed > 1024 * 1024) {
288
+ progressData.speed = (speed / (1024 * 1024)).toFixed(2) + ' MB/s';
289
+ } else {
290
+ progressData.speed = (speed / 1024).toFixed(2) + ' KB/s';
291
+ }
292
+ lastLoaded = progressEvent.loaded;
293
+ lastTime = currentTime;
294
+ }
295
+ })
296
+
297
+ if (response.code === 200) {
298
+ ElMessage.success(`文件 ${file.name} 上传成功`)
299
+ const progressData = uploadProgress.value[file.uid];
300
+ if(progressData) progressData.speed = '完成';
301
+ } else {
302
+ ElMessage.error(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`)
303
+ }
304
+ } catch (error) {
305
+ console.error(`上传文件 ${file.name} 出错:`, error)
306
+ ElMessage.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`)
307
  }
 
 
 
 
 
308
  }
309
+
310
+ isUploading.value = false
311
+ // Keep dialog open to show results
312
+ // uploadDialogVisible.value = false
313
+ await fetchMaterials()
314
  }
315
 
316
  // 预览素材
 
490
  justify-content: flex-end;
491
  }
492
 
493
+ .upload-file-list {
494
+ width: 100%;
495
+ }
496
+
497
+ .upload-file-item {
498
+ border: 1px solid #dcdfe6;
499
+ border-radius: 4px;
500
+ padding: 10px;
501
+ margin-bottom: 10px;
502
+ }
503
+
504
+ .upload-file-item .file-name {
505
+ font-size: 14px;
506
+ color: #606266;
507
+ margin-bottom: 5px;
508
+ display: block;
509
+ }
510
+
511
  /* 覆盖Element Plus对话框样式 */
512
  :deep(.el-dialog__body) {
513
  padding: 20px 0;
 
523
  padding-top: 10px;
524
  padding-bottom: 15px;
525
  }
526
+
527
+ /* 修改上传进度条样式 */
528
+ :deep(.el-progress__text) {
529
+ color: #303133 !important; /* 深灰色字体,确保在各种背景上都可见 */
530
+ font-size: 12px;
531
+ }
532
+
533
+ :deep(.el-progress--line) {
534
+ margin-bottom: 10px;
535
+ }
536
+
537
+ .upload-file-item {
538
+ border: 1px solid #dcdfe6;
539
+ border-radius: 6px; /* 增加圆角 */
540
+ padding: 12px; /* 增加内边距 */
541
+ margin-bottom: 12px; /* 增加外边距 */
542
+ background-color: #fafafa; /* 轻微背景色 */
543
+ transition: box-shadow 0.3s; /* 添加过渡效果 */
544
+ }
545
+
546
+ .upload-file-item:hover {
547
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 悬停效果 */
548
+ }
549
+
550
+ .upload-file-item .file-name {
551
+ font-size: 14px;
552
+ color: #303133; /* 深灰色字体 */
553
+ margin-bottom: 8px; /* 增加底部间距 */
554
+ display: block;
555
+ font-weight: 500;
556
+ }
557
+ </style>
sau_frontend/src/views/PublishCenter.vue CHANGED
@@ -296,6 +296,36 @@
296
  </el-radio-group>
297
  </div>
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  <!-- 标题输入 -->
300
  <div class="title-section">
301
  <h3>标题</h3>
@@ -457,7 +487,14 @@
457
  <!-- 操作按钮 -->
458
  <div class="action-buttons">
459
  <el-button size="small" @click="cancelPublish(tab)">取消</el-button>
460
- <el-button size="small" type="primary" @click="confirmPublish(tab)">发布</el-button>
 
 
 
 
 
 
 
461
  </div>
462
  </div>
463
  </div>
@@ -526,7 +563,9 @@ const defaultTabInit = {
526
  videosPerDay: 1, // 每天发布视频数量
527
  dailyTimes: ['10:00'], // 每天发布时间点列表
528
  startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
529
- publishStatus: null // 发布状态,包含message和type
 
 
530
  }
531
 
532
  // helper to create a fresh deep-copied tab from defaultTabInit
@@ -733,29 +772,40 @@ const cancelPublish = (tab) => {
733
 
734
  // 确认发布
735
  const confirmPublish = async (tab) => {
 
 
 
 
 
 
 
736
  return new Promise((resolve, reject) => {
737
  // 数据验证
738
  if (tab.fileList.length === 0) {
739
  ElMessage.error('请先上传视频文件')
 
740
  reject(new Error('请先上传视频文件'))
741
  return
742
  }
743
  if (!tab.title.trim()) {
744
  ElMessage.error('请输入标题')
 
745
  reject(new Error('请输入标题'))
746
  return
747
  }
748
  if (!tab.selectedPlatform) {
749
  ElMessage.error('请选择发布平台')
 
750
  reject(new Error('请选择发布平台'))
751
  return
752
  }
753
  if (tab.selectedAccounts.length === 0) {
754
  ElMessage.error('请选择发布账号')
 
755
  reject(new Error('请选择发布账号'))
756
  return
757
  }
758
-
759
  // 构造发布数据,符合后端API格式
760
  const publishData = {
761
  type: tab.selectedPlatform,
@@ -772,9 +822,10 @@ const confirmPublish = async (tab) => {
772
  startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
773
  category: 0, //表示非原创
774
  productLink: tab.productLink.trim() || '', // 商品链接
775
- productTitle: tab.productTitle.trim() || '' // 商品名称
 
776
  }
777
-
778
  // 调用后端发布API
779
  fetch(`${apiBaseUrl}/postVideo`, {
780
  method: 'POST',
@@ -815,6 +866,9 @@ const confirmPublish = async (tab) => {
815
  }
816
  reject(error)
817
  })
 
 
 
818
  })
819
  }
820
 
@@ -1261,10 +1315,19 @@ const batchPublish = async () => {
1261
  padding-top: 20px;
1262
  border-top: 1px solid #ebeef5;
1263
  }
 
 
 
 
 
 
 
 
 
1264
  }
1265
  }
1266
  }
1267
-
1268
  // 已上传文件列表样式
1269
  .uploaded-files {
1270
  margin-top: 20px;
 
296
  </el-radio-group>
297
  </div>
298
 
299
+ <!-- 草稿选项 (仅在视频号可见) -->
300
+ <div v-if="tab.selectedPlatform === 2" class="draft-section">
301
+ <el-checkbox
302
+ v-model="tab.isDraft"
303
+ label="视频号仅保存草稿(用手机发布)"
304
+ class="draft-checkbox"
305
+ />
306
+ </div>
307
+
308
+ <!-- 标签 (仅在抖音可见) -->
309
+ <div v-if="tab.selectedPlatform === 3" class="product-section">
310
+ <h3>商品链接</h3>
311
+ <el-input
312
+ v-model="tab.productTitle"
313
+ type="text"
314
+ :rows="1"
315
+ placeholder="请输入商品名称"
316
+ maxlength="200"
317
+ class="product-name-input"
318
+ />
319
+ <el-input
320
+ v-model="tab.productLink"
321
+ type="text"
322
+ :rows="1"
323
+ placeholder="请输入商品链接"
324
+ maxlength="200"
325
+ class="product-link-input"
326
+ />
327
+ </div>
328
+
329
  <!-- 标题输入 -->
330
  <div class="title-section">
331
  <h3>标题</h3>
 
487
  <!-- 操作按钮 -->
488
  <div class="action-buttons">
489
  <el-button size="small" @click="cancelPublish(tab)">取消</el-button>
490
+ <el-button
491
+ size="small"
492
+ type="primary"
493
+ @click="confirmPublish(tab)"
494
+ :loading="tab.publishing || false"
495
+ >
496
+ {{ tab.publishing ? '发布中...' : '发布' }}
497
+ </el-button>
498
  </div>
499
  </div>
500
  </div>
 
563
  videosPerDay: 1, // 每天发布视频数量
564
  dailyTimes: ['10:00'], // 每天发布时间点列表
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
 
772
 
773
  // 确认发布
774
  const confirmPublish = async (tab) => {
775
+ // 防止重复点击
776
+ if (tab.publishing) {
777
+ return Promise.reject(new Error('正在发布中,请稍候...'))
778
+ }
779
+
780
+ tab.publishing = true // 设置发布状态为进行中
781
+
782
  return new Promise((resolve, reject) => {
783
  // 数据验证
784
  if (tab.fileList.length === 0) {
785
  ElMessage.error('请先上传视频文件')
786
+ tab.publishing = false // 重置发布状态
787
  reject(new Error('请先上传视频文件'))
788
  return
789
  }
790
  if (!tab.title.trim()) {
791
  ElMessage.error('请输入标题')
792
+ tab.publishing = false // 重置发布状态
793
  reject(new Error('请输入标题'))
794
  return
795
  }
796
  if (!tab.selectedPlatform) {
797
  ElMessage.error('请选择发布平台')
798
+ tab.publishing = false // 重置发布状态
799
  reject(new Error('请选择发布平台'))
800
  return
801
  }
802
  if (tab.selectedAccounts.length === 0) {
803
  ElMessage.error('请选择发布账号')
804
+ tab.publishing = false // 重置发布状态
805
  reject(new Error('请选择发布账号'))
806
  return
807
  }
808
+
809
  // 构造发布数据,符合后端API格式
810
  const publishData = {
811
  type: tab.selectedPlatform,
 
822
  startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
823
  category: 0, //表示非原创
824
  productLink: tab.productLink.trim() || '', // 商品链接
825
+ productTitle: tab.productTitle.trim() || '', // 商品名称
826
+ isDraft: tab.isDraft // 是否保存为草稿,仅视频号平台使用
827
  }
828
+
829
  // 调用后端发布API
830
  fetch(`${apiBaseUrl}/postVideo`, {
831
  method: 'POST',
 
866
  }
867
  reject(error)
868
  })
869
+ .finally(() => {
870
+ tab.publishing = false // 重置发布状态
871
+ })
872
  })
873
  }
874
 
 
1315
  padding-top: 20px;
1316
  border-top: 1px solid #ebeef5;
1317
  }
1318
+
1319
+ .draft-section {
1320
+ margin: 20px 0;
1321
+
1322
+ .draft-checkbox {
1323
+ display: block;
1324
+ margin: 10px 0;
1325
+ }
1326
+ }
1327
  }
1328
  }
1329
  }
1330
+
1331
  // 已上传文件列表样式
1332
  .uploaded-files {
1333
  margin-top: 20px;
sau_frontend/vite.config.js CHANGED
@@ -19,7 +19,14 @@ export default defineConfig({
19
  },
20
  server: {
21
  port: 5173,
22
- open: true
 
 
 
 
 
 
 
23
  },
24
  build: {
25
  outDir: 'dist',
 
19
  },
20
  server: {
21
  port: 5173,
22
+ open: true,
23
+ proxy: {
24
+ '/api': {
25
+ target: 'http://localhost:5409',
26
+ changeOrigin: true,
27
+ rewrite: (path) => path.replace(/^\/api/, '')
28
+ }
29
+ }
30
  },
31
  build: {
32
  outDir: 'dist',
uploader/tencent_uploader/main.py CHANGED
@@ -82,15 +82,16 @@ async def weixin_setup(account_file, handle=False):
82
 
83
 
84
  class TencentVideo(object):
85
- def __init__(self, title, file_path, tags, publish_date: datetime, account_file, category=None):
86
  self.title = title # 视频标题
87
  self.file_path = file_path
88
  self.tags = tags
89
  self.publish_date = publish_date
90
  self.account_file = account_file
91
  self.category = category
92
- self.local_executable_path = LOCAL_CHROME_PATH
93
  self.headless = LOCAL_CHROME_HEADLESS
 
 
94
 
95
  async def set_schedule_time_tencent(self, page, publish_date):
96
  label_element = page.locator("label").filter(has_text="定时").nth(1)
@@ -186,21 +187,37 @@ class TencentVideo(object):
186
  async def click_publish(self, page):
187
  while True:
188
  try:
189
- publish_buttion = page.locator('div.form-btns button:has-text("发表")')
190
- if await publish_buttion.count():
191
- await publish_buttion.click()
192
- await page.wait_for_url("https://channels.weixin.qq.com/platform/post/list", timeout=5000)
193
- tencent_logger.success(" [-]视频发布成功")
 
 
 
 
 
 
 
 
 
 
194
  break
195
  except Exception as e:
196
  current_url = page.url
197
- if "https://channels.weixin.qq.com/platform/post/list" in current_url:
198
- tencent_logger.success(" [-]视频发布成功")
199
- break
 
 
200
  else:
201
- tencent_logger.exception(f" [-] Exception: {e}")
202
- tencent_logger.info(" [-] 视频正在发布中...")
203
- await asyncio.sleep(0.5)
 
 
 
 
204
 
205
  async def detect_upload_status(self, page):
206
  while True:
 
82
 
83
 
84
  class TencentVideo(object):
85
+ def __init__(self, title, file_path, tags, publish_date: datetime, account_file, category=None, is_draft=False):
86
  self.title = title # 视频标题
87
  self.file_path = file_path
88
  self.tags = tags
89
  self.publish_date = publish_date
90
  self.account_file = account_file
91
  self.category = category
 
92
  self.headless = LOCAL_CHROME_HEADLESS
93
+ self.is_draft = is_draft # 是否保存为草稿
94
+ self.local_executable_path = LOCAL_CHROME_PATH or None
95
 
96
  async def set_schedule_time_tencent(self, page, publish_date):
97
  label_element = page.locator("label").filter(has_text="定时").nth(1)
 
187
  async def click_publish(self, page):
188
  while True:
189
  try:
190
+ if self.is_draft:
191
+ # 点击"保存草稿"按钮
192
+ draft_button = page.locator('div.form-btns button:has-text("保存草稿")')
193
+ if await draft_button.count():
194
+ await draft_button.click()
195
+ # 等待跳转到草稿箱页面或确认保存成功
196
+ await page.wait_for_url("**/post/list**", timeout=5000) # 使用通配符匹配包含post/list的URL
197
+ tencent_logger.success(" [-]视频草稿保存成功")
198
+ else:
199
+ # 点击"发表"按钮
200
+ publish_button = page.locator('div.form-btns button:has-text("发表")')
201
+ if await publish_button.count():
202
+ await publish_button.click()
203
+ await page.wait_for_url("https://channels.weixin.qq.com/platform/post/list", timeout=5000)
204
+ tencent_logger.success(" [-]视频发布成功")
205
  break
206
  except Exception as e:
207
  current_url = page.url
208
+ if self.is_draft:
209
+ # 检查是否在草稿相关的页面
210
+ if "post/list" in current_url or "draft" in current_url:
211
+ tencent_logger.success(" [-]视频草稿保存成功")
212
+ break
213
  else:
214
+ # 检查是否在发布列表页面
215
+ if "https://channels.weixin.qq.com/platform/post/list" in current_url:
216
+ tencent_logger.success(" [-]视频发布成功")
217
+ break
218
+ tencent_logger.exception(f" [-] Exception: {e}")
219
+ tencent_logger.info(" [-] 视频正在发布中...")
220
+ await asyncio.sleep(0.5)
221
 
222
  async def detect_upload_status(self, page):
223
  while True: