返回 Social Auto Upload
AccountManagement.vue
根目录 / sau_frontend / src / views / AccountManagement.vue
1 <template>
2 <div class="account-management">
3 <div class="page-header">
4 <h1>账号管理</h1>
5 </div>
6
7 <div class="account-tabs">
8 <el-tabs v-model="activeTab" class="account-tabs-nav">
9 <el-tab-pane label="全部" name="all">
10 <div class="account-list-container">
11 <div class="account-search">
12 <el-input
13 v-model="searchKeyword"
14 placeholder="输入名称或账号搜索"
15 prefix-icon="Search"
16 clearable
17 @clear="handleSearch"
18 @input="handleSearch"
19 />
20 <div class="action-buttons">
21 <el-button type="primary" @click="handleAddAccount">添加账号</el-button>
22 <el-button type="info" @click="fetchAccounts" :loading="false">
23 <el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
24 <span v-if="appStore.isAccountRefreshing">刷新中</span>
25 </el-button>
26 </div>
27 </div>
28
29 <div v-if="filteredAccounts.length > 0" class="account-list">
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" />
37 <el-table-column prop="platform" label="平台">
38 <template #default="scope">
39 <el-tag
40 :type="getPlatformTagType(scope.row.platform)"
41 effect="plain"
42 >
43 {{ scope.row.platform }}
44 </el-tag>
45 </template>
46 </el-table-column>
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>
61 </el-table-column>
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>
70 </el-table>
71 </div>
72
73 <div v-else class="empty-data">
74 <el-empty description="暂无账号数据" />
75 </div>
76 </div>
77 </el-tab-pane>
78
79 <el-tab-pane label="快手" name="kuaishou">
80 <div class="account-list-container">
81 <div class="account-search">
82 <el-input
83 v-model="searchKeyword"
84 placeholder="输入名称或账号搜索"
85 prefix-icon="Search"
86 clearable
87 @clear="handleSearch"
88 @input="handleSearch"
89 />
90 <div class="action-buttons">
91 <el-button type="primary" @click="handleAddAccount">添加账号</el-button>
92 <el-button type="info" @click="fetchAccounts" :loading="false">
93 <el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
94 <span v-if="appStore.isAccountRefreshing">刷新中</span>
95 </el-button>
96 </div>
97 </div>
98
99 <div v-if="filteredKuaishouAccounts.length > 0" class="account-list">
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" />
107 <el-table-column prop="platform" label="平台">
108 <template #default="scope">
109 <el-tag
110 :type="getPlatformTagType(scope.row.platform)"
111 effect="plain"
112 >
113 {{ scope.row.platform }}
114 </el-tag>
115 </template>
116 </el-table-column>
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>
131 </el-table-column>
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>
140 </el-table>
141 </div>
142
143 <div v-else class="empty-data">
144 <el-empty description="暂无快手账号数据" />
145 </div>
146 </div>
147 </el-tab-pane>
148
149 <el-tab-pane label="抖音" name="douyin">
150 <div class="account-list-container">
151 <div class="account-search">
152 <el-input
153 v-model="searchKeyword"
154 placeholder="输入名称或账号搜索"
155 prefix-icon="Search"
156 clearable
157 @clear="handleSearch"
158 @input="handleSearch"
159 />
160 <div class="action-buttons">
161 <el-button type="primary" @click="handleAddAccount">添加账号</el-button>
162 <el-button type="info" @click="fetchAccounts" :loading="false">
163 <el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
164 <span v-if="appStore.isAccountRefreshing">刷新中</span>
165 </el-button>
166 </div>
167 </div>
168
169 <div v-if="filteredDouyinAccounts.length > 0" class="account-list">
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" />
177 <el-table-column prop="platform" label="平台">
178 <template #default="scope">
179 <el-tag
180 :type="getPlatformTagType(scope.row.platform)"
181 effect="plain"
182 >
183 {{ scope.row.platform }}
184 </el-tag>
185 </template>
186 </el-table-column>
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>
201 </el-table-column>
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>
210 </el-table>
211 </div>
212
213 <div v-else class="empty-data">
214 <el-empty description="暂无抖音账号数据" />
215 </div>
216 </div>
217 </el-tab-pane>
218
219 <el-tab-pane label="视频号" name="channels">
220 <div class="account-list-container">
221 <div class="account-search">
222 <el-input
223 v-model="searchKeyword"
224 placeholder="输入名称或账号搜索"
225 prefix-icon="Search"
226 clearable
227 @clear="handleSearch"
228 @input="handleSearch"
229 />
230 <div class="action-buttons">
231 <el-button type="primary" @click="handleAddAccount">添加账号</el-button>
232 <el-button type="info" @click="fetchAccounts" :loading="false">
233 <el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
234 <span v-if="appStore.isAccountRefreshing">刷新中</span>
235 </el-button>
236 </div>
237 </div>
238
239 <div v-if="filteredChannelsAccounts.length > 0" class="account-list">
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" />
247 <el-table-column prop="platform" label="平台">
248 <template #default="scope">
249 <el-tag
250 :type="getPlatformTagType(scope.row.platform)"
251 effect="plain"
252 >
253 {{ scope.row.platform }}
254 </el-tag>
255 </template>
256 </el-table-column>
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>
271 </el-table-column>
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>
280 </el-table>
281 </div>
282
283 <div v-else class="empty-data">
284 <el-empty description="暂无视频号账号数据" />
285 </div>
286 </div>
287 </el-tab-pane>
288
289 <el-tab-pane label="小红书" name="xiaohongshu">
290 <div class="account-list-container">
291 <div class="account-search">
292 <el-input
293 v-model="searchKeyword"
294 placeholder="输入名称或账号搜索"
295 prefix-icon="Search"
296 clearable
297 @clear="handleSearch"
298 @input="handleSearch"
299 />
300 <div class="action-buttons">
301 <el-button type="primary" @click="handleAddAccount">添加账号</el-button>
302 <el-button type="info" @click="fetchAccounts" :loading="false">
303 <el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
304 <span v-if="appStore.isAccountRefreshing">刷新中</span>
305 </el-button>
306 </div>
307 </div>
308
309 <div v-if="filteredXiaohongshuAccounts.length > 0" class="account-list">
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" />
317 <el-table-column prop="platform" label="平台">
318 <template #default="scope">
319 <el-tag
320 :type="getPlatformTagType(scope.row.platform)"
321 effect="plain"
322 >
323 {{ scope.row.platform }}
324 </el-tag>
325 </template>
326 </el-table-column>
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>
341 </el-table-column>
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>
350 </el-table>
351 </div>
352
353 <div v-else class="empty-data">
354 <el-empty description="暂无小红书账号数据" />
355 </div>
356 </div>
357 </el-tab-pane>
358 </el-tabs>
359 </div>
360
361 <!-- 添加/编辑账号对话框 -->
362 <el-dialog
363 v-model="dialogVisible"
364 :title="dialogType === 'add' ? '添加账号' : '编辑账号'"
365 width="500px"
366 :close-on-click-modal="false"
367 :close-on-press-escape="!sseConnecting"
368 :show-close="!sseConnecting"
369 >
370 <el-form :model="accountForm" label-width="80px" :rules="rules" ref="accountFormRef">
371 <el-form-item label="平台" prop="platform">
372 <el-select
373 v-model="accountForm.platform"
374 placeholder="请选择平台"
375 style="width: 100%"
376 :disabled="dialogType === 'edit' || sseConnecting"
377 >
378 <el-option label="快手" value="快手" />
379 <el-option label="抖音" value="抖音" />
380 <el-option label="视频号" value="视频号" />
381 <el-option label="小红书" value="小红书" />
382 </el-select>
383 </el-form-item>
384 <el-form-item label="名称" prop="name">
385 <el-input
386 v-model="accountForm.name"
387 placeholder="请输入账号名称"
388 :disabled="sseConnecting"
389 />
390 </el-form-item>
391
392 <!-- 二维码显示区域 -->
393 <div v-if="sseConnecting" class="qrcode-container">
394 <div v-if="qrCodeData && !loginStatus" class="qrcode-wrapper">
395 <p class="qrcode-tip">请使用对应平台APP扫描二维码登录</p>
396 <img :src="qrCodeData" alt="登录二维码" class="qrcode-image" />
397 </div>
398 <div v-else-if="!qrCodeData && !loginStatus" class="loading-wrapper">
399 <el-icon class="is-loading"><Refresh /></el-icon>
400 <span>请求中...</span>
401 </div>
402 <div v-else-if="loginStatus === '200'" class="success-wrapper">
403 <el-icon><CircleCheckFilled /></el-icon>
404 <span>添加成功</span>
405 </div>
406 <div v-else-if="loginStatus === '500'" class="error-wrapper">
407 <el-icon><CircleCloseFilled /></el-icon>
408 <span>添加失败,请稍后再试</span>
409 </div>
410 </div>
411 </el-form>
412 <template #footer>
413 <span class="dialog-footer">
414 <el-button @click="dialogVisible = false">取消</el-button>
415 <el-button
416 type="primary"
417 @click="submitAccountForm"
418 :loading="sseConnecting"
419 :disabled="sseConnecting"
420 >
421 {{ sseConnecting ? '请求中' : '确认' }}
422 </el-button>
423 </span>
424 </template>
425 </el-dialog>
426 </div>
427 </template>
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'
435 import { useAppStore } from '@/stores/app'
436 import { http } from '@/utils/request'
437
438 // 获取账号状态管理
439 const accountStore = useAccountStore()
440 // 获取应用状态管理
441 const appStore = useAppStore()
442
443 // 当前激活的标签页
444 const activeTab = ref('all')
445
446 // 搜索关键词
447 const searchKeyword = ref('')
448
449 // 获取账号数据(快速,不验证)
450 const fetchAccountsQuick = async () => {
451 try {
452 const res = await accountApi.getAccounts()
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);
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) {
476 accountStore.setAccounts(res.data)
477 ElMessage.success('账号数据获取成功')
478 // 标记为已访问
479 if (appStore.isFirstTimeAccountManagement) {
480 appStore.setAccountManagementVisited()
481 }
482 } else {
483 ElMessage.error('获取账号数据失败')
484 }
485 } catch (error) {
486 console.error('获取账号数据失败:', error)
487 ElMessage.error('获取账号数据失败')
488 } finally {
489 appStore.setAccountRefreshing(false)
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 // 获取平台标签类型
520 const getPlatformTagType = (platform) => {
521 const typeMap = {
522 '快手': 'success',
523 '抖音': 'danger',
524 '视频号': 'warning',
525 '小红书': 'info'
526 }
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 })
561
562 // 按平台过滤的账号列表
563 const filteredKuaishouAccounts = computed(() => {
564 return filteredAccounts.value.filter(account => account.platform === '快手')
565 })
566
567 const filteredDouyinAccounts = computed(() => {
568 return filteredAccounts.value.filter(account => account.platform === '抖音')
569 })
570
571 const filteredChannelsAccounts = computed(() => {
572 return filteredAccounts.value.filter(account => account.platform === '视频号')
573 })
574
575 const filteredXiaohongshuAccounts = computed(() => {
576 return filteredAccounts.value.filter(account => account.platform === '小红书')
577 })
578
579 // 搜索处理
580 const handleSearch = () => {
581 // 搜索逻辑已通过计算属性实现
582 }
583
584 // 对话框相关
585 const dialogVisible = ref(false)
586 const dialogType = ref('add') // 'add' 或 'edit'
587 const accountFormRef = ref(null)
588
589 // 账号表单
590 const accountForm = reactive({
591 id: null,
592 name: '',
593 platform: '',
594 status: '正常'
595 })
596
597 // 表单验证规则
598 const rules = {
599 platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
600 name: [{ required: true, message: '请输入账号名称', trigger: 'blur' }]
601 }
602
603 // SSE连接状态
604 const sseConnecting = ref(false)
605 const qrCodeData = ref('')
606 const loginStatus = ref('')
607
608 // 添加账号
609 const handleAddAccount = () => {
610 dialogType.value = 'add'
611 Object.assign(accountForm, {
612 id: null,
613 name: '',
614 platform: '',
615 status: '正常'
616 })
617 // 重置SSE状态
618 sseConnecting.value = false
619 qrCodeData.value = ''
620 loginStatus.value = ''
621 dialogVisible.value = true
622 }
623
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
636 // 删除账号
637 const handleDelete = (row) => {
638 ElMessageBox.confirm(
639 `确定要删除账号 ${row.name} 吗?`,
640 '警告',
641 {
642 confirmButtonText: '确定',
643 cancelButtonText: '取消',
644 type: 'warning',
645 }
646 )
647 .then(async () => {
648 try {
649 // 调用API删除账号
650 const response = await accountApi.deleteAccount(row.id)
651
652 if (response.code === 200) {
653 // 从状态管理中删除账号
654 accountStore.deleteAccount(row.id)
655 ElMessage({
656 type: 'success',
657 message: '删除成功',
658 })
659 } else {
660 ElMessage.error(response.msg || '删除失败')
661 }
662 } catch (error) {
663 console.error('删除账号失败:', error)
664 ElMessage.error('删除账号失败')
665 }
666 })
667 .catch(() => {
668 // 取消删除
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 // 使用统一的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)
726 }
727 }
728
729 input.click()
730 }
731
732 // 重新登录账号
733 const handleReLogin = (row) => {
734 // 设置表单信息
735 dialogType.value = 'edit'
736 Object.assign(accountForm, {
737 id: row.id,
738 name: row.name,
739 platform: row.platform,
740 status: row.status
741 })
742
743 // 重置SSE状态
744 sseConnecting.value = false
745 qrCodeData.value = ''
746 loginStatus.value = ''
747
748 // 显示对话框
749 dialogVisible.value = true
750
751 // 立即开始登录流程
752 setTimeout(() => {
753 connectSSE(row.platform, row.name)
754 }, 300)
755 }
756
757 // 获取默认头像
758 const getDefaultAvatar = (name) => {
759 // 使用简单的默认头像,可以基于用户名生成不同的颜色
760 return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`
761 }
762
763 // SSE事件源对象
764 let eventSource = null
765
766 // 关闭SSE连接
767 const closeSSEConnection = () => {
768 if (eventSource) {
769 eventSource.close()
770 eventSource = null
771 }
772 }
773
774 // 建立SSE连接
775 const connectSSE = (platform, name) => {
776 // 关闭可能存在的连接
777 closeSSEConnection()
778
779 // 设置连接状态
780 sseConnecting.value = true
781 qrCodeData.value = ''
782 loginStatus.value = ''
783
784 // 获取平台类型编号
785 const platformTypeMap = {
786 '小红书': '1',
787 '视频号': '2',
788 '抖音': '3',
789 '快手': '4'
790 }
791
792 const type = platformTypeMap[platform] || '1'
793
794 // 创建SSE连接
795 const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
796 const url = `${baseUrl}/login?type=${type}&id=${encodeURIComponent(name)}`
797
798 eventSource = new EventSource(url)
799
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 // 如果收到状态码
817 else if (data === '200' || data === '500') {
818 loginStatus.value = data
819
820 // 如果登录成功
821 if (data === '200') {
822 setTimeout(() => {
823 // 关闭连接
824 closeSSEConnection()
825
826 // 1秒后关闭对话框并开始刷新
827 setTimeout(() => {
828 dialogVisible.value = false
829 sseConnecting.value = false
830
831 // 根据是否是重新登录显示不同提示
832 ElMessage.success(dialogType.value === 'edit' ? '重新登录成功' : '账号添加成功')
833
834 // 显示更新账号信息提示
835 ElMessage({
836 type: 'info',
837 message: '正在同步账号信息...',
838 duration: 0
839 })
840
841 // 触发刷新操作
842 fetchAccounts().then(() => {
843 // 刷新完成后关闭提示
844 ElMessage.closeAll()
845 ElMessage.success('账号信息已更新')
846 })
847 }, 1000)
848 }, 1000)
849 } else {
850 // 登录失败,关闭连接
851 closeSSEConnection()
852
853 // 2秒后重置状态,允许重试
854 setTimeout(() => {
855 sseConnecting.value = false
856 qrCodeData.value = ''
857 loginStatus.value = ''
858 }, 2000)
859 }
860 }
861 }
862
863 // 监听错误
864 eventSource.onerror = (error) => {
865 console.error('SSE连接错误:', error)
866 ElMessage.error('连接服务器失败,请稍后再试')
867 closeSSEConnection()
868 sseConnecting.value = false
869 }
870 }
871
872 // 提交账号表单
873 const submitAccountForm = () => {
874 accountFormRef.value.validate(async (valid) => {
875 if (valid) {
876 if (dialogType.value === 'add') {
877 // 建立SSE连接
878 connectSSE(accountForm.platform, accountForm.name)
879 } else {
880 // 编辑账号逻辑
881 try {
882 // 将平台名称转换为类型数字
883 const platformTypeMap = {
884 '小红书': 1,
885 '视频号': 2,
886 '抖音': 3,
887 '快手': 4
888 };
889 const type = platformTypeMap[accountForm.platform] || 1;
890
891 const res = await accountApi.updateAccount({
892 id: accountForm.id,
893 type: type,
894 userName: accountForm.name
895 })
896 if (res.code === 200) {
897 // 更新状态管理中的账号
898 const updatedAccount = {
899 id: accountForm.id,
900 name: accountForm.name,
901 platform: accountForm.platform,
902 status: accountForm.status // Keep the existing status
903 };
904 accountStore.updateAccount(accountForm.id, updatedAccount)
905 ElMessage.success('更新成功')
906 dialogVisible.value = false
907 // 刷新账号列表
908 fetchAccounts()
909 } else {
910 ElMessage.error(res.msg || '更新账号失败')
911 }
912 } catch (error) {
913 console.error('更新账号失败:', error)
914 ElMessage.error('更新账号失败')
915 }
916 }
917 } else {
918 return false
919 }
920 })
921 }
922
923 // 组件卸载前关闭SSE连接
924 onBeforeUnmount(() => {
925 closeSSEConnection()
926 })
927 </script>
928
929 <style lang="scss" scoped>
930 @use '@/styles/variables.scss' as *;
931
932 @keyframes rotate {
933 from {
934 transform: rotate(0deg);
935 }
936 to {
937 transform: rotate(360deg);
938 }
939 }
940
941 .account-management {
942 .page-header {
943 margin-bottom: 20px;
944
945 h1 {
946 font-size: 24px;
947 color: $text-primary;
948 margin: 0;
949 }
950 }
951
952 .account-tabs {
953 background-color: #fff;
954 border-radius: 4px;
955 box-shadow: $box-shadow-light;
956
957 .account-tabs-nav {
958 padding: 20px;
959 }
960 }
961
962 .account-list-container {
963 .account-search {
964 display: flex;
965 justify-content: space-between;
966 margin-bottom: 20px;
967
968 .el-input {
969 width: 300px;
970 }
971
972 .action-buttons {
973 display: flex;
974 gap: 10px;
975
976 .el-icon.is-loading {
977 animation: rotate 1s linear infinite;
978 }
979 }
980 }
981
982 .account-list {
983 margin-bottom: 20px;
984 }
985
986 .empty-data {
987 padding: 40px 0;
988 }
989 }
990
991 // 二维码容器样式
992 .clickable-status {
993 cursor: pointer;
994 transition: all 0.3s;
995
996 &:hover {
997 transform: scale(1.05);
998 box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
999 }
1000 }
1001
1002 .qrcode-container {
1003 margin-top: 20px;
1004 display: flex;
1005 flex-direction: column;
1006 align-items: center;
1007 justify-content: center;
1008 min-height: 250px;
1009
1010 .qrcode-wrapper {
1011 text-align: center;
1012
1013 .qrcode-tip {
1014 margin-bottom: 15px;
1015 color: #606266;
1016 }
1017
1018 .qrcode-image {
1019 max-width: 200px;
1020 max-height: 200px;
1021 border: 1px solid #ebeef5;
1022 background-color: black;
1023 }
1024 }
1025
1026 .loading-wrapper, .success-wrapper, .error-wrapper {
1027 display: flex;
1028 flex-direction: column;
1029 align-items: center;
1030 justify-content: center;
1031 gap: 10px;
1032
1033 .el-icon {
1034 font-size: 48px;
1035
1036 &.is-loading {
1037 animation: rotate 1s linear infinite;
1038 }
1039 }
1040
1041 span {
1042 font-size: 16px;
1043 }
1044 }
1045
1046 .success-wrapper .el-icon {
1047 color: #67c23a;
1048 }
1049
1050 .error-wrapper .el-icon {
1051 color: #f56c6c;
1052 }
1053 }
1054 }
1055 </style>
1055 lines Plain Text