feat: 账号管理页面

This commit is contained in:
Edan's Win11
2025-06-03 21:29:42 +08:00
parent 2b0a027bde
commit 1b553ebe7a
2 changed files with 868 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
<template>
<div class="about">
<h1>关于我们</h1>
<p>这是关于页面</p>
</div>
</template>
<script setup>
// 关于页面组件
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
.about {
text-align: center;
padding: 2rem;
h1 {
color: #2c3e50;
margin-bottom: 1rem;
}
p {
color: #7f8c8d;
}
}
</style>
@@ -0,0 +1,840 @@
<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>