Files
a_long_vue/src/views/Layout.vue
2026-06-27 01:38:21 +08:00

685 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-container class="layout">
<el-aside :width="isCollapse ? '72px' : '240px'" class="aside" :class="{ collapsed: isCollapse }">
<div class="logo-area">
<span class="logo-text" v-if="!isCollapse">阿龙·终端系统</span>
<span class="logo-icon" v-else></span>
</div>
<el-menu
:default-active="$route.path"
class="el-menu-vertical"
:collapse="isCollapse"
:collapse-transition="false"
router
background-color="transparent"
text-color="rgba(0,242,255,0.7)"
active-text-color="#00f2ff"
>
<!-- 板块与财务 -->
<el-sub-menu index="/finance">
<template #title>
<el-icon><DataAnalysis /></el-icon>
<span>板块与财务</span>
</template>
<el-menu-item index="/finance/bankuai">板块配置</el-menu-item>
<el-menu-item index="/finance/data">财务数据</el-menu-item>
</el-sub-menu>
<!-- 考核管理 -->
<el-sub-menu index="/assessment">
<template #title>
<el-icon><Medal /></el-icon>
<span>考核管理</span>
</template>
<el-menu-item index="/assessment/examiner">考核官管理</el-menu-item>
<el-menu-item index="/assessment/config">考核配置</el-menu-item>
<el-menu-item index="/assessment/record">考核记录</el-menu-item>
</el-sub-menu>
<!-- 订单管理 -->
<el-sub-menu index="/order">
<template #title>
<el-icon><List /></el-icon>
<span>订单管理</span>
</template>
<el-menu-item index="/order/platform">平台订单</el-menu-item>
<el-menu-item index="/order/merchant">商家订单</el-menu-item>
</el-sub-menu>
<!-- 跨平台管理 -->
<el-sub-menu index="/cross-platform">
<template #title>
<el-icon><Connection /></el-icon>
<span>跨平台管理</span>
</template>
<el-menu-item index="/cross-order">跨平台订单</el-menu-item>
<el-menu-item index="/cross-settings">跨平台设置</el-menu-item>
<el-menu-item index="/cross-data">跨平台数据</el-menu-item>
<el-menu-item index="/cross-pre">预结算管理</el-menu-item>
</el-sub-menu>
<!-- 用户管理 -->
<el-sub-menu index="/user">
<template #title>
<el-icon><User /></el-icon>
<span>用户管理</span>
</template>
<el-menu-item index="/user/player">打手管理</el-menu-item>
<el-menu-item index="/user/guanli">管事管理</el-menu-item>
<el-menu-item index="/user/shangjia">商家管理</el-menu-item>
<el-menu-item index="/user/zuzhang">组长管理</el-menu-item>
</el-sub-menu>
<!-- 提现管理 -->
<el-sub-menu index="/withdraw">
<template #title>
<el-icon><Money /></el-icon>
<span>提现管理</span>
</template>
<el-menu-item index="/withdraw/audit">提现审核</el-menu-item>
<el-menu-item index="/withdraw/data">提现数据</el-menu-item>
<el-menu-item index="/withdraw/settings">提现设置</el-menu-item>
</el-sub-menu>
<!-- 管理员管理 -->
<el-sub-menu index="/admin">
<template #title>
<el-icon><Setting /></el-icon>
<span>管理员管理</span>
</template>
<el-menu-item index="/admin/role">角色管理</el-menu-item>
<el-menu-item index="/admin/user">后台用户</el-menu-item>
</el-sub-menu>
<!-- 处罚管理 -->
<el-menu-item index="/punishment">
<el-icon><Warning /></el-icon>
<span>处罚管理</span>
</el-menu-item>
<!-- 商品管理 -->
<el-sub-menu index="/product">
<template #title>
<el-icon><Goods /></el-icon>
<span>商品管理</span>
</template>
<el-menu-item index="/product/list">商品列表</el-menu-item>
<el-menu-item index="/product/type-zone">商品类型专区管理</el-menu-item>
<el-menu-item index="/product/data">商品数据分析</el-menu-item>
</el-sub-menu>
<!-- 会员管理 -->
<el-sub-menu index="/member">
<template #title>
<el-icon><UserFilled /></el-icon>
<span>会员管理</span>
</template>
<el-menu-item index="/member/list">会员管理</el-menu-item>
<el-menu-item index="/member/data">会员数据分析</el-menu-item>
</el-sub-menu>
<!-- 小程序配置 -->
<el-sub-menu index="/miniapp">
<template #title>
<el-icon><Setting /></el-icon>
<span>小程序配置</span>
</template>
<el-menu-item index="/miniapp/popup-notice">弹窗公告管理</el-menu-item>
<el-menu-item index="/miniapp/script-config">自动弹窗及话术配置</el-menu-item>
<el-menu-item index="/miniapp/sys-config">微信与页面资源配置</el-menu-item>
</el-sub-menu>
<!-- 会话管理客服专用 -->
<el-menu-item index="/chat/agent" :class="{ 'menu-disabled': !hasAnyChatPerm }">
<el-icon><Comment /></el-icon>
<span>会话管理</span>
<el-badge
v-if="hasAnyChatPerm && agentUnread > 0"
:value="agentUnread"
class="agent-badge"
style="margin-left: 8px;"
/>
</el-menu-item>
</el-menu>
<div class="collapse-btn" @click="toggleCollapse">
<el-icon :size="20">
<component :is="isCollapse ? 'Expand' : 'Fold'" />
</el-icon>
</div>
</el-aside>
<el-container>
<el-header class="header">
<div class="header-left">
<span class="page-title">{{ currentTitle }}</span>
</div>
<div class="stats-mini">
<div v-for="stat in statsList" :key="stat.label" class="stat-mini-card">
<div class="stat-mini-icon">{{ stat.icon }}</div>
<div class="stat-mini-content">
<div class="stat-mini-label">{{ stat.label }}</div>
<div class="stat-mini-value">{{ stat.value }}</div>
</div>
</div>
</div>
<div class="header-right">
<span class="user-badge">{{ userPhone }}</span>
<el-button class="logout-btn" size="small" @click="handleLogout" plain>退出</el-button>
</div>
</el-header>
<el-main class="main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</el-main>
</el-container>
<KefuConnect v-if="hasAnyChatPerm" />
</el-container>
</template>
<script setup>
import { computed, ref, onMounted, onUnmounted, getCurrentInstance, provide } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElNotification } from 'element-plus'
import {
List, User, Warning, Money, Fold, Expand, Connection, Setting,
Goods, UserFilled, Comment,DataAnalysis, Medal // 新增
} from '@element-plus/icons-vue'
import request from '@/utils/request'
import KefuConnect from '@/components/KefuConnect.vue'
import { disconnectKefuChat } from '@/utils/kefuChat'
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const isCollapse = ref(false)
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
const currentTitle = computed(() => route.meta?.title || '客服终端')
const userPhone = ref((localStorage.getItem('username') || '未知账号').trim())
const stats = ref({
jinrichuli: 0,
jinyuechuli: 0,
zongchuli: 0
})
const statsList = computed(() => [
{ label: '今日处理', value: stats.value.jinrichuli, icon: '⚡' },
{ label: '今月处理', value: stats.value.jinyuechuli, icon: '🌙' },
{ label: '总处理', value: stats.value.zongchuli, icon: '🔰' }
])
let abortController = null
const fetchStats = async () => {
if (abortController) abortController.abort()
abortController = new AbortController()
try {
const res = await request.post('/yonghu/kfjrhqzl', {
phone: userPhone.value
}, { signal: abortController.signal })
if (res.code === 0) {
stats.value = res.data
}
} catch (error) {
if (error.name !== 'CanceledError') {
console.error('获取统计失败:', error)
}
} finally {
abortController = null
}
}
const handleLogout = () => {
disconnectKefuChat()
localStorage.clear()
router.push('/login')
ElMessage.success('已退出登录')
}
// ===================== 客服相关 =====================
const kefuPermissions = ref([]) // 后端返回的权限码数组
const goeasyAppkey = ref('') // GoEasy appkey
const hasAnyChatPerm = computed(() => kefuPermissions.value.length > 0) // 是否至少有一个聊天权限
const agentUnread = ref(0) // 未读消息总数
// 权限→身份前缀映射
const permPrefixMap = {
'abca1': 'Ds', // 打手
'baac2': 'Boss', // 老板
'cabc3': 'Gs', // 管事
'cb3a2': 'Sj', // 商家
'bcaa4': 'Zz' // 组长
}
// 获取客服权限及GoEasy配置
const fetchKefuPermission = async () => {
try {
const res = await request.post('/houtai/kfhqltqx', {
phone: userPhone.value
})
if (res.code === 0) {
const { permissions, goeasy_appkey } = res.data
kefuPermissions.value = permissions || []
goeasyAppkey.value = goeasy_appkey || ''
}
} catch (error) {
console.error('获取客服权限失败:', error)
}
}
// 监听子组件传来的未读消息更新(由 KefuConnect + kefuChat 统一推送)
const onAgentUnread = (count) => {
agentUnread.value = count
}
let lastUnreadForNotify = 0
let unreadBaselineReady = false
const showMessageNotification = (title, message) => {
ElNotification({
title,
message,
type: 'info',
duration: 0,
showClose: true,
position: 'top-right',
customClass: 'kefu-msg-notify',
onClick: () => {
ElNotification.closeAll()
router.push('/chat/agent')
}
})
}
const onMessageNotify = (payload) => {
if (route.path === '/chat/agent') return
const customerId = payload?.customerId || '用户'
const preview = payload?.preview || '收到新消息'
showMessageNotification('新会话消息', `${customerId}: ${preview}`)
}
const onUnreadBumpNotify = (count) => {
if (!unreadBaselineReady) {
lastUnreadForNotify = count
unreadBaselineReady = true
return
}
if (route.path === '/chat/agent') {
lastUnreadForNotify = count
return
}
if (count <= 0) {
lastUnreadForNotify = 0
return
}
if (count > lastUnreadForNotify) {
showMessageNotification('会话未读提醒', `您有 ${count} 条未读消息,点击查看`)
}
lastUnreadForNotify = count
}
// 向子组件提供客服相关数据
provide('kefuPermissions', kefuPermissions)
provide('permPrefixMap', permPrefixMap)
provide('goeasyAppkey', goeasyAppkey)
provide('hasAnyChatPerm', hasAnyChatPerm)
onMounted(() => {
fetchStats()
fetchKefuPermission()
proxy.$emitter?.on('refresh-stats', fetchStats)
proxy.$emitter?.on('agent-unread', onAgentUnread)
proxy.$emitter?.on('agent-unread', onUnreadBumpNotify)
proxy.$emitter?.on('kefu-message-notify', onMessageNotify)
})
onUnmounted(() => {
if (abortController) abortController.abort()
proxy.$emitter?.off('refresh-stats', fetchStats)
proxy.$emitter?.off('agent-unread', onAgentUnread)
proxy.$emitter?.off('agent-unread', onUnreadBumpNotify)
proxy.$emitter?.off('kefu-message-notify', onMessageNotify)
})
</script>
<style scoped>
/* ========= 超时空机甲风格 ========= */
* {
box-sizing: border-box;
}
.layout {
height: 100vh;
background: radial-gradient(circle at 20% 30%, #0a0f1a, #03060c);
font-family: 'Inter', 'Segoe UI', 'Poppins', sans-serif;
overflow: hidden;
}
/* ----- 左侧菜单(机甲深色)----- */
.aside {
background: rgba(2, 8, 18, 0.85);
backdrop-filter: blur(12px);
border-right: 1px solid rgba(0, 242, 255, 0.25);
transition: width 0.3s cubic-bezier(0.2, 0.9, 0.4, 1.1);
display: flex;
flex-direction: column;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
position: relative;
z-index: 10;
}
.aside::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 2px;
height: 100%;
background: linear-gradient(to bottom, #00f2ff, #0066ff, #00f2ff);
opacity: 0.6;
}
.logo-area {
height: 70px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgba(0, 242, 255, 0.3);
margin-bottom: 20px;
position: relative;
}
.logo-text {
background: linear-gradient(135deg, #00f2ff, #3b82f6);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-size: 20px;
font-weight: 700;
letter-spacing: 1.5px;
text-shadow: 0 0 8px rgba(0,242,255,0.3);
}
.logo-icon {
color: #00f2ff;
font-size: 32px;
filter: drop-shadow(0 0 6px #00f2ff);
}
/* 菜单样式(机甲科技) */
:deep(.el-menu) {
border-right: none;
background: transparent !important;
flex: 1;
}
:deep(.el-menu-item) {
height: 48px;
line-height: 48px;
margin: 6px 12px;
border-radius: 12px;
transition: all 0.2s ease;
font-weight: 500;
letter-spacing: 0.5px;
border-left: 2px solid transparent;
}
:deep(.el-menu-item.is-active) {
background: rgba(0, 242, 255, 0.12);
border-left: 2px solid #00f2ff;
box-shadow: -4px 0 10px rgba(0,242,255,0.2);
color: #00f2ff !important;
text-shadow: 0 0 4px rgba(0,242,255,0.5);
}
:deep(.el-menu-item:hover) {
background: rgba(0, 242, 255, 0.08) !important;
border-left-color: #00f2ff;
transform: translateX(4px);
}
:deep(.el-sub-menu__title) {
height: 48px;
line-height: 48px;
margin: 6px 12px;
border-radius: 12px;
font-weight: 500;
}
:deep(.el-sub-menu__title:hover) {
background: rgba(0, 242, 255, 0.08) !important;
color: #00f2ff !important;
}
/* 折叠按钮 */
.collapse-btn {
height: 56px;
display: flex;
align-items: center;
justify-content: center;
color: #00f2ff;
border-top: 1px solid rgba(0, 242, 255, 0.3);
cursor: pointer;
transition: all 0.2s;
font-size: 20px;
}
.collapse-btn:hover {
background: rgba(0, 242, 255, 0.1);
text-shadow: 0 0 6px #00f2ff;
}
/* ----- 头部区域(含统计卡片)----- */
.header {
background: rgba(6, 12, 20, 0.7);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0, 242, 255, 0.3);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
height: 80px;
gap: 20px;
flex-wrap: wrap;
}
.header-left {
flex-shrink: 0;
}
.page-title {
font-size: 1.4rem;
font-weight: 700;
background: linear-gradient(135deg, #fff, #00f2ff);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: 1px;
text-shadow: 0 0 6px rgba(0,242,255,0.3);
}
/* 紧凑型统计卡片 */
.stats-mini {
display: flex;
gap: 16px;
flex-wrap: wrap;
flex: 1;
justify-content: center;
}
.stat-mini-card {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
border: 1px solid rgba(0, 242, 255, 0.4);
border-radius: 28px;
padding: 8px 20px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
box-shadow: 0 0 8px rgba(0,242,255,0.2);
}
.stat-mini-card:hover {
transform: translateY(-2px);
border-color: #00f2ff;
box-shadow: 0 0 16px rgba(0,242,255,0.4);
}
.stat-mini-icon {
font-size: 26px;
filter: drop-shadow(0 0 4px #00f2ff);
}
.stat-mini-content {
text-align: left;
}
.stat-mini-label {
font-size: 12px;
color: rgba(0, 242, 255, 0.7);
letter-spacing: 0.5px;
}
.stat-mini-value {
font-size: 20px;
font-weight: 700;
color: #fff;
text-shadow: 0 0 4px #00f2ff;
line-height: 1.2;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.user-badge {
background: rgba(0, 242, 255, 0.15);
border: 1px solid rgba(0, 242, 255, 0.5);
border-radius: 40px;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
color: #00f2ff;
backdrop-filter: blur(4px);
}
.logout-btn {
background: transparent;
border-color: rgba(0, 242, 255, 0.5);
color: #00f2ff;
transition: 0.2s;
}
.logout-btn:hover {
background: rgba(0, 242, 255, 0.2);
border-color: #00f2ff;
box-shadow: 0 0 8px #00f2ff;
}
/* 主内容区域 */
.main {
padding: 24px;
background: transparent;
overflow-y: auto;
color: #eef2ff;
}
/* 页面切换动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* 滚动条定制 */
.main::-webkit-scrollbar {
width: 6px;
}
.main::-webkit-scrollbar-track {
background: rgba(0,0,0,0.3);
border-radius: 4px;
}
.main::-webkit-scrollbar-thumb {
background: #00f2ff;
border-radius: 4px;
box-shadow: inset 0 0 3px #000;
}
/* 响应式 */
@media screen and (max-width: 1024px) {
.header {
height: auto;
padding: 12px 20px;
flex-wrap: wrap;
}
.stats-mini {
order: 3;
width: 100%;
justify-content: flex-start;
margin-top: 8px;
}
.stat-mini-card {
padding: 6px 16px;
}
.stat-mini-value {
font-size: 18px;
}
}
@media screen and (max-width: 768px) {
.layout {
min-width: 768px;
overflow-x: auto;
}
.stat-mini-card {
padding: 4px 12px;
}
.stat-mini-label {
font-size: 10px;
}
.stat-mini-value {
font-size: 16px;
}
.page-title {
font-size: 1.2rem;
}
}
/* 会话未读角标微调 */
.agent-badge {
margin-left: 4px;
}
</style>