一、技术实现方案
1.1 应用场景架构
实时推送系统
├── 订单模块
│ ├── 订单状态更新
│ ├── 物流信息推送
│ └── 支付结果通知
│
├── 数据看板
│ ├── 实时销售数据
│ ├── 用户活跃统计
│ └── 系统性能监控
│
├── 在线状态
│ ├── 用户在线检测
│ ├── 状态同步
│ └── 离线通知
│
└── 通知中心
├── 系统通知
├── 业务通知
└── 通知管理
1.2 技术选型
- 实时通信: WebSocket
- 状态管理: Vue 3 Reactive
- 数据可视化: ECharts
- 通知: Notification API + Toast
- 持久化: IndexedDB
二、订单状态推送
2.1 订单推送管理器
order-push-manager.js
import { ref, reactive } from 'vue'
export class OrderPushManager {
constructor() {
this.orders = reactive(new Map())
this.callbacks = {
onCreate: [],
onUpdate: [],
onStatusChange: []
}
}
// 订单状态映射
statusMap = {
'pending': { text: '待支付', color: '#e6a23c' },
'paid': { text: '已支付', color: '#409eff' },
'processing': { text: '处理中', color: '#409eff' },
'shipped': { text: '已发货', color: '#67c23a' },
'delivered': { text: '已送达', color: '#67c23a' },
'completed': { text: '已完成', color: '#909399' },
'cancelled': { text: '已取消', color: '#f56c6c' }
}
// 处理订单推送
handleOrderPush(data) {
const { type, order } = data
switch (type) {
case 'order_created':
this.createOrder(order)
break
case 'order_updated':
this.updateOrder(order)
break
case 'status_changed':
this.changeOrderStatus(order.id, order.status, order.statusTime)
break
case 'logistics_updated':
this.updateLogistics(order.id, order.logistics)
break
}
}
// 创建订单
createOrder(order) {
const newOrder = {
id: order.id,
orderNo: order.orderNo,
customerId: order.customerId,
customerName: order.customerName,
amount: order.amount,
status: order.status,
createTime: order.createTime || Date.now(),
statusHistory: [{
status: order.status,
time: order.createTime || Date.now()
}],
logistics: null
}
this.orders.set(order.id, newOrder)
this.emit('onCreate', newOrder)
}
// 更新订单
updateOrder(order) {
const existOrder = this.orders.get(order.id)
if (!existOrder) return
Object.assign(existOrder, order)
this.emit('onUpdate', existOrder)
}
// 改变订单状态
changeOrderStatus(orderId, newStatus, statusTime) {
const order = this.orders.get(orderId)
if (!order) return
const oldStatus = order.status
order.status = newStatus
// 添加状态历史
order.statusHistory.push({
status: newStatus,
time: statusTime || Date.now()
})
this.emit('onStatusChange', {
order,
oldStatus,
newStatus
})
}
// 更新物流信息
updateLogistics(orderId, logistics) {
const order = this.orders.get(orderId)
if (!order) return
order.logistics = {
company: logistics.company,
trackingNo: logistics.trackingNo,
status: logistics.status,
location: logistics.location,
updateTime: logistics.updateTime || Date.now(),
history: logistics.history || []
}
this.emit('onUpdate', order)
}
// 获取订单
getOrder(orderId) {
return this.orders.get(orderId)
}
// 获取所有订单
getAllOrders() {
return Array.from(this.orders.values())
.sort((a, b) => b.createTime - a.createTime)
}
// 获取订单状态信息
getStatusInfo(status) {
return this.statusMap[status] || { text: '未知', color: '#909399' }
}
// 订阅事件
on(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event].push(callback)
}
}
// 触发事件
emit(event, data) {
if (this.callbacks[event]) {
this.callbacks[event].forEach(callback => callback(data))
}
}
}
export const orderPushManager = new OrderPushManager()
2.2 订单推送组件
OrderPushView.vue
<script setup>
import { ref, computed, onMounted } from 'vue'
import { orderPushManager } from './order-push-manager.js'
const ws = ref(null)
const isConnected = ref(false)
const orders = ref([])
const selectedOrderId = ref(null)
const showNotification = ref(true)
// 当前选中的订单
const selectedOrder = computed(() => {
if (!selectedOrderId.value) return null
return orderPushManager.getOrder(selectedOrderId.value)
})
// 初始化WebSocket
const initWebSocket = () => {
ws.value = new WebSocket('ws://localhost:8080/order-push')
ws.value.onopen = () => {
isConnected.value = true
console.log('订单推送连接成功')
}
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data)
handlePushMessage(data)
}
ws.value.onclose = () => {
isConnected.value = false
console.log('订单推送连接关闭')
setTimeout(initWebSocket, 3000)
}
ws.value.onerror = (error) => {
console.error('WebSocket错误:', error)
}
}
// 处理推送消息
const handlePushMessage = (data) => {
orderPushManager.handleOrderPush(data)
loadOrders()
// 显示通知
if (showNotification.value) {
showOrderNotification(data)
}
}
// 显示订单通知
const showOrderNotification = (data) => {
const { type, order } = data
let title = ''
let body = ''
switch (type) {
case 'order_created':
title = '新订单'
body = `订单号: ${order.orderNo},金额: ¥${order.amount}`
break
case 'status_changed':
const statusInfo = orderPushManager.getStatusInfo(order.status)
title = '订单状态更新'
body = `订单${order.orderNo}状态变更为: ${statusInfo.text}`
break
case 'logistics_updated':
title = '物流更新'
body = `订单${order.orderNo}: ${order.logistics.location}`
break
}
if (title && 'Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body })
}
}
// 加载订单列表
const loadOrders = () => {
orders.value = orderPushManager.getAllOrders()
}
// 选择订单
const selectOrder = (orderId) => {
selectedOrderId.value = orderId
}
// 格式化时间
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleString('zh-CN')
}
// 格式化金额
const formatAmount = (amount) => {
return `¥${amount.toFixed(2)}`
}
// 模拟订单推送
const simulateOrderPush = () => {
const types = ['order_created', 'status_changed', 'logistics_updated']
const type = types[Math.floor(Math.random() * types.length)]
const orderId = `order_${Date.now()}`
let data = {
type: type,
order: {
id: orderId,
orderNo: `ORD${Date.now()}`,
customerId: 'cust_001',
customerName: '张三',
amount: Math.random() * 1000,
status: 'pending',
createTime: Date.now()
}
}
if (type === 'status_changed') {
const statuses = ['paid', 'processing', 'shipped', 'delivered']
data.order.status = statuses[Math.floor(Math.random() * statuses.length)]
}
if (type === 'logistics_updated') {
data.order.logistics = {
company: '顺丰速运',
trackingNo: 'SF' + Date.now(),
location: '上海市浦东新区',
status: '运输中'
}
}
handlePushMessage(data)
}
// 请求通知权限
const requestNotificationPermission = () => {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission()
}
}
onMounted(() => {
initWebSocket()
requestNotificationPermission()
// 监听订单变化
orderPushManager.on('onCreate', loadOrders)
orderPushManager.on('onUpdate', loadOrders)
orderPushManager.on('onStatusChange', (data) => {
loadOrders()
console.log('订单状态变化:', data)
})
})
</script>
<template>
<div class="order-push-view">
<div class="header">
<h2>订单实时推送</h2>
<div class="header-actions">
<button @click="simulateOrderPush" class="simulate-btn">
模拟推送
</button>
<div class="connection-badge" :class="{ connected: isConnected }">
{{ isConnected ? '已连接' : '未连接' }}
</div>
</div>
</div>
<div class="content">
<!-- 左侧订单列表 -->
<div class="order-list">
<div class="list-header">
<h3>订单列表 ({{ orders.length }})</h3>
<label class="notification-toggle">
<input type="checkbox" v-model="showNotification" />
<span>开启通知</span>
</label>
</div>
<div class="list-items">
<div
v-for="order in orders"
:key="order.id"
:class="['order-item', { active: order.id === selectedOrderId }]"
@click="selectOrder(order.id)"
>
<div class="order-header">
<span class="order-no">{{ order.orderNo }}</span>
<span class="order-amount">{{ formatAmount(order.amount) }}</span>
</div>
<div class="order-info">
<span class="customer-name">{{ order.customerName }}</span>
<span
class="order-status"
:style="{ color: orderPushManager.getStatusInfo(order.status).color }"
>
{{ orderPushManager.getStatusInfo(order.status).text }}
</span>
</div>
<div class="order-time">
{{ formatTime(order.createTime) }}
</div>
</div>
<div v-if="orders.length === 0" class="empty-list">
<div class="empty-icon">📦</div>
<div class="empty-text">暂无订单</div>
</div>
</div>
</div>
<!-- 右侧订单详情 -->
<div class="order-detail">
<div v-if="!selectedOrder" class="empty-detail">
<div class="empty-icon">📋</div>
<div class="empty-text">请选择一个订单查看详情</div>
</div>
<div v-else class="detail-content">
<div class="detail-section">
<h3>订单信息</h3>
<div class="info-grid">
<div class="info-item">
<label>订单号:</label>
<span>{{ selectedOrder.orderNo }}</span>
</div>
<div class="info-item">
<label>客户:</label>
<span>{{ selectedOrder.customerName }}</span>
</div>
<div class="info-item">
<label>金额:</label>
<span class="amount">{{ formatAmount(selectedOrder.amount) }}</span>
</div>
<div class="info-item">
<label>当前状态:</label>
<span
class="status-badge"
:style="{
background: orderPushManager.getStatusInfo(selectedOrder.status).color + '20',
color: orderPushManager.getStatusInfo(selectedOrder.status).color
}"
>
{{ orderPushManager.getStatusInfo(selectedOrder.status).text }}
</span>
</div>
</div>
</div>
<div class="detail-section">
<h3>状态历史</h3>
<div class="status-timeline">
<div
v-for="(item, index) in selectedOrder.statusHistory"
:key="index"
class="timeline-item"
>
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="timeline-status">
{{ orderPushManager.getStatusInfo(item.status).text }}
</div>
<div class="timeline-time">
{{ formatTime(item.time) }}
</div>
</div>
</div>
</div>
</div>
<div v-if="selectedOrder.logistics" class="detail-section">
<h3>物流信息</h3>
<div class="logistics-info">
<div class="logistics-item">
<label>物流公司:</label>
<span>{{ selectedOrder.logistics.company }}</span>
</div>
<div class="logistics-item">
<label>运单号:</label>
<span>{{ selectedOrder.logistics.trackingNo }}</span>
</div>
<div class="logistics-item">
<label>当前位置:</label>
<span>{{ selectedOrder.logistics.location }}</span>
</div>
<div class="logistics-item">
<label>状态:</label>
<span>{{ selectedOrder.logistics.status }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.order-push-view {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
.header {
padding: 20px;
background: white;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h2 {
margin: 0;
font-size: 20px;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.simulate-btn {
padding: 8px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.simulate-btn:hover {
background: #66b1ff;
}
.connection-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
background: #f56c6c;
color: white;
}
.connection-badge.connected {
background: #67c23a;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
/* 订单列表 */
.order-list {
width: 350px;
background: white;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
}
.list-header {
padding: 16px;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
}
.list-header h3 {
margin: 0;
font-size: 16px;
color: #303133;
}
.notification-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #606266;
cursor: pointer;
}
.list-items {
flex: 1;
overflow-y: auto;
}
.order-item {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}
.order-item:hover {
background: #f5f7fa;
}
.order-item.active {
background: #ecf5ff;
border-left: 3px solid #409eff;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.order-no {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.order-amount {
font-size: 16px;
font-weight: 700;
color: #f56c6c;
}
.order-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.customer-name {
font-size: 13px;
color: #606266;
}
.order-status {
font-size: 12px;
font-weight: 600;
}
.order-time {
font-size: 12px;
color: #c0c4cc;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: #909399;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-text {
font-size: 14px;
}
/* 订单详情 */
.order-detail {
flex: 1;
background: white;
overflow-y: auto;
}
.empty-detail {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
}
.detail-content {
padding: 24px;
}
.detail-section {
margin-bottom: 32px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h3 {
margin: 0 0 16px 0;
font-size: 16px;
color: #303133;
padding-bottom: 12px;
border-bottom: 2px solid #f0f0f0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-item label {
font-size: 13px;
color: #909399;
}
.info-item span {
font-size: 14px;
color: #303133;
}
.info-item .amount {
font-size: 20px;
font-weight: 700;
color: #f56c6c;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
}
/* 状态时间线 */
.status-timeline {
position: relative;
padding-left: 24px;
}
.timeline-item {
position: relative;
padding-bottom: 24px;
}
.timeline-item:last-child {
padding-bottom: 0;
}
.timeline-item::before {
content: '';
position: absolute;
left: -18px;
top: 8px;
bottom: -16px;
width: 2px;
background: #e4e7ed;
}
.timeline-item:last-child::before {
display: none;
}
.timeline-dot {
position: absolute;
left: -24px;
top: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background: #409eff;
border: 2px solid white;
box-shadow: 0 0 0 2px #409eff;
}
.timeline-status {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.timeline-time {
font-size: 12px;
color: #909399;
}
/* 物流信息 */
.logistics-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.logistics-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.logistics-item label {
font-size: 13px;
color: #909399;
}
.logistics-item span {
font-size: 14px;
color: #303133;
}
</style>
三、实时数据看板
3.1 数据看板管理器
dashboard-manager.js
import { ref, reactive } from 'vue'
export class DashboardManager {
constructor() {
this.metrics = reactive({
sales: {
today: 0,
yesterday: 0,
trend: []
},
orders: {
total: 0,
pending: 0,
completed: 0,
trend: []
},
users: {
online: 0,
total: 0,
newToday: 0,
trend: []
},
performance: {
cpu: 0,
memory: 0,
qps: 0,
responseTime: 0
}
})
this.updateQueue = []
this.isProcessing = false
}
// 处理数据推送
handleDataPush(data) {
const { type, value, timestamp } = data
switch (type) {
case 'sales':
this.updateSales(value)
break
case 'order':
this.updateOrders(value)
break
case 'user':
this.updateUsers(value)
break
case 'performance':
this.updatePerformance(value)
break
}
}
// 更新销售数据
updateSales(data) {
this.metrics.sales.today = data.today || this.metrics.sales.today
this.metrics.sales.yesterday = data.yesterday || this.metrics.sales.yesterday
if (data.amount) {
this.metrics.sales.trend.push({
time: new Date().toLocaleTimeString(),
value: data.amount
})
// 保留最近30个点
if (this.metrics.sales.trend.length > 30) {
this.metrics.sales.trend.shift()
}
}
}
// 更新订单数据
updateOrders(data) {
this.metrics.orders.total = data.total || this.metrics.orders.total
this.metrics.orders.pending = data.pending || this.metrics.orders.pending
this.metrics.orders.completed = data.completed || this.metrics.orders.completed
if (data.count) {
this.metrics.orders.trend.push({
time: new Date().toLocaleTimeString(),
value: data.count
})
if (this.metrics.orders.trend.length > 30) {
this.metrics.orders.trend.shift()
}
}
}
// 更新用户数据
updateUsers(data) {
this.metrics.users.online = data.online || this.metrics.users.online
this.metrics.users.total = data.total || this.metrics.users.total
this.metrics.users.newToday = data.newToday || this.metrics.users.newToday
if (data.onlineCount !== undefined) {
this.metrics.users.trend.push({
time: new Date().toLocaleTimeString(),
value: data.onlineCount
})
if (this.metrics.users.trend.length > 30) {
this.metrics.users.trend.shift()
}
}
}
// 更新性能数据
updatePerformance(data) {
this.metrics.performance.cpu = data.cpu || this.metrics.performance.cpu
this.metrics.performance.memory = data.memory || this.metrics.performance.memory
this.metrics.performance.qps = data.qps || this.metrics.performance.qps
this.metrics.performance.responseTime = data.responseTime || this.metrics.performance.responseTime
}
// 批量更新(优化性能)
batchUpdate(updates) {
this.updateQueue.push(...updates)
if (!this.isProcessing) {
this.processQueue()
}
}
// 处理更新队列
async processQueue() {
this.isProcessing = true
while (this.updateQueue.length > 0) {
const batch = this.updateQueue.splice(0, 10)
batch.forEach(update => {
this.handleDataPush(update)
})
// 等待下一帧
await new Promise(resolve => requestAnimationFrame(resolve))
}
this.isProcessing = false
}
// 获取指标数据
getMetrics() {
return this.metrics
}
}
export const dashboardManager = new DashboardManager()
3.2 实时数据看板组件
RealtimeDashboard.vue
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { dashboardManager } from './dashboard-manager.js'
import * as echarts from 'echarts'
const ws = ref(null)
const isConnected = ref(false)
const metrics = computed(() => dashboardManager.getMetrics())
// 图表实例
const salesChart = ref(null)
const ordersChart = ref(null)
const usersChart = ref(null)
// 初始化WebSocket
const initWebSocket = () => {
ws.value = new WebSocket('ws://localhost:8080/dashboard')
ws.value.onopen = () => {
isConnected.value = true
console.log('数据看板连接成功')
}
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data)
dashboardManager.handleDataPush(data)
updateCharts()
}
ws.value.onclose = () => {
isConnected.value = false
setTimeout(initWebSocket, 3000)
}
}
// 初始化图表
const initCharts = () => {
// 销售趋势图
const salesChartDom = document.getElementById('salesChart')
salesChart.value = echarts.init(salesChartDom)
salesChart.value.setOption({
title: { text: '销售趋势', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: [] },
yAxis: { type: 'value' },
series: [{
data: [],
type: 'line',
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.5)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
])
}
}]
})
// 订单趋势图
const ordersChartDom = document.getElementById('ordersChart')
ordersChart.value = echarts.init(ordersChartDom)
ordersChart.value.setOption({
title: { text: '订单趋势', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: [] },
yAxis: { type: 'value' },
series: [{
data: [],
type: 'line',
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103, 194, 58, 0.5)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' }
])
}
}]
})
// 在线用户图
const usersChartDom = document.getElementById('usersChart')
usersChart.value = echarts.init(usersChartDom)
usersChart.value.setOption({
title: { text: '在线用户', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: [] },
yAxis: { type: 'value' },
series: [{
data: [],
type: 'line',
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(230, 162, 60, 0.5)' },
{ offset: 1, color: 'rgba(230, 162, 60, 0.1)' }
])
}
}]
})
}
// 更新图表
const updateCharts = () => {
// 更新销售图表
if (salesChart.value && metrics.value.sales.trend.length > 0) {
salesChart.value.setOption({
xAxis: {
data: metrics.value.sales.trend.map(item => item.time)
},
series: [{
data: metrics.value.sales.trend.map(item => item.value)
}]
})
}
// 更新订单图表
if (ordersChart.value && metrics.value.orders.trend.length > 0) {
ordersChart.value.setOption({
xAxis: {
data: metrics.value.orders.trend.map(item => item.time)
},
series: [{
data: metrics.value.orders.trend.map(item => item.value)
}]
})
}
// 更新用户图表
if (usersChart.value && metrics.value.users.trend.length > 0) {
usersChart.value.setOption({
xAxis: {
data: metrics.value.users.trend.map(item => item.time)
},
series: [{
data: metrics.value.users.trend.map(item => item.value)
}]
})
}
}
// 模拟数据推送
const simulateDataPush = () => {
const types = ['sales', 'order', 'user', 'performance']
const type = types[Math.floor(Math.random() * types.length)]
let data = { type, timestamp: Date.now() }
switch (type) {
case 'sales':
data.value = {
today: Math.floor(Math.random() * 100000),
amount: Math.floor(Math.random() * 10000)
}
break
case 'order':
data.value = {
total: Math.floor(Math.random() * 1000),
pending: Math.floor(Math.random() * 100),
completed: Math.floor(Math.random() * 900),
count: Math.floor(Math.random() * 50)
}
break
case 'user':
data.value = {
online: Math.floor(Math.random() * 1000),
total: Math.floor(Math.random() * 10000),
newToday: Math.floor(Math.random() * 100),
onlineCount: Math.floor(Math.random() * 1000)
}
break
case 'performance':
data.value = {
cpu: Math.random() * 100,
memory: Math.random() * 100,
qps: Math.floor(Math.random() * 1000),
responseTime: Math.floor(Math.random() * 500)
}
break
}
dashboardManager.handleDataPush(data)
updateCharts()
}
// 开始自动推送
let pushInterval = null
const startAutoPush = () => {
pushInterval = setInterval(simulateDataPush, 2000)
}
const stopAutoPush = () => {
if (pushInterval) {
clearInterval(pushInterval)
pushInterval = null
}
}
onMounted(() => {
initWebSocket()
initCharts()
startAutoPush()
// 监听窗口大小变化
window.addEventListener('resize', () => {
salesChart.value?.resize()
ordersChart.value?.resize()
usersChart.value?.resize()
})
})
onUnmounted(() => {
stopAutoPush()
ws.value?.close()
salesChart.value?.dispose()
ordersChart.value?.dispose()
usersChart.value?.dispose()
})
</script>
<template>
<div class="realtime-dashboard">
<div class="dashboard-header">
<h2>实时数据看板</h2>
<div class="header-status">
<div class="status-badge" :class="{ connected: isConnected }">
{{ isConnected ? '实时更新中' : '连接断开' }}
</div>
</div>
</div>
<!-- 指标卡片 -->
<div class="metrics-grid">
<div class="metric-card sales">
<div class="metric-icon">💰</div>
<div class="metric-content">
<div class="metric-label">今日销售额</div>
<div class="metric-value">¥{{ metrics.sales.today.toLocaleString() }}</div>
<div class="metric-sub">
昨日: ¥{{ metrics.sales.yesterday.toLocaleString() }}
</div>
</div>
</div>
<div class="metric-card orders">
<div class="metric-icon">📦</div>
<div class="metric-content">
<div class="metric-label">订单统计</div>
<div class="metric-value">{{ metrics.orders.total }}</div>
<div class="metric-sub">
待处理: {{ metrics.orders.pending }} | 已完成: {{ metrics.orders.completed }}
</div>
</div>
</div>
<div class="metric-card users">
<div class="metric-icon">👥</div>
<div class="metric-content">
<div class="metric-label">在线用户</div>
<div class="metric-value">{{ metrics.users.online }}</div>
<div class="metric-sub">
总用户: {{ metrics.users.total }} | 今日新增: {{ metrics.users.newToday }}
</div>
</div>
</div>
<div class="metric-card performance">
<div class="metric-icon">⚡</div>
<div class="metric-content">
<div class="metric-label">系统性能</div>
<div class="metric-value">{{ metrics.performance.qps }} QPS</div>
<div class="metric-sub">
CPU: {{ metrics.performance.cpu.toFixed(1) }}% |
内存: {{ metrics.performance.memory.toFixed(1) }}%
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-grid">
<div class="chart-container">
<div id="salesChart" class="chart"></div>
</div>
<div class="chart-container">
<div id="ordersChart" class="chart"></div>
</div>
<div class="chart-container">
<div id="usersChart" class="chart"></div>
</div>
</div>
</div>
</template>
<style scoped>
.realtime-dashboard {
height: 100vh;
background: #f5f7fa;
padding: 20px;
overflow-y: auto;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.dashboard-header h2 {
margin: 0;
font-size: 24px;
color: #303133;
}
.status-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
background: #f56c6c;
color: white;
animation: pulse 2s infinite;
}
.status-badge.connected {
background: #67c23a;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* 指标卡片 */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.metric-card {
background: white;
border-radius: 8px;
padding: 24px;
display: flex;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.3s, box-shadow 0.3s;
}
.metric-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.metric-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
flex-shrink: 0;
}
.metric-card.sales .metric-icon {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.metric-card.orders .metric-icon {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.metric-card.users .metric-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.metric-card.performance .metric-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.metric-content {
flex: 1;
}
.metric-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.metric-value {
font-size: 28px;
font-weight: 700;
color: #303133;
margin-bottom: 8px;
}
.metric-sub {
font-size: 12px;
color: #c0c4cc;
}
/* 图表区域 */
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
}
.chart-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.chart {
width: 100%;
height: 300px;
}
</style>
四、在线状态显示
4.1 在线状态管理器
online-status-manager.js
import { ref, reactive } from 'vue'
export class OnlineStatusManager {
constructor() {
this.users = reactive(new Map())
this.heartbeatInterval = 30000 // 30秒心跳
this.offlineTimeout = 60000 // 60秒无心跳则离线
this.checkTimer = null
}
// 用户上线
userOnline(userId, userInfo = {}) {
const user = {
id: userId,
name: userInfo.name || `用户${userId}`,
avatar: userInfo.avatar,
status: 'online', // online, away, offline
lastActiveTime: Date.now(),
device: userInfo.device || 'web',
...userInfo
}
this.users.set(userId, user)
this.startHeartbeatCheck()
return user
}
// 用户离线
userOffline(userId) {
const user = this.users.get(userId)
if (user) {
user.status = 'offline'
user.offlineTime = Date.now()
}
}
// 更新心跳
updateHeartbeat(userId) {
const user = this.users.get(userId)
if (user) {
user.lastActiveTime = Date.now()
if (user.status === 'offline') {
user.status = 'online'
}
}
}
// 设置用户状态
setUserStatus(userId, status) {
const user = this.users.get(userId)
if (user) {
user.status = status
user.statusTime = Date.now()
}
}
// 获取用户状态
getUserStatus(userId) {
const user = this.users.get(userId)
return user ? user.status : 'offline'
}
// 获取所有在线用户
getOnlineUsers() {
return Array.from(this.users.values())
.filter(user => user.status === 'online')
}
// 获取用户数统计
getStats() {
const all = Array.from(this.users.values())
return {
total: all.length,
online: all.filter(u => u.status === 'online').length,
away: all.filter(u => u.status === 'away').length,
offline: all.filter(u => u.status === 'offline').length
}
}
// 启动心跳检查
startHeartbeatCheck() {
if (this.checkTimer) return
this.checkTimer = setInterval(() => {
const now = Date.now()
for (const [userId, user] of this.users) {
if (user.status === 'online') {
const timeSinceActive = now - user.lastActiveTime
// 超时未活跃,标记为离线
if (timeSinceActive > this.offlineTimeout) {
this.userOffline(userId)
}
}
}
}, 10000) // 每10秒检查一次
}
// 停止心跳检查
stopHeartbeatCheck() {
if (this.checkTimer) {
clearInterval(this.checkTimer)
this.checkTimer = null
}
}
// 清空所有用户
clear() {
this.users.clear()
this.stopHeartbeatCheck()
}
}
export const onlineStatusManager = new OnlineStatusManager()
4.2 在线状态组件
OnlineStatusView.vue
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onlineStatusManager } from './online-status-manager.js'
const ws = ref(null)
const isConnected = ref(false)
const currentUserId = ref('user_' + Date.now())
const users = ref([])
const filterStatus = ref('all') // all, online, away, offline
// 统计数据
const stats = computed(() => onlineStatusManager.getStats())
// 过滤后的用户列表
const filteredUsers = computed(() => {
if (filterStatus.value === 'all') {
return users.value
}
return users.value.filter(user => user.status === filterStatus.value)
})
// 初始化WebSocket
const initWebSocket = () => {
ws.value = new WebSocket('ws://localhost:8080/online-status')
ws.value.onopen = () => {
isConnected.value = true
// 发送上线消息
ws.value.send(JSON.stringify({
type: 'online',
userId: currentUserId.value,
userInfo: {
name: '当前用户',
device: 'web'
}
}))
startHeartbeat()
}
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data)
handleStatusMessage(data)
}
ws.value.onclose = () => {
isConnected.value = false
stopHeartbeat()
setTimeout(initWebSocket, 3000)
}
}
// 处理状态消息
const handleStatusMessage = (data) => {
switch (data.type) {
case 'user_online':
onlineStatusManager.userOnline(data.userId, data.userInfo)
break
case 'user_offline':
onlineStatusManager.userOffline(data.userId)
break
case 'status_change':
onlineStatusManager.setUserStatus(data.userId, data.status)
break
case 'heartbeat_ack':
onlineStatusManager.updateHeartbeat(currentUserId.value)
break
case 'users_list':
data.users.forEach(user => {
onlineStatusManager.userOnline(user.id, user)
})
break
}
loadUsers()
}
// 加载用户列表
const loadUsers = () => {
users.value = Array.from(onlineStatusManager.users.values())
.sort((a, b) => {
// 在线优先
if (a.status === 'online' && b.status !== 'online') return -1
if (a.status !== 'online' && b.status === 'online') return 1
// 按最后活跃时间排序
return b.lastActiveTime - a.lastActiveTime
})
}
// 心跳定时器
let heartbeatTimer = null
const startHeartbeat = () => {
heartbeatTimer = setInterval(() => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
ws.value.send(JSON.stringify({
type: 'heartbeat',
userId: currentUserId.value,
timestamp: Date.now()
}))
}
}, 30000)
}
const stopHeartbeat = () => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer)
heartbeatTimer = null
}
}
// 模拟用户上线/离线
const simulateUserStatus = () => {
const actions = ['online', 'offline', 'away']
const action = actions[Math.floor(Math.random() * actions.length)]
const userId = 'user_' + Math.floor(Math.random() * 100)
handleStatusMessage({
type: action === 'away' ? 'status_change' : `user_${action}`,
userId: userId,
status: action,
userInfo: {
name: `用户${userId.substr(-2)}`,
device: 'web'
}
})
}
// 获取状态样式
const getStatusStyle = (status) => {
const styles = {
online: { color: '#67c23a', text: '在线' },
away: { color: '#e6a23c', text: '离开' },
offline: { color: '#909399', text: '离线' }
}
return styles[status] || styles.offline
}
// 格式化时间
const formatTime = (timestamp) => {
const now = Date.now()
const diff = now - timestamp
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
return `${Math.floor(diff / 86400000)}天前`
}
onMounted(() => {
initWebSocket()
})
onUnmounted(() => {
stopHeartbeat()
ws.value?.close()
onlineStatusManager.clear()
})
</script>
<template>
<div class="online-status-view">
<div class="header">
<h2>在线状态监控</h2>
<div class="header-actions">
<button @click="simulateUserStatus" class="simulate-btn">
模拟状态变化
</button>
<div class="connection-badge" :class="{ connected: isConnected }">
{{ isConnected ? '已连接' : '未连接' }}
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card total">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">总用户数</div>
</div>
</div>
<div class="stat-card online">
<div class="stat-icon">🟢</div>
<div class="stat-content">
<div class="stat-value">{{ stats.online }}</div>
<div class="stat-label">在线</div>
</div>
</div>
<div class="stat-card away">
<div class="stat-icon">🟡</div>
<div class="stat-content">
<div class="stat-value">{{ stats.away }}</div>
<div class="stat-label">离开</div>
</div>
</div>
<div class="stat-card offline">
<div class="stat-icon">⚫</div>
<div class="stat-content">
<div class="stat-value">{{ stats.offline }}</div>
<div class="stat-label">离线</div>
</div>
</div>
</div>
<!-- 过滤标签 -->
<div class="filter-tabs">
<button
:class="{ active: filterStatus === 'all' }"
@click="filterStatus = 'all'"
>
全部 ({{ stats.total }})
</button>
<button
:class="{ active: filterStatus === 'online' }"
@click="filterStatus = 'online'"
>
在线 ({{ stats.online }})
</button>
<button
:class="{ active: filterStatus === 'away' }"
@click="filterStatus = 'away'"
>
离开 ({{ stats.away }})
</button>
<button
:class="{ active: filterStatus === 'offline' }"
@click="filterStatus = 'offline'"
>
离线 ({{ stats.offline }})
</button>
</div>
<!-- 用户列表 -->
<div class="user-list">
<div
v-for="user in filteredUsers"
:key="user.id"
class="user-item"
>
<div class="user-avatar">
{{ user.name.charAt(0) }}
<span
class="status-dot"
:style="{ background: getStatusStyle(user.status).color }"
></span>
</div>
<div class="user-info">
<div class="user-name">{{ user.name }}</div>
<div class="user-device">{{ user.device }}</div>
</div>
<div class="user-status">
<span
class="status-badge"
:style="{ color: getStatusStyle(user.status).color }"
>
{{ getStatusStyle(user.status).text }}
</span>
<span class="last-active">
{{ formatTime(user.lastActiveTime) }}
</span>
</div>
</div>
<div v-if="filteredUsers.length === 0" class="empty-list">
<div class="empty-icon">👤</div>
<div class="empty-text">暂无用户</div>
</div>
</div>
</div>
</template>
<style scoped>
.online-status-view {
height: 100vh;
background: #f5f7fa;
padding: 20px;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.header h2 {
margin: 0;
font-size: 24px;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.simulate-btn {
padding: 8px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.simulate-btn:hover {
background: #66b1ff;
}
.connection-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
background: #f56c6c;
color: white;
}
.connection-badge.connected {
background: #67c23a;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.stat-icon {
font-size: 36px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #303133;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
/* 过滤标签 */
.filter-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.filter-tabs button {
padding: 8px 20px;
background: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #606266;
transition: all 0.3s;
}
.filter-tabs button.active {
background: #409eff;
color: white;
border-color: #409eff;
}
/* 用户列表 */
.user-list {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.user-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.user-item:last-child {
border-bottom: none;
}
.user-avatar {
position: relative;
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
flex-shrink: 0;
}
.status-dot {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid white;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.user-device {
font-size: 13px;
color: #909399;
}
.user-status {
text-align: right;
}
.status-badge {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
.last-active {
font-size: 12px;
color: #c0c4cc;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-text {
font-size: 14px;
}
</style>
五、消息通知中心
5.1 通知中心管理器
notification-center-manager.js
import { ref, reactive } from 'vue'
export class NotificationCenterManager {
constructor() {
this.notifications = reactive([])
this.unreadCount = ref(0)
this.maxNotifications = 100
}
// 通知类型配置
typeConfig = {
system: {
icon: '⚙️',
color: '#909399',
title: '系统通知'
},
order: {
icon: '📦',
color: '#409eff',
title: '订单通知'
},
message: {
icon: '💬',
color: '#67c23a',
title: '消息通知'
},
alert: {
icon: '⚠️',
color: '#e6a23c',
title: '告警通知'
},
error: {
icon: '❌',
color: '#f56c6c',
title: '错误通知'
}
}
// 添加通知
addNotification(notification) {
const newNotification = {
id: notification.id || this.generateId(),
type: notification.type || 'system',
title: notification.title,
content: notification.content,
data: notification.data,
timestamp: notification.timestamp || Date.now(),
read: false,
priority: notification.priority || 'normal' // low, normal, high
}
this.notifications.unshift(newNotification)
this.unreadCount.value++
// 限制通知数量
if (this.notifications.length > this.maxNotifications) {
this.notifications.pop()
}
return newNotification
}
// 标记为已读
markAsRead(notificationId) {
const notification = this.notifications.find(n => n.id === notificationId)
if (notification && !notification.read) {
notification.read = true
this.unreadCount.value = Math.max(0, this.unreadCount.value - 1)
}
}
// 标记全部为已读
markAllAsRead() {
this.notifications.forEach(n => {
n.read = true
})
this.unreadCount.value = 0
}
// 删除通知
deleteNotification(notificationId) {
const index = this.notifications.findIndex(n => n.id === notificationId)
if (index > -1) {
const notification = this.notifications[index]
if (!notification.read) {
this.unreadCount.value = Math.max(0, this.unreadCount.value - 1)
}
this.notifications.splice(index, 1)
}
}
// 清空所有通知
clearAll() {
this.notifications.length = 0
this.unreadCount.value = 0
}
// 获取未读通知
getUnreadNotifications() {
return this.notifications.filter(n => !n.read)
}
// 获取通知配置
getTypeConfig(type) {
return this.typeConfig[type] || this.typeConfig.system
}
// 生成ID
generateId() {
return `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
}
export const notificationCenterManager = new NotificationCenterManager()
5.2 通知中心组件
NotificationCenter.vue
<script setup>
import { ref, computed, onMounted } from 'vue'
import { notificationCenterManager } from './notification-center-manager.js'
const ws = ref(null)
const isConnected = ref(false)
const isOpen = ref(false)
const filterType = ref('all')
const notifications = computed(() => notificationCenterManager.notifications)
const unreadCount = computed(() => notificationCenterManager.unreadCount.value)
// 过滤后的通知
const filteredNotifications = computed(() => {
if (filterType.value === 'all') {
return notifications.value
}
if (filterType.value === 'unread') {
return notifications.value.filter(n => !n.read)
}
return notifications.value.filter(n => n.type === filterType.value)
})
// 初始化WebSocket
const initWebSocket = () => {
ws.value = new WebSocket('ws://localhost:8080/notifications')
ws.value.onopen = () => {
isConnected.value = true
}
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data)
handleNotification(data)
}
ws.value.onclose = () => {
isConnected.value = false
setTimeout(initWebSocket, 3000)
}
}
// 处理通知
const handleNotification = (data) => {
notificationCenterManager.addNotification(data)
// 播放提示音
playNotificationSound()
// 显示桌面通知
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(data.title, {
body: data.content,
icon: '/notification-icon.png'
})
}
}
// 播放提示音
const playNotificationSound = () => {
const audio = new Audio('/notification.mp3')
audio.play().catch(err => console.log('播放失败:', err))
}
// 打开/关闭通知中心
const toggleNotificationCenter = () => {
isOpen.value = !isOpen.value
}
// 标记为已读
const markAsRead = (notification) => {
notificationCenterManager.markAsRead(notification.id)
}
// 标记全部已读
const markAllAsRead = () => {
notificationCenterManager.markAllAsRead()
}
// 删除通知
const deleteNotification = (notification, event) => {
event.stopPropagation()
notificationCenterManager.deleteNotification(notification.id)
}
// 清空所有
const clearAll = () => {
if (confirm('确定要清空所有通知吗?')) {
notificationCenterManager.clearAll()
}
}
// 格式化时间
const formatTime = (timestamp) => {
const now = Date.now()
const diff = now - timestamp
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
return new Date(timestamp).toLocaleDateString()
}
// 模拟通知
const simulateNotification = () => {
const types = ['system', 'order', 'message', 'alert', 'error']
const type = types[Math.floor(Math.random() * types.length)]
const config = notificationCenterManager.getTypeConfig(type)
handleNotification({
type: type,
title: config.title,
content: `这是一条${config.title}的内容`,
priority: Math.random() > 0.7 ? 'high' : 'normal'
})
}
onMounted(() => {
initWebSocket()
// 请求通知权限
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission()
}
})
</script>
<template>
<div class="notification-center">
<!-- 通知按钮 -->
<div class="notification-trigger" @click="toggleNotificationCenter">
<div class="notification-icon">🔔</div>
<div v-if="unreadCount > 0" class="badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</div>
</div>
<!-- 通知面板 -->
<transition name="slide">
<div v-show="isOpen" class="notification-panel">
<div class="panel-header">
<h3>通知中心</h3>
<button @click="simulateNotification" class="simulate-btn">
模拟
</button>
<button @click="toggleNotificationCenter" class="close-btn">
✕
</button>
</div>
<div class="panel-filters">
<button
:class="{ active: filterType === 'all' }"
@click="filterType = 'all'"
>
全部 ({{ notifications.length }})
</button>
<button
:class="{ active: filterType === 'unread' }"
@click="filterType = 'unread'"
>
未读 ({{ unreadCount }})
</button>
<button
:class="{ active: filterType === 'system' }"
@click="filterType = 'system'"
>
系统
</button>
<button
:class="{ active: filterType === 'order' }"
@click="filterType = 'order'"
>
订单
</button>
<button
:class="{ active: filterType === 'message' }"
@click="filterType = 'message'"
>
消息
</button>
</div>
<div class="panel-actions">
<button @click="markAllAsRead" :disabled="unreadCount === 0">
全部已读
</button>
<button @click="clearAll" :disabled="notifications.length === 0">
清空
</button>
</div>
<div class="notification-list">
<div
v-for="notification in filteredNotifications"
:key="notification.id"
:class="['notification-item', { unread: !notification.read }]"
@click="markAsRead(notification)"
>
<div class="notification-icon-wrapper">
<span
class="type-icon"
:style="{ color: notificationCenterManager.getTypeConfig(notification.type).color }"
>
{{ notificationCenterManager.getTypeConfig(notification.type).icon }}
</span>
</div>
<div class="notification-content">
<div class="notification-title">
{{ notification.title }}
<span v-if="notification.priority === 'high'" class="priority-badge">
重要
</span>
</div>
<div class="notification-text">
{{ notification.content }}
</div>
<div class="notification-time">
{{ formatTime(notification.timestamp) }}
</div>
</div>
<button
class="delete-btn"
@click="deleteNotification(notification, $event)"
>
✕
</button>
</div>
<div v-if="filteredNotifications.length === 0" class="empty-notifications">
<div class="empty-icon">📭</div>
<div class="empty-text">暂无通知</div>
</div>
</div>
</div>
</transition>
<!-- 遮罩层 -->
<transition name="fade">
<div v-show="isOpen" class="overlay" @click="toggleNotificationCenter"></div>
</transition>
</div>
</template>
<style scoped>
.notification-center {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.notification-trigger {
position: relative;
width: 48px;
height: 48px;
background: white;
border-radius: 50%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.3s;
}
.notification-trigger:hover {
transform: scale(1.1);
}
.notification-icon {
font-size: 24px;
}
.badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 20px;
height: 20px;
background: #f56c6c;
color: white;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
padding: 0 6px;
box-shadow: 0 2px 4px rgba(245, 108, 108, 0.4);
}
.notification-panel {
position: fixed;
top: 80px;
right: 20px;
width: 400px;
max-height: 600px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
gap: 12px;
}
.panel-header h3 {
flex: 1;
margin: 0;
font-size: 18px;
color: #303133;
}
.simulate-btn,
.close-btn {
width: 32px;
height: 32px;
border: none;
background: #f5f7fa;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #606266;
}
.simulate-btn:hover,
.close-btn:hover {
background: #e4e7ed;
}
.panel-filters {
padding: 12px 20px;
display: flex;
gap: 8px;
overflow-x: auto;
border-bottom: 1px solid #e4e7ed;
}
.panel-filters button {
padding: 4px 12px;
background: #f5f7fa;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 13px;
color: #606266;
white-space: nowrap;
transition: all 0.3s;
}
.panel-filters button.active {
background: #409eff;
color: white;
}
.panel-actions {
padding: 12px 20px;
display: flex;
gap: 12px;
border-bottom: 1px solid #e4e7ed;
}
.panel-actions button {
padding: 6px 16px;
background: #ecf5ff;
color: #409eff;
border: 1px solid #d9ecff;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.panel-actions button:hover:not(:disabled) {
background: #d9ecff;
}
.panel-actions button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.notification-list {
flex: 1;
overflow-y: auto;
}
.notification-item {
display: flex;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
position: relative;
}
.notification-item:hover {
background: #f5f7fa;
}
.notification-item.unread {
background: #ecf5ff;
}
.notification-item.unread::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #409eff;
}
.notification-icon-wrapper {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.type-icon {
font-size: 20px;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.priority-badge {
padding: 2px 6px;
background: #f56c6c;
color: white;
border-radius: 8px;
font-size: 11px;
font-weight: 600;
}
.notification-text {
font-size: 13px;
color: #606266;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-time {
font-size: 12px;
color: #c0c4cc;
}
.delete-btn {
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
color: #c0c4cc;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.2s;
}
.notification-item:hover .delete-btn {
opacity: 1;
}
.delete-btn:hover {
color: #f56c6c;
}
.empty-notifications {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-text {
font-size: 14px;
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999;
}
/* 动画 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s;
}
.slide-enter-from {
opacity: 0;
transform: translateY(-20px);
}
.slide-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
六、简历描述模板
实时推送系统开发 (2023.09 - 2024.01)
负责公司多个业务场景的实时推送功能开发,包括订单状态、数据看板、在线状态和通知中心,日处理推送消息10万+条。
核心职责
- 开发订单状态实时推送系统,支持订单创建、状态变更、物流更新等多种事件推送
- 实现基于ECharts的实时数据看板,支持销售、订单、用户等多维度数据可视化
- 开发用户在线状态监控系统,实现30秒心跳检测和自动离线判定
- 构建统一的消息通知中心,支持系统、业务、告警等多类型通知管理
技术实现
- 使用WebSocket实现服务端主动推送,推送延迟控制在100ms以内
- 通过reactive响应式数据管理,实现UI的自动更新
- 使用requestAnimationFrame优化批量数据更新,避免页面卡顿
- 集成Notification API和提示音,提升消息触达率至95%
项目成果
- 订单状态推送准确率99.9%
- 数据看板支持最高1000点/秒的数据更新
- 在线状态监控误差率低于1%
- 通知中心日均处理通知10万+条,用户满意度提升30%
七、SOP标准回答
面试问题: 介绍一下实时数据看板的实现
标准回答
"我开发的实时数据看板主要用于展示销售、订单、用户等业务数据,特点是高频更新、低延迟。
技术架构上,前端通过WebSocket建立长连接,后端采用发布订阅模式,有新数据就主动推送。我设计了一个DashboardManager统一管理所有指标数据,使用Vue 3的reactive API实现响应式更新。
性能优化是重点。首先是批量更新,我维护了一个更新队列,用requestAnimationFrame分帧处理,每帧最多更新10条数据,避免一次性更新太多导致卡顿。其次是数据精简,服务端只推送变化的数据,不是完整数据集,减少网络传输。
图表渲染用的ECharts。我做了两个优化:一是数据点控制,每个图表最多保留30个数据点,超出就删除最早的;二是延迟渲染,收到推送后不是立即更新图表,而是等到下一帧再批量更新所有图表。
指标展示分为卡片和图表两部分。卡片显示当前值和对比值,比如今日销售额vs昨日。图表显示趋势,用面积图更直观。所有数据都是实时更新的,用户能看到数据的变化动画。
实际效果是,支持每秒1000个数据点的更新,CPU占用率控制在30%以内,用户体验很流畅。"
八、难点与亮点分析
难点1: 如何优化高频数据更新性能?
问题: 数据看板每秒可能收到上百条更新,直接更新会导致页面卡顿。
解决方案
- 更新队列 + 分帧处理
async processQueue() {
while (this.updateQueue.length > 0) {
const batch = this.updateQueue.splice(0, 10)
batch.forEach(update => {
this.handleDataPush(update)
})
// 等待下一帧
await new Promise(resolve => requestAnimationFrame(resolve))
}
}
- 数据点限制
if (this.metrics.sales.trend.length > 30) {
this.metrics.sales.trend.shift()
}
- 节流更新
const throttledUpdate = throttle(() => {
updateCharts()
}, 1000)
亮点1: 智能通知优先级
创新点
- 根据通知类型自动分级
- 高优先级通知置顶显示
- 智能合并相似通知
实现
addNotification(notification) {
// 检查是否有相似通知
const similar = this.findSimilarNotification(notification)
if (similar) {
similar.count = (similar.count || 1) + 1
similar.timestamp = Date.now()
return similar
}
// 根据优先级插入
const insertIndex = this.notifications.findIndex(
n => n.priority < notification.priority
)
if (insertIndex >= 0) {
this.notifications.splice(insertIndex, 0, notification)
} else {
this.notifications.push(notification)
}
}
亮点2: 断线自动恢复机制
创新点
- WebSocket断线后自动重连
- 重连成功后拉取遗漏数据
- 用户无感知的数据补偿
实现
ws.onclose = () => {
const lastTimestamp = this.getLastDataTimestamp()
setTimeout(() => {
this.reconnect()
// 重连成功后拉取遗漏数据
this.fetchMissedData(lastTimestamp)
}, 3000)
}