Files
Vue3/src/views/Layout.vue
2026-07-27 01:29:29 +08:00

949 lines
28 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 v-if="canShow('finance')" index="/finance">
<template #title>
<el-icon><DataAnalysis /></el-icon>
<span>板块与财务</span>
</template>
<el-menu-item v-if="canShow('finance.bankuai') && isAllClubScope" index="/finance/bankuai">板块配置</el-menu-item>
<el-menu-item v-if="canShow('finance.data')" index="/finance/data">财务数据</el-menu-item>
<el-menu-item v-if="canShow('finance.fund-freeze-config')" index="/finance/fund-freeze-config">资金冻结规则</el-menu-item>
<el-menu-item v-if="canShow('finance.fund-freeze-ledger')" index="/finance/fund-freeze-ledger">资金冻结明细</el-menu-item>
<el-menu-item v-if="canShow('finance.fund-risk-stats')" index="/finance/fund-risk-stats">罚款投诉指标</el-menu-item>
</el-sub-menu>
<!-- 考核管理 -->
<el-sub-menu v-if="canShow('assessment')" index="/assessment">
<template #title>
<el-icon><Medal /></el-icon>
<span>考核管理</span>
</template>
<el-menu-item v-if="canShow('assessment.examiner')" index="/assessment/examiner">考核官管理</el-menu-item>
<el-menu-item v-if="canShow('assessment.config') && isAllClubScope" index="/assessment/config">考核配置</el-menu-item>
<el-menu-item v-if="canShow('assessment.record')" index="/assessment/record">考核记录</el-menu-item>
</el-sub-menu>
<!-- 订单管理 -->
<el-sub-menu v-if="canShow('order')" index="/order">
<template #title>
<el-icon><List /></el-icon>
<span>订单管理</span>
</template>
<el-menu-item v-if="canShow('order.platform')" index="/order/platform">平台订单</el-menu-item>
<el-menu-item v-if="canShow('order.merchant')" index="/order/merchant">商家订单</el-menu-item>
<el-menu-item v-if="canShow('order.deal-config')" index="/order/deal-config">成交与自动结算</el-menu-item>
</el-sub-menu>
<!-- 跨平台管理 -->
<el-sub-menu v-if="canShow('cross-platform')" index="/cross-platform">
<template #title>
<el-icon><Connection /></el-icon>
<span>跨平台管理</span>
</template>
<el-menu-item v-if="canShow('cross-platform.order')" index="/cross-order">跨平台订单</el-menu-item>
<el-menu-item v-if="canShow('cross-platform.settings')" index="/cross-settings">跨平台设置</el-menu-item>
<el-menu-item v-if="canShow('cross-platform.data')" index="/cross-data">跨平台数据</el-menu-item>
<el-menu-item v-if="canShow('cross-platform.pre')" index="/cross-pre">预结算管理</el-menu-item>
</el-sub-menu>
<!-- 用户管理 -->
<el-sub-menu v-if="canShow('user')" index="/user">
<template #title>
<el-icon><User /></el-icon>
<span>用户管理</span>
</template>
<el-menu-item v-if="canShow('user.player')" index="/user/player">打手管理</el-menu-item>
<el-menu-item v-if="canShow('user.guanli')" index="/user/guanli">管事管理</el-menu-item>
<el-menu-item v-if="canShow('user.shangjia')" index="/user/shangjia">商家管理</el-menu-item>
<el-menu-item v-if="canShow('user.zuzhang')" index="/user/zuzhang">组长管理</el-menu-item>
<el-menu-item v-if="canShow('user.identity-tag')" index="/user/identity-tag">身份装饰标签</el-menu-item>
</el-sub-menu>
<!-- 提现管理 -->
<el-sub-menu v-if="canShow('withdraw')" index="/withdraw">
<template #title>
<el-icon><Money /></el-icon>
<span>提现管理</span>
</template>
<el-menu-item v-if="canShow('withdraw.audit')" index="/withdraw/audit">提现审核</el-menu-item>
<el-menu-item v-if="canShow('withdraw.data')" index="/withdraw/data">提现数据</el-menu-item>
<el-menu-item v-if="canShow('withdraw.settings')" index="/withdraw/settings">提现设置</el-menu-item>
</el-sub-menu>
<!-- 管理员管理 -->
<el-sub-menu v-if="canShow('admin')" index="/admin">
<template #title>
<el-icon><Setting /></el-icon>
<span>管理员管理</span>
</template>
<el-menu-item v-if="canShow('admin.role')" index="/admin/role">角色管理</el-menu-item>
<el-menu-item v-if="canShow('admin.data-scope') && isGroupAdmin" index="/admin/data-scope">数据范围配置</el-menu-item>
<el-menu-item v-if="canShow('admin.club-config') && isGroupAdmin" index="/admin/club-config">俱乐部密钥配置</el-menu-item>
<el-menu-item v-if="canShow('admin.order-grab-share') && isGroupAdmin" index="/admin/order-grab-share">订单互通抢单</el-menu-item>
<el-menu-item v-if="canShow('admin.user')" index="/admin/user">后台用户</el-menu-item>
<el-menu-item v-if="canShow('admin.operation-log')" index="/admin/operation-log">操作日志</el-menu-item>
</el-sub-menu>
<!-- 处罚管理 -->
<el-menu-item v-if="canShow('punishment')" index="/punishment">
<el-icon><Warning /></el-icon>
<span>处罚管理</span>
</el-menu-item>
<el-menu-item v-if="canShow('gongdan')" index="/gongdan">
<el-icon><ChatDotRound /></el-icon>
<span>投诉工单</span>
</el-menu-item>
<!-- 商品管理 -->
<el-sub-menu v-if="canShow('product')" index="/product">
<template #title>
<el-icon><Goods /></el-icon>
<span>商品管理</span>
</template>
<el-menu-item v-if="canShow('product.list')" index="/product/list">商品列表</el-menu-item>
<el-menu-item v-if="canShow('product.type-zone')" index="/product/type-zone">商品类型专区管理</el-menu-item>
<el-menu-item v-if="canShow('product.turntable')" index="/product/turntable">转盘活动</el-menu-item>
<el-menu-item v-if="canShow('product.data') && isAllClubScope" index="/product/data">商品数据分析</el-menu-item>
<el-menu-item v-if="canShow('product.fake-orders')" index="/product/fake-orders">假单管理</el-menu-item>
</el-sub-menu>
<!-- 会员管理 -->
<el-sub-menu v-if="canShow('member')" index="/member">
<template #title>
<el-icon><UserFilled /></el-icon>
<span>会员管理</span>
</template>
<el-menu-item v-if="canShow('member.list')" index="/member/list">会员管理</el-menu-item>
<el-menu-item v-if="canShow('member.data')" index="/member/data">会员数据分析</el-menu-item>
</el-sub-menu>
<!-- 小程序配置 -->
<el-sub-menu v-if="canShow('miniapp')" index="/miniapp">
<template #title>
<el-icon><Setting /></el-icon>
<span>小程序配置</span>
</template>
<el-menu-item v-if="canShow('miniapp.carousel')" index="/miniapp/carousel">轮播图与公告</el-menu-item>
<el-menu-item v-if="canShow('miniapp.carousel') || canShow('miniapp.assets')" index="/miniapp/assets">图标与频道</el-menu-item>
<el-menu-item v-if="canShow('miniapp.popup')" index="/miniapp/popup-notice">弹窗公告管理</el-menu-item>
<el-menu-item v-if="canShow('miniapp.leixing')" index="/miniapp/leixing">抢单商品类型</el-menu-item>
<el-menu-item v-if="canShow('miniapp.kaohe-tags')" index="/miniapp/kaohe-tags">抢单考核标签</el-menu-item>
<el-menu-item v-if="canShow('miniapp.dashou-exam')" index="/miniapp/dashou-exam">打手接单考试</el-menu-item>
<el-menu-item v-if="canShow('miniapp.rank-reward')" index="/miniapp/rank-reward">排行榜奖励</el-menu-item>
<el-menu-item v-if="canShow('miniapp.recharge-copy')" index="/miniapp/recharge-copy">充值页文案</el-menu-item>
<el-menu-item v-if="canShow('miniapp.role-agreement')" index="/miniapp/role-agreement">角色协议</el-menu-item>
<el-menu-item v-if="canShow('miniapp.withdraw-settings')" index="/withdraw/settings">提现与收款设置</el-menu-item>
</el-sub-menu>
<!-- 店铺管理 -->
<el-sub-menu v-if="canShow('shop')" index="/shop">
<template #title>
<el-icon><Shop /></el-icon>
<span>店铺管理</span>
</template>
<el-menu-item v-if="canShow('shop.list')" index="/shop/list">店铺列表</el-menu-item>
<el-menu-item v-if="canShow('shop.withdraw')" index="/shop/withdraw-apply">店铺提现申请</el-menu-item>
<el-menu-item v-if="canShow('shop.products')" index="/shop/products">店铺商品管理</el-menu-item>
</el-sub-menu>
<!-- 会话管理 -->
<el-sub-menu v-if="canShow('chat')" index="/chat">
<template #title>
<el-icon><Comment /></el-icon>
<span>会话管理</span>
<el-badge
v-if="hasAnyChatPerm && agentUnread > 0"
:value="agentUnread"
class="agent-badge"
style="margin-left: 8px;"
/>
</template>
<el-menu-item v-if="canShow('chat.agent')" index="/chat/agent" :class="{ 'menu-disabled': !hasAnyChatPerm }">
会话接入
</el-menu-item>
<el-menu-item v-if="canShow('chat.script-config')" index="/chat/script-config">
客服聊天配置
</el-menu-item>
</el-sub-menu>
</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-row header-row-main">
<div class="header-left">
<span class="page-title">{{ currentTitle }}</span>
<el-select
v-if="showScopeSelector"
v-model="scopeSelectValue"
size="small"
class="club-select"
@change="onScopeSelectChange"
>
<el-option
v-if="canGroupScope"
label="集团汇总(全部子公司)"
value="__all__"
/>
<el-option
v-for="item in clubOptions"
:key="item.club_id"
:label="item.name"
:value="item.club_id"
/>
</el-select>
<span v-else-if="scopeBadgeText" class="club-badge">{{ scopeBadgeText }}</span>
<span v-if="adminRoleName" class="role-badge">{{ adminRoleName }}</span>
</div>
<div class="header-right">
<span class="user-badge">{{ userPhone }}</span>
<el-button class="logout-btn" size="small" @click="handleLogout" plain>退出</el-button>
</div>
</div>
<div class="header-row header-row-stats">
<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>
</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, Shop, Comment, DataAnalysis, Medal, ChatDotRound
} from '@element-plus/icons-vue'
import request from '@/utils/request'
import KefuConnect from '@/components/KefuConnect.vue'
import { disconnectKefuChat } from '@/utils/kefuChat'
import {
getAdminClubContext,
getAdminClubId,
getAdminClubScope,
setAdminClubContext,
mergeServerClubContext,
JITUAN_API,
setMenuAccess,
getMenuAccess,
canSwitchClubByMenu,
ensureMenuAccess,
canShowMenuPage,
clearMenuAccess,
} from '@/utils/club-context'
const menuAccessState = ref(getMenuAccess())
const canShow = (pageId) => {
void menuAccessState.value
return canShowMenuPage(pageId)
}
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') || '未知账号')
const clubOptions = ref([])
const scopeSelectValue = ref('')
const adminCtx = computed(() => getAdminClubContext())
const isGroupAdmin = computed(() => !!adminCtx.value?.is_group_admin)
const canGroupScope = computed(() => isGroupAdmin.value)
const isAllClubScope = computed(() => getAdminClubScope() === 'all')
const isSingleClubScope = computed(() => getAdminClubScope() !== 'all')
const showScopeSelector = computed(() => canSwitchClubByMenu())
const scopeBadgeText = computed(() => {
const scope = getAdminClubScope()
if (scope === 'all') return '集团汇总(全部子公司)'
const cid = getAdminClubId()
const hit = clubOptions.value.find((c) => c.club_id === cid)
return hit ? `${hit.name}${cid}` : cid
})
const adminRoleName = computed(() => adminCtx.value?.role_name || '')
const syncScopeSelectValue = () => {
const scope = getAdminClubScope()
scopeSelectValue.value = scope === 'all' ? '__all__' : getAdminClubId()
}
const loadMenuAccess = async () => {
try {
await ensureMenuAccess(async () => {
const res = await request.get(JITUAN_API.menuAccess)
return res.code === 0 ? res.data : null
}, { force: true })
const data = getMenuAccess()
if (data) {
menuAccessState.value = data
const ctx = getAdminClubContext()
if (ctx) {
setAdminClubContext({ ...ctx, can_switch_club: data.can_switch_club })
}
}
} catch (error) {
console.error('加载菜单权限失败:', error)
}
}
const loadClubContext = async () => {
try {
const res = await request.get(JITUAN_API.meContext, { skipErrorToast: true })
if (res.code === 0 && res.data) {
mergeServerClubContext(res.data)
clubOptions.value = res.data.clubs || []
syncScopeSelectValue()
return
}
} catch (error) {
console.error('加载俱乐部上下文失败:', error)
}
const ctx = getAdminClubContext()
if (ctx && ctx.clubs) {
clubOptions.value = ctx.clubs
syncScopeSelectValue()
}
}
const onScopeSelectChange = (val) => {
const ctx = getAdminClubContext() || {}
if (val === '__all__') {
setAdminClubContext({
...ctx,
scope: 'ALL_CLUBS',
club_id: ctx.club_id || 'xq',
})
ElMessage.success('已切换至:集团汇总(全部子公司)')
} else {
const assign = (ctx.assignments || []).find((a) => a.club_id === val)
setAdminClubContext({
...ctx,
scope: 'SINGLE_CLUB',
club_id: val,
role_code: assign?.role_code || ctx.role_code,
role_name: assign?.role_name || ctx.role_name,
})
const hit = clubOptions.value.find((c) => c.club_id === val)
ElMessage.success(`已切换至:${hit?.name || val}`)
}
clearMenuAccess()
window.location.reload()
}
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, skipErrorToast: true })
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()
sessionStorage.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': 'Ds', // 管事(与小程序一致,统一 Ds
'cb3a2': 'Sj', // 商家
'bcaa4': 'Ds' // 组长(与小程序一致,统一 Ds
}
// 获取客服权限及GoEasy配置
const fetchKefuPermission = async () => {
try {
const res = await request.post('/houtai/kfhqltqx', {
phone: userPhone.value
}, { skipErrorToast: true })
if (res.code === 0) {
const { permissions, goeasy_appkey } = res.data
kefuPermissions.value = permissions || []
goeasyAppkey.value = goeasy_appkey || ''
}
} catch (error) {
console.error('获取客服权限失败:', error)
}
}
const onAgentUnread = (count) => {
agentUnread.value = count
}
let lastUnreadForNotify = 0
let unreadBaselineReady = false
const playNotifySound = () => {
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext
if (!AudioCtx) return
const ctx = new AudioCtx()
const playBeep = (freq, start, duration) => {
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.connect(gain)
gain.connect(ctx.destination)
osc.frequency.value = freq
gain.gain.value = 0.25
osc.start(start)
osc.stop(start + duration)
}
playBeep(880, ctx.currentTime, 0.12)
playBeep(1100, ctx.currentTime + 0.15, 0.12)
setTimeout(() => ctx.close(), 500)
} catch (e) {
// ignore
}
}
const showMessageNotification = (title, message) => {
playNotifySound()
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(async () => {
await loadClubContext()
await loadMenuAccess()
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;
flex-direction: column;
padding: 10px 24px 12px;
gap: 10px;
height: auto;
min-height: 80px;
}
.header-row {
display: flex;
align-items: center;
width: 100%;
}
.header-row-main {
justify-content: space-between;
gap: 16px;
}
.header-row-stats {
justify-content: center;
}
.header-left {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
}
.club-select {
width: 200px;
flex-shrink: 0;
}
.club-badge {
white-space: nowrap;
font-size: 12px;
padding: 4px 10px;
border-radius: 4px;
color: rgba(0, 242, 255, 0.9);
border: 1px solid rgba(0, 242, 255, 0.35);
background: rgba(0, 242, 255, 0.08);
max-width: 240px;
overflow: hidden;
text-overflow: ellipsis;
}
.role-badge {
white-space: nowrap;
flex-shrink: 0;
font-size: 12px;
padding: 4px 10px;
border-radius: 4px;
color: rgba(0, 242, 255, 0.85);
border: 1px solid rgba(0, 242, 255, 0.35);
background: rgba(0, 242, 255, 0.08);
}
.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: nowrap;
justify-content: center;
overflow-x: auto;
max-width: 100%;
}
.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 {
padding: 10px 16px;
}
.header-row-main {
flex-wrap: wrap;
}
.stats-mini {
justify-content: flex-start;
}
.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>
<style>
.kefu-msg-notify {
min-width: 320px;
border-left: 4px solid #409eff !important;
box-shadow: 0 4px 20px rgba(64, 158, 255, 0.35) !important;
}
.kefu-msg-notify .el-notification__title {
font-size: 16px;
font-weight: 700;
color: #303133;
}
.kefu-msg-notify .el-notification__content {
font-size: 14px;
color: #606266;
line-height: 1.5;
}
</style>