482 lines
12 KiB
Vue
482 lines
12 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import api from '../api'
|
||
import MessageBubble from '../components/MessageBubble.vue'
|
||
import {
|
||
UserOutlined,
|
||
MessageOutlined,
|
||
CheckCircleOutlined,
|
||
ClockCircleOutlined,
|
||
ArrowRightOutlined,
|
||
ThunderboltOutlined
|
||
} from '@ant-design/icons-vue'
|
||
|
||
const stats = ref({
|
||
totalAccounts: 0,
|
||
activeAccounts: 0,
|
||
totalMessages: 0,
|
||
repliedMessages: 0,
|
||
replyRate: '0%'
|
||
})
|
||
|
||
const recentLogs = ref([])
|
||
const loading = ref(true)
|
||
|
||
const fetchStats = async () => {
|
||
try {
|
||
const [accountsRes, statsRes] = await Promise.all([
|
||
api.get(`/accounts`),
|
||
api.get(`/logs/stats`)
|
||
])
|
||
|
||
const accounts = accountsRes.data
|
||
stats.value.totalAccounts = accounts.length
|
||
stats.value.activeAccounts = accounts.filter(a => a.status === 'online').length
|
||
|
||
// 后端全量统计所有消息,不受列表条数限制
|
||
stats.value.totalMessages = statsRes.data.total || 0
|
||
stats.value.repliedMessages = statsRes.data.replied || 0
|
||
|
||
if (stats.value.totalMessages > 0) {
|
||
stats.value.replyRate = ((stats.value.repliedMessages / stats.value.totalMessages) * 100).toFixed(1) + '%'
|
||
} else {
|
||
stats.value.replyRate = '0%'
|
||
}
|
||
} catch (error) {
|
||
console.error('获取仪表盘统计失败', error)
|
||
}
|
||
}
|
||
|
||
// 近期动态只在进入页面时加载一次,不自动刷新
|
||
const fetchRecentLogs = async () => {
|
||
try {
|
||
loading.value = true
|
||
const logsRes = await api.get(`/logs?limit=5`)
|
||
recentLogs.value = logsRes.data
|
||
} catch (error) {
|
||
console.error('获取近期动态失败', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
let statsInterval = null
|
||
|
||
onMounted(() => {
|
||
fetchStats()
|
||
fetchRecentLogs()
|
||
// 统计卡片每10秒自动刷新一次
|
||
statsInterval = setInterval(fetchStats, 10000)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (statsInterval) clearInterval(statsInterval)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="dashboard-container">
|
||
<!-- 头部欢迎卡片 -->
|
||
<div class="welcome-banner glass-card">
|
||
<div class="banner-content">
|
||
<h1 class="text-gradient" style="margin: 0 0 8px 0; font-size: 2rem;">欢迎使用抖音多账号客服系统</h1>
|
||
<p style="color: var(--text-secondary); font-size: 1rem;">
|
||
多账户自动回复RPA后台。支持快捷扫码登录、状态持久化保存、以及自定义关键字规则精准答复。
|
||
</p>
|
||
</div>
|
||
<div class="banner-icon">
|
||
<ThunderboltOutlined style="font-size: 4rem; color: #c084fc; opacity: 0.3;" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计网格 -->
|
||
<a-row :gutter="[24, 24]" style="margin-top: 24px;">
|
||
<!-- 托管账号总数 -->
|
||
<a-col :xs="24" :sm="12" :lg="6">
|
||
<div class="glass-card stat-card border-purple">
|
||
<div class="stat-icon purple-glow">
|
||
<UserOutlined />
|
||
</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">托管账号</span>
|
||
<h2 class="stat-value">{{ stats.totalAccounts }}</h2>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 在线工作账号 -->
|
||
<a-col :xs="24" :sm="12" :lg="6">
|
||
<div class="glass-card stat-card border-green">
|
||
<div class="stat-icon green-glow">
|
||
<CheckCircleOutlined />
|
||
</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">在线运行</span>
|
||
<h2 class="stat-value text-green">{{ stats.activeAccounts }}</h2>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 处理消息数 -->
|
||
<a-col :xs="24" :sm="12" :lg="6">
|
||
<div class="glass-card stat-card border-blue">
|
||
<div class="stat-icon blue-glow">
|
||
<MessageOutlined />
|
||
</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">接收消息</span>
|
||
<h2 class="stat-value">{{ stats.totalMessages }}</h2>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 自动回复率 -->
|
||
<a-col :xs="24" :sm="12" :lg="6">
|
||
<div class="glass-card stat-card border-pink">
|
||
<div class="stat-icon pink-glow">
|
||
<ClockCircleOutlined />
|
||
</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">自动回复率</span>
|
||
<h2 class="stat-value text-pink">{{ stats.replyRate }}</h2>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 中部内容区 -->
|
||
<a-row :gutter="[24, 24]" style="margin-top: 24px;">
|
||
<!-- 最近动态 -->
|
||
<a-col :xs="24" :lg="16">
|
||
<div class="glass-card" style="height: 100%;">
|
||
<div class="card-header">
|
||
<h3>近期自动回复动态</h3>
|
||
<router-link to="/logs" class="view-all-link">
|
||
全部记录 <ArrowRightOutlined />
|
||
</router-link>
|
||
</div>
|
||
|
||
<a-list :loading="loading" :data-source="recentLogs" style="margin-top: 16px;">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item class="log-item">
|
||
<div class="log-left">
|
||
<div class="log-dot" :class="item.status"></div>
|
||
<div class="log-details">
|
||
<div class="log-sender">
|
||
<strong>{{ item.sender_name }}</strong> 给账号 <strong>#{{ item.account_id }}</strong> 发送:
|
||
</div>
|
||
<div class="log-content">
|
||
<MessageBubble :content="item.message_content" compact />
|
||
</div>
|
||
<div v-if="item.reply_content" class="log-reply">
|
||
<span class="reply-tag">自动回复</span>
|
||
<MessageBubble :content="item.reply_content" compact />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="log-right-info">
|
||
<span class="log-time">{{ new Date(item.created_at).toLocaleTimeString() }}</span>
|
||
<a-tag :color="item.status === 'replied' ? 'success' : item.status === 'ignored' ? 'default' : 'error'">
|
||
{{ item.status === 'replied' ? '已回复' : item.status === 'ignored' ? '已略过' : '失败' }}
|
||
</a-tag>
|
||
</div>
|
||
</a-list-item>
|
||
</template>
|
||
<template #empty>
|
||
<div class="empty-state">
|
||
<MessageOutlined style="font-size: 3rem; color: var(--text-muted); margin-bottom: 12px;" />
|
||
<p>暂无消息日志,启动账号 RPA 并接收私信后会自动记录</p>
|
||
</div>
|
||
</template>
|
||
</a-list>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 快速导航与操作 -->
|
||
<a-col :xs="24" :lg="8">
|
||
<div class="glass-card" style="height: 100%;">
|
||
<h3>快捷导航</h3>
|
||
<div class="quick-actions-grid" style="margin-top: 20px;">
|
||
<router-link to="/accounts" class="quick-action-card">
|
||
<UserOutlined class="action-icon text-gradient" />
|
||
<span>账号配置</span>
|
||
<p>扫码登录并托管多个抖音账号</p>
|
||
</router-link>
|
||
|
||
<router-link to="/rules" class="quick-action-card">
|
||
<SettingOutlined class="action-icon text-gradient" />
|
||
<span>回复策略</span>
|
||
<p>自定义关键字匹配与自动回复模板</p>
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.welcome-banner {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 32px;
|
||
background: linear-gradient(135deg, rgba(20, 20, 30, 0.8) 0%, rgba(35, 20, 45, 0.8) 100%);
|
||
border-left: 4px solid var(--primary-color);
|
||
}
|
||
|
||
.stat-card {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24px;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
margin-right: 20px;
|
||
}
|
||
|
||
.purple-glow {
|
||
background: rgba(170, 59, 255, 0.15);
|
||
color: #c084fc;
|
||
border: 1px solid rgba(170, 59, 255, 0.3);
|
||
}
|
||
|
||
.green-glow {
|
||
background: rgba(21, 200, 100, 0.15);
|
||
color: #22c55e;
|
||
border: 1px solid rgba(21, 200, 100, 0.3);
|
||
}
|
||
|
||
.blue-glow {
|
||
background: rgba(0, 170, 255, 0.15);
|
||
color: #38bdf8;
|
||
border: 1px solid rgba(0, 170, 255, 0.3);
|
||
}
|
||
|
||
.pink-glow {
|
||
background: rgba(236, 72, 153, 0.15);
|
||
color: #f472b6;
|
||
border: 1px solid rgba(236, 72, 153, 0.3);
|
||
}
|
||
|
||
.border-purple:hover { border-color: rgba(170, 59, 255, 0.5); }
|
||
.border-green:hover { border-color: rgba(21, 200, 100, 0.5); }
|
||
.border-blue:hover { border-color: rgba(0, 170, 255, 0.5); }
|
||
.border-pink:hover { border-color: rgba(236, 72, 153, 0.5); }
|
||
|
||
.stat-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.stat-value {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: 1.75rem;
|
||
font-weight: 700;
|
||
margin: 4px 0 0 0;
|
||
color: #fff;
|
||
}
|
||
|
||
.text-green { color: var(--accent-green) !important; }
|
||
.text-pink { color: var(--accent-pink) !important; }
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--border-light);
|
||
padding-bottom: 16px;
|
||
}
|
||
|
||
.view-all-link {
|
||
font-size: 0.85rem;
|
||
color: var(--primary-color);
|
||
transition: var(--transition-smooth);
|
||
}
|
||
|
||
.view-all-link:hover {
|
||
color: #c084fc;
|
||
}
|
||
|
||
.log-item {
|
||
border-bottom: 1px solid var(--border-light) !important;
|
||
padding: 16px 0 !important;
|
||
}
|
||
|
||
.log-left {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.log-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
margin-top: 6px;
|
||
margin-right: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.log-dot.replied { background: var(--accent-green); }
|
||
.log-dot.ignored { background: var(--text-muted); }
|
||
.log-dot.failed { background: var(--accent-red); }
|
||
|
||
.log-sender {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.log-content {
|
||
margin-top: 4px;
|
||
color: #fff;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.log-reply {
|
||
margin-top: 8px;
|
||
font-size: 0.9rem;
|
||
background: rgba(170, 59, 255, 0.08);
|
||
padding: 6px 12px;
|
||
border-radius: 8px;
|
||
border-left: 2px solid var(--primary-color);
|
||
display: inline-block;
|
||
}
|
||
|
||
.reply-tag {
|
||
color: #c084fc;
|
||
font-weight: 600;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.log-right-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.log-time {
|
||
font-size: 0.75rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 48px 0;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.quick-actions-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.quick-action-card {
|
||
display: block;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid var(--border-light);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
text-decoration: none !important;
|
||
transition: var(--transition-smooth);
|
||
}
|
||
|
||
.quick-action-card:hover {
|
||
background: rgba(170, 59, 255, 0.05);
|
||
border-color: var(--border-glow);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 24px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.quick-action-card span {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.quick-action-card p {
|
||
font-size: 0.8rem;
|
||
color: var(--text-secondary);
|
||
margin: 0;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.welcome-banner {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
padding: 20px 16px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.welcome-banner h1 {
|
||
font-size: 1.35rem !important;
|
||
}
|
||
|
||
.banner-icon {
|
||
display: none;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 16px;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
font-size: 20px;
|
||
margin-right: 14px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.45rem;
|
||
}
|
||
|
||
.card-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.log-item {
|
||
flex-direction: column !important;
|
||
align-items: flex-start !important;
|
||
gap: 10px;
|
||
}
|
||
|
||
.log-left {
|
||
width: 100%;
|
||
}
|
||
|
||
.log-right-info {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.log-reply {
|
||
display: block;
|
||
max-width: 100%;
|
||
}
|
||
}
|
||
</style>
|