|
|
@@ -0,0 +1,694 @@
|
|
|
+<template>
|
|
|
+ <div class="behavior-screen">
|
|
|
+ <!-- 顶部标题栏 -->
|
|
|
+ <div class="screen-header">
|
|
|
+ <div class="header-title">
|
|
|
+ <h1>直播行为实时大屏</h1>
|
|
|
+ <div class="live-info">
|
|
|
+ <span class="live-id">直播间ID: {{ liveId || '未连接' }}</span>
|
|
|
+ <span class="connection-status" :class="{ 'connected': isConnected, 'disconnected': !isConnected }">
|
|
|
+ {{ isConnected ? '已连接 🟢' : '未连接 🔴' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 行为列表容器 -->
|
|
|
+ <div class="behavior-container">
|
|
|
+ <!-- 空数据提示 -->
|
|
|
+ <div v-if="behaviorList.length === 0" class="empty-data">
|
|
|
+ <div class="empty-icon">📊</div>
|
|
|
+ <div class="empty-text">暂无用户行为数据</div>
|
|
|
+ <div class="empty-status">
|
|
|
+ <span>连接状态:{{ isConnected ? '已连接 🟢' : '连接中...' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="empty-tip">等待用户行为推送...</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 行为列表 -->
|
|
|
+ <div v-else class="behavior-list">
|
|
|
+ <transition-group name="behavior-fade" tag="div">
|
|
|
+ <div
|
|
|
+ v-for="(behavior, index) in behaviorList"
|
|
|
+ :key="behavior.id"
|
|
|
+ class="behavior-item"
|
|
|
+ :class="getBehaviorClass(behavior.behaviorDesc)"
|
|
|
+ :style="{ animationDelay: `${index * 0.05}s` }"
|
|
|
+ >
|
|
|
+ <div class="behavior-user">
|
|
|
+ <span class="user-id">{{ formatUserId(behavior.userId) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="behavior-desc">
|
|
|
+ <span class="desc-icon">{{ getBehaviorIcon(behavior.behaviorDesc) }}</span>
|
|
|
+ <span class="desc-text">{{ behavior.behaviorDesc }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="behavior-time">
|
|
|
+ {{ formatTime(behavior.behaviorTime) }}
|
|
|
+ </div>
|
|
|
+ <div v-if="behavior.resourceId" class="behavior-resource">
|
|
|
+ 资源ID: {{ behavior.resourceId }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </transition-group>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 统计信息 -->
|
|
|
+ <div class="stats-footer">
|
|
|
+ <div class="stat-item">
|
|
|
+ <span class="stat-label">实时在线</span>
|
|
|
+ <span class="stat-value">{{ onlineCount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <span class="stat-label">行为总数</span>
|
|
|
+ <span class="stat-value">{{ totalBehaviorCount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <span class="stat-label">最近更新</span>
|
|
|
+ <span class="stat-value">{{ lastUpdateTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import CryptoJS from 'crypto-js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'LiveBehaviorScreen',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ liveId: null, // 直播间ID
|
|
|
+ websocket: null, // WebSocket连接
|
|
|
+ isConnected: false, // 连接状态
|
|
|
+ behaviorList: [], // 行为列表
|
|
|
+ maxBehaviorCount: 100, // 最多展示100条
|
|
|
+ heartbeatTimer: null, // 心跳定时器
|
|
|
+ reconnectTimer: null, // 重连定时器
|
|
|
+ isManualClose: false, // 是否手动关闭
|
|
|
+ onlineCount: 0, // 在线人数
|
|
|
+ totalBehaviorCount: 0, // 行为总数
|
|
|
+ lastUpdateTime: '--:--:--', // 最后更新时间
|
|
|
+ wsUrl: 'ws.moonxiang.com' // WebSocket域名
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ // 从URL参数获取liveId
|
|
|
+ this.liveId = this.$route.query.liveId || null
|
|
|
+
|
|
|
+ if (!this.liveId) {
|
|
|
+ this.$message.error('缺少直播间ID参数')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化WebSocket连接
|
|
|
+ this.connectWebSocket()
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 组件销毁前关闭连接
|
|
|
+ this.closeWebSocket()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 建立WebSocket连接
|
|
|
+ */
|
|
|
+ connectWebSocket() {
|
|
|
+ try {
|
|
|
+ // 生成签名参数
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+ const userId = -1 // 大屏固定使用-1
|
|
|
+ const userType = 2 // 大屏标识
|
|
|
+
|
|
|
+ // 生成HmacSHA256签名
|
|
|
+ const signature = CryptoJS.HmacSHA256(
|
|
|
+ CryptoJS.enc.Utf8.parse(`${this.liveId}${userId}${userType}${timestamp}`),
|
|
|
+ CryptoJS.enc.Utf8.parse(String(timestamp))
|
|
|
+ ).toString(CryptoJS.enc.Hex)
|
|
|
+
|
|
|
+ // 构建WebSocket URL
|
|
|
+ const wsUrl = `wss://${this.wsUrl}/ws/app/webSocket?liveId=${this.liveId}&userId=${userId}&userType=${userType}×tamp=${timestamp}&signature=${signature}`
|
|
|
+
|
|
|
+ console.log('连接WebSocket:', wsUrl)
|
|
|
+
|
|
|
+ // 创建WebSocket连接
|
|
|
+ this.websocket = new WebSocket(wsUrl)
|
|
|
+
|
|
|
+ // 连接打开
|
|
|
+ this.websocket.onopen = this.onWebSocketOpen
|
|
|
+
|
|
|
+ // 接收消息
|
|
|
+ this.websocket.onmessage = this.onWebSocketMessage
|
|
|
+
|
|
|
+ // 连接错误
|
|
|
+ this.websocket.onerror = this.onWebSocketError
|
|
|
+
|
|
|
+ // 连接关闭
|
|
|
+ this.websocket.onclose = this.onWebSocketClose
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('WebSocket连接失败:', error)
|
|
|
+ this.$message.error('WebSocket连接失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * WebSocket连接打开
|
|
|
+ */
|
|
|
+ onWebSocketOpen() {
|
|
|
+ console.log('WebSocket连接成功')
|
|
|
+ this.isConnected = true
|
|
|
+ this.$message.success('大屏连接成功')
|
|
|
+
|
|
|
+ // 清除重连定时器
|
|
|
+ if (this.reconnectTimer) {
|
|
|
+ clearTimeout(this.reconnectTimer)
|
|
|
+ this.reconnectTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启动心跳
|
|
|
+ this.startHeartbeat()
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * WebSocket接收消息
|
|
|
+ */
|
|
|
+ onWebSocketMessage(event) {
|
|
|
+ try {
|
|
|
+ const response = JSON.parse(event.data)
|
|
|
+
|
|
|
+ // 只处理行为轨迹批量推送消息
|
|
|
+ if (response.data?.cmd === 'behaviorTrackBatch') {
|
|
|
+ const behaviors = JSON.parse(response.data.data)
|
|
|
+ this.handleBehaviorData(behaviors)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理在线人数更新
|
|
|
+ if (response.data?.cmd === 'onlineCount') {
|
|
|
+ this.onlineCount = response.data.data || 0
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析WebSocket消息失败:', error)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * WebSocket连接错误
|
|
|
+ */
|
|
|
+ onWebSocketError(error) {
|
|
|
+ console.error('WebSocket错误:', error)
|
|
|
+ this.isConnected = false
|
|
|
+
|
|
|
+ // 3秒后重连
|
|
|
+ this.scheduleReconnect()
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * WebSocket连接关闭
|
|
|
+ */
|
|
|
+ onWebSocketClose() {
|
|
|
+ console.log('WebSocket连接关闭')
|
|
|
+ this.isConnected = false
|
|
|
+
|
|
|
+ // 停止心跳
|
|
|
+ this.stopHeartbeat()
|
|
|
+
|
|
|
+ // 如果不是手动关闭,则重连
|
|
|
+ if (!this.isManualClose) {
|
|
|
+ this.scheduleReconnect()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理行为数据
|
|
|
+ */
|
|
|
+ handleBehaviorData(behaviors) {
|
|
|
+ if (!Array.isArray(behaviors) || behaviors.length === 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 为每个行为添加唯一ID
|
|
|
+ const newBehaviors = behaviors.map(behavior => ({
|
|
|
+ ...behavior,
|
|
|
+ id: `${behavior.userId}_${behavior.behaviorTime}_${Math.random()}`
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 将新行为添加到列表顶部
|
|
|
+ this.behaviorList.unshift(...newBehaviors)
|
|
|
+
|
|
|
+ // 限制最多100条
|
|
|
+ if (this.behaviorList.length > this.maxBehaviorCount) {
|
|
|
+ this.behaviorList = this.behaviorList.slice(0, this.maxBehaviorCount)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新统计信息
|
|
|
+ this.totalBehaviorCount += newBehaviors.length
|
|
|
+ this.lastUpdateTime = this.formatTime(Date.now())
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动心跳
|
|
|
+ */
|
|
|
+ startHeartbeat() {
|
|
|
+ this.stopHeartbeat() // 先清除旧的定时器
|
|
|
+
|
|
|
+ this.heartbeatTimer = setInterval(() => {
|
|
|
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
|
+ this.websocket.send(JSON.stringify({
|
|
|
+ cmd: 'heartbeat',
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: -1,
|
|
|
+ userType: 2
|
|
|
+ }))
|
|
|
+ console.log('发送心跳')
|
|
|
+ }
|
|
|
+ }, 30000) // 每30秒发送一次
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 停止心跳
|
|
|
+ */
|
|
|
+ stopHeartbeat() {
|
|
|
+ if (this.heartbeatTimer) {
|
|
|
+ clearInterval(this.heartbeatTimer)
|
|
|
+ this.heartbeatTimer = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调度重连
|
|
|
+ */
|
|
|
+ scheduleReconnect() {
|
|
|
+ if (this.reconnectTimer) {
|
|
|
+ return // 避免重复重连
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('3秒后重连...')
|
|
|
+ this.$message.warning('连接断开,3秒后重连...')
|
|
|
+
|
|
|
+ this.reconnectTimer = setTimeout(() => {
|
|
|
+ this.reconnectTimer = null
|
|
|
+ this.connectWebSocket()
|
|
|
+ }, 3000)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭WebSocket连接
|
|
|
+ */
|
|
|
+ closeWebSocket() {
|
|
|
+ this.isManualClose = true
|
|
|
+ this.stopHeartbeat()
|
|
|
+
|
|
|
+ if (this.reconnectTimer) {
|
|
|
+ clearTimeout(this.reconnectTimer)
|
|
|
+ this.reconnectTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.websocket) {
|
|
|
+ this.websocket.close()
|
|
|
+ this.websocket = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取行为类型对应的CSS类
|
|
|
+ */
|
|
|
+ getBehaviorClass(behaviorDesc) {
|
|
|
+ const typeMap = {
|
|
|
+ '点赞': 'behavior-like',
|
|
|
+ '发送消息': 'behavior-message',
|
|
|
+ '点击商品': 'behavior-goods',
|
|
|
+ '下单': 'behavior-order',
|
|
|
+ '领取红包': 'behavior-redpacket',
|
|
|
+ '参与抽奖': 'behavior-lottery',
|
|
|
+ '领取优惠券': 'behavior-coupon',
|
|
|
+ '进入直播间': 'behavior-enter',
|
|
|
+ '离开直播间': 'behavior-leave'
|
|
|
+ }
|
|
|
+ return typeMap[behaviorDesc] || ''
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取行为类型对应的图标
|
|
|
+ */
|
|
|
+ getBehaviorIcon(behaviorDesc) {
|
|
|
+ const iconMap = {
|
|
|
+ '点赞': '❤️',
|
|
|
+ '发送消息': '💬',
|
|
|
+ '点击商品': '🛍️',
|
|
|
+ '下单': '✅',
|
|
|
+ '领取红包': '🧧',
|
|
|
+ '参与抽奖': '🎲',
|
|
|
+ '领取优惠券': '🎟️',
|
|
|
+ '进入直播间': '👋',
|
|
|
+ '离开直播间': '👋'
|
|
|
+ }
|
|
|
+ return iconMap[behaviorDesc] || '📌'
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化用户ID(过长时截取)
|
|
|
+ */
|
|
|
+ formatUserId(userId) {
|
|
|
+ if (!userId) return '--'
|
|
|
+ const userIdStr = String(userId)
|
|
|
+ if (userIdStr.length > 10) {
|
|
|
+ return `${userIdStr.substring(0, 5)}...${userIdStr.substring(userIdStr.length - 4)}`
|
|
|
+ }
|
|
|
+ return userIdStr
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化时间为 HH:mm:ss
|
|
|
+ */
|
|
|
+ formatTime(timestamp) {
|
|
|
+ if (!timestamp) return '--:--:--'
|
|
|
+
|
|
|
+ const date = new Date(timestamp)
|
|
|
+ const hours = String(date.getHours()).padStart(2, '0')
|
|
|
+ const minutes = String(date.getMinutes()).padStart(2, '0')
|
|
|
+ const seconds = String(date.getSeconds()).padStart(2, '0')
|
|
|
+
|
|
|
+ return `${hours}:${minutes}:${seconds}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 全局样式 */
|
|
|
+.behavior-screen {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
|
+ color: #e0e0e0;
|
|
|
+ font-family: 'Microsoft YaHei', Arial, sans-serif;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+/* 顶部标题栏 */
|
|
|
+.screen-header {
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-bottom: 2px solid rgba(83, 82, 237, 0.3);
|
|
|
+ padding: 20px 40px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.header-title {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.header-title h1 {
|
|
|
+ font-size: 36px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin: 0;
|
|
|
+ background: linear-gradient(90deg, #5352ed, #3742fa);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+ text-shadow: 0 0 30px rgba(83, 82, 237, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.live-info {
|
|
|
+ display: flex;
|
|
|
+ gap: 30px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.live-id {
|
|
|
+ color: #70a1ff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status {
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status.connected {
|
|
|
+ background: rgba(46, 213, 115, 0.2);
|
|
|
+ color: #2ed573;
|
|
|
+ box-shadow: 0 0 15px rgba(46, 213, 115, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status.disconnected {
|
|
|
+ background: rgba(255, 71, 87, 0.2);
|
|
|
+ color: #ff4757;
|
|
|
+}
|
|
|
+
|
|
|
+/* 行为容器 */
|
|
|
+.behavior-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 20px 40px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+/* 自定义滚动条 */
|
|
|
+.behavior-container::-webkit-scrollbar {
|
|
|
+ width: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-container::-webkit-scrollbar-track {
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-container::-webkit-scrollbar-thumb {
|
|
|
+ background: rgba(83, 82, 237, 0.5);
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-container::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: rgba(83, 82, 237, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+/* 空数据提示 */
|
|
|
+.empty-data {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-icon {
|
|
|
+ font-size: 80px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ animation: float 3s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes float {
|
|
|
+ 0%, 100% {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: translateY(-20px);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.empty-text {
|
|
|
+ font-size: 28px;
|
|
|
+ color: #a0a0a0;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-status {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #70a1ff;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-tip {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #808080;
|
|
|
+}
|
|
|
+
|
|
|
+/* 行为列表 */
|
|
|
+.behavior-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 行为项 */
|
|
|
+.behavior-item {
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px 30px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 30px;
|
|
|
+ border-left: 4px solid transparent;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ animation: slideIn 0.5s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes slideIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateX(-50px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateX(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-item:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ transform: translateX(5px);
|
|
|
+}
|
|
|
+
|
|
|
+/* 行为类型样式 */
|
|
|
+.behavior-like {
|
|
|
+ border-left-color: #ff4757;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-message {
|
|
|
+ border-left-color: #5352ed;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-goods {
|
|
|
+ border-left-color: #ffa502;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-order {
|
|
|
+ border-left-color: #2ed573;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-redpacket {
|
|
|
+ border-left-color: #ffd700;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-lottery {
|
|
|
+ border-left-color: #a55eea;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-coupon {
|
|
|
+ border-left-color: #ff6b81;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-enter {
|
|
|
+ border-left-color: #70a1ff;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-leave {
|
|
|
+ border-left-color: #747d8c;
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户信息 */
|
|
|
+.behavior-user {
|
|
|
+ min-width: 150px;
|
|
|
+}
|
|
|
+
|
|
|
+.user-id {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #70a1ff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 行为描述 */
|
|
|
+.behavior-desc {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.desc-icon {
|
|
|
+ font-size: 28px;
|
|
|
+}
|
|
|
+
|
|
|
+.desc-text {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #e0e0e0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 时间 */
|
|
|
+.behavior-time {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #a0a0a0;
|
|
|
+ font-family: 'Consolas', monospace;
|
|
|
+ min-width: 100px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+/* 资源ID */
|
|
|
+.behavior-resource {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #808080;
|
|
|
+ min-width: 120px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+/* 淡入淡出动画 */
|
|
|
+.behavior-fade-enter-active {
|
|
|
+ animation: fadeIn 0.5s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.behavior-fade-leave-active {
|
|
|
+ animation: fadeOut 0.3s ease-in;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-20px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes fadeOut {
|
|
|
+ from {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 统计信息底栏 */
|
|
|
+.stats-footer {
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-top: 2px solid rgba(83, 82, 237, 0.3);
|
|
|
+ padding: 20px 40px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #a0a0a0;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #5352ed;
|
|
|
+ font-family: 'Consolas', monospace;
|
|
|
+}
|
|
|
+</style>
|