Files
EveryPublish/sau_frontend/src/views/AccountManagement.vue
T
2025-06-03 21:29:42 +08:00

840 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="account-management">
<div class="page-header">
<h1>账号管理</h1>
</div>
<div class="account-tabs">
<el-tabs v-model="activeTab" class="account-tabs-nav">
<el-tab-pane label="全部" name="all">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredAccounts.length > 0" class="account-list">
<el-table :data="filteredAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="scope.row.avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'danger'"
effect="plain"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无账号数据" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="快手" name="kuaishou">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredKuaishouAccounts.length > 0" class="account-list">
<el-table :data="filteredKuaishouAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="scope.row.avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'danger'"
effect="plain"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无快手账号数据" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="抖音" name="douyin">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredDouyinAccounts.length > 0" class="account-list">
<el-table :data="filteredDouyinAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="scope.row.avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'danger'"
effect="plain"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无抖音账号数据" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="视频号" name="channels">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredChannelsAccounts.length > 0" class="account-list">
<el-table :data="filteredChannelsAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="scope.row.avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'danger'"
effect="plain"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无视频号账号数据" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="小红书" name="xiaohongshu">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredXiaohongshuAccounts.length > 0" class="account-list">
<el-table :data="filteredXiaohongshuAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="scope.row.avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'danger'"
effect="plain"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无小红书账号数据" />
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 添加/编辑账号对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="!sseConnecting"
:show-close="!sseConnecting"
>
<el-form :model="accountForm" label-width="80px" :rules="rules" ref="accountFormRef">
<el-form-item label="平台" prop="platform">
<el-select
v-model="accountForm.platform"
placeholder="请选择平台"
style="width: 100%"
:disabled="sseConnecting"
>
<el-option label="快手" value="快手" />
<el-option label="抖音" value="抖音" />
<el-option label="视频号" value="视频号" />
<el-option label="小红书" value="小红书" />
</el-select>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input
v-model="accountForm.name"
placeholder="请输入账号名称"
:disabled="sseConnecting"
/>
</el-form-item>
<!-- 二维码显示区域 -->
<div v-if="sseConnecting" class="qrcode-container">
<div v-if="qrCodeData && !loginStatus" class="qrcode-wrapper">
<p class="qrcode-tip">请使用对应平台APP扫描二维码登录</p>
<img :src="qrCodeData" alt="登录二维码" class="qrcode-image" />
</div>
<div v-else-if="!qrCodeData && !loginStatus" class="loading-wrapper">
<el-icon class="is-loading"><Refresh /></el-icon>
<span>请求中...</span>
</div>
<div v-else-if="loginStatus === '200'" class="success-wrapper">
<el-icon><CircleCheckFilled /></el-icon>
<span>添加成功</span>
</div>
<div v-else-if="loginStatus === '500'" class="error-wrapper">
<el-icon><CircleCloseFilled /></el-icon>
<span>添加失败,请稍后再试</span>
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :disabled="sseConnecting && !loginStatus">取消</el-button>
<el-button
type="primary"
@click="submitAccountForm"
:loading="sseConnecting"
:disabled="sseConnecting"
>
{{ sseConnecting ? '请求中' : '确认' }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { Refresh, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { accountApi } from '@/api/account'
import { useAccountStore } from '@/stores/account'
import { useAppStore } from '@/stores/app'
// 获取账号状态管理
const accountStore = useAccountStore()
// 获取应用状态管理
const appStore = useAppStore()
// 当前激活的标签页
const activeTab = ref('all')
// 搜索关键词
const searchKeyword = ref('')
// 获取账号数据
const fetchAccounts = async () => {
if (appStore.isAccountRefreshing) return
appStore.setAccountRefreshing(true)
try {
const res = await accountApi.getValidAccounts()
if (res.code === 200 && res.data) {
accountStore.setAccounts(res.data)
ElMessage.success('账号数据获取成功')
// 标记为已访问
if (appStore.isFirstTimeAccountManagement) {
appStore.setAccountManagementVisited()
}
} else {
ElMessage.error('获取账号数据失败')
}
} catch (error) {
console.error('获取账号数据失败:', error)
ElMessage.error('获取账号数据失败')
} finally {
appStore.setAccountRefreshing(false)
}
}
// 页面加载时获取账号数据
onMounted(() => {
// 只有第一次进入时才获取数据
if (appStore.isFirstTimeAccountManagement) {
fetchAccounts()
}
})
// 获取平台标签类型
const getPlatformTagType = (platform) => {
const typeMap = {
'快手': 'success',
'抖音': 'danger',
'视频号': 'warning',
'小红书': 'info'
}
return typeMap[platform] || 'info'
}
// 过滤后的账号列表
const filteredAccounts = computed(() => {
if (!searchKeyword.value) return accountStore.accounts
return accountStore.accounts.filter(account =>
account.name.includes(searchKeyword.value)
)
})
// 按平台过滤的账号列表
const filteredKuaishouAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '快手')
})
const filteredDouyinAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '抖音')
})
const filteredChannelsAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '视频号')
})
const filteredXiaohongshuAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '小红书')
})
// 搜索处理
const handleSearch = () => {
// 搜索逻辑已通过计算属性实现
}
// 对话框相关
const dialogVisible = ref(false)
const dialogType = ref('add') // 'add' 或 'edit'
const accountFormRef = ref(null)
// 账号表单
const accountForm = reactive({
id: null,
name: '',
platform: '',
status: '正常'
})
// 表单验证规则
const rules = {
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
name: [{ required: true, message: '请输入账号名称', trigger: 'blur' }]
}
// SSE连接状态
const sseConnecting = ref(false)
const qrCodeData = ref('')
const loginStatus = ref('')
// 添加账号
const handleAddAccount = () => {
dialogType.value = 'add'
Object.assign(accountForm, {
id: null,
name: '',
platform: '',
status: '正常'
})
// 重置SSE状态
sseConnecting.value = false
qrCodeData.value = ''
loginStatus.value = ''
dialogVisible.value = true
}
// 编辑账号
const handleEdit = (row) => {
dialogType.value = 'edit'
Object.assign(accountForm, { ...row })
dialogVisible.value = true
}
// 删除账号
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除账号 ${row.name} 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
// 调用API删除账号
const response = await accountApi.deleteAccount(row.id)
if (response.code === 200) {
// 从状态管理中删除账号
accountStore.deleteAccount(row.id)
ElMessage({
type: 'success',
message: '删除成功',
})
} else {
ElMessage.error(response.msg || '删除失败')
}
} catch (error) {
console.error('删除账号失败:', error)
ElMessage.error('删除账号失败')
}
})
.catch(() => {
// 取消删除
})
}
// SSE事件源对象
let eventSource = null
// 关闭SSE连接
const closeSSEConnection = () => {
if (eventSource) {
eventSource.close()
eventSource = null
}
}
// 建立SSE连接
const connectSSE = (platform, name) => {
// 关闭可能存在的连接
closeSSEConnection()
// 设置连接状态
sseConnecting.value = true
qrCodeData.value = ''
loginStatus.value = ''
// 获取平台类型编号
const platformTypeMap = {
'小红书': '1',
'视频号': '2',
'抖音': '3',
'快手': '4'
}
const type = platformTypeMap[platform] || '1'
// 创建SSE连接
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
const url = `${baseUrl}/login?type=${type}&id=${encodeURIComponent(name)}`
eventSource = new EventSource(url)
// 监听消息
eventSource.onmessage = (event) => {
const data = event.data
console.log('SSE消息:', data)
// 如果还没有二维码数据,且数据长度较长,认为是二维码
if (!qrCodeData.value && data.length > 100) {
try {
// 确保数据是有效的base64编码
// 如果数据已经包含了data:image前缀,直接使用
if (data.startsWith('data:image')) {
qrCodeData.value = data
} else {
// 否则添加前缀
qrCodeData.value = `data:image/png;base64,${data}`
}
console.log('设置二维码数据,长度:', data.length)
} catch (error) {
console.error('处理二维码数据出错:', error)
}
}
// 如果收到状态码
else if (data === '200' || data === '500') {
loginStatus.value = data
// 如果登录成功,添加账号到状态管理
if (data === '200') {
setTimeout(() => {
const newId = accountStore.accounts.length > 0 ?
Math.max(...accountStore.accounts.map(a => a.id)) + 1 : 1
const newAccount = {
id: newId,
name: accountForm.name,
platform: accountForm.platform,
status: '正常',
avatar: '/vite.svg' // 默认头像
}
// 添加到状态管理
accountStore.addAccount(newAccount)
// 关闭连接
closeSSEConnection()
// 1秒后关闭对话框
setTimeout(() => {
dialogVisible.value = false
sseConnecting.value = false
ElMessage.success('账号添加成功')
// 显示更新账号信息提示并刷新账号列表
ElMessage({
type: 'info',
message: '更新账号信息中...',
duration: 2000
})
// 触发刷新操作
isRefreshing.value = true
fetchAccounts()
}, 1000)
}, 1000)
} else {
// 登录失败,关闭连接
closeSSEConnection()
// 2秒后重置状态,允许重试
setTimeout(() => {
sseConnecting.value = false
qrCodeData.value = ''
loginStatus.value = ''
}, 2000)
}
}
}
// 监听错误
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error)
ElMessage.error('连接服务器失败,请稍后再试')
closeSSEConnection()
sseConnecting.value = false
}
}
// 提交账号表单
const submitAccountForm = () => {
accountFormRef.value.validate(async (valid) => {
if (valid) {
if (dialogType.value === 'add') {
// 建立SSE连接
connectSSE(accountForm.platform, accountForm.name)
} else {
// 编辑账号逻辑保持不变
try {
// 更新状态管理中的账号
accountStore.updateAccount(accountForm.id, accountForm)
ElMessage.success('更新成功')
dialogVisible.value = false
} catch (error) {
console.error('更新账号失败:', error)
ElMessage.error('更新账号失败')
}
}
} else {
return false
}
})
}
// 组件卸载前关闭SSE连接
onBeforeUnmount(() => {
closeSSEConnection()
})
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.account-management {
.page-header {
margin-bottom: 20px;
h1 {
font-size: 24px;
color: $text-primary;
margin: 0;
}
}
.account-tabs {
background-color: #fff;
border-radius: 4px;
box-shadow: $box-shadow-light;
.account-tabs-nav {
padding: 20px;
}
}
.account-list-container {
.account-search {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.el-input {
width: 300px;
}
.action-buttons {
display: flex;
gap: 10px;
.el-icon.is-loading {
animation: rotate 1s linear infinite;
}
}
}
.account-list {
margin-bottom: 20px;
}
.empty-data {
padding: 40px 0;
}
}
// 二维码容器样式
.qrcode-container {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
.qrcode-wrapper {
text-align: center;
.qrcode-tip {
margin-bottom: 15px;
color: #606266;
}
.qrcode-image {
max-width: 200px;
max-height: 200px;
border: 1px solid #ebeef5;
}
}
.loading-wrapper, .success-wrapper, .error-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
.el-icon {
font-size: 48px;
&.is-loading {
animation: rotate 1s linear infinite;
}
}
span {
font-size: 16px;
}
}
.success-wrapper .el-icon {
color: #67c23a;
}
.error-wrapper .el-icon {
color: #f56c6c;
}
}
}
</style>