第一次提交:Vue3后台前端最新版本

This commit is contained in:
XingQue
2026-06-14 02:43:42 +08:00
commit cf07faccc4
68 changed files with 29381 additions and 0 deletions

675
src/views/Layout.vue Normal file
View File

@@ -0,0 +1,675 @@
<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-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>
</el-container>
</template>
<script setup>
import { computed, ref, onMounted, onUnmounted, getCurrentInstance, provide } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } 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'
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 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 = () => {
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 || ''
// 如果有权限且有appkey初始化GoEasy仅一次
if (hasAnyChatPerm.value && goeasyAppkey.value) {
initGoEasy()
}
}
} catch (error) {
console.error('获取客服权限失败:', error)
}
}
// 初始化GoEasy实例并连接
const initGoEasy = () => {
// 防止重复初始化
if (window.goeasy) return
// 1. 初始化实例
window.goeasy = GoEasy.getInstance({
host: 'hangzhou.goeasy.io', // 根据实际区域调整
appkey: goeasyAppkey.value,
modules: ['im']
})
// 2. 构建客服自己的userId = KF + 账号
const agentId = 'KF' + userPhone.value
const agentData = {
name: userPhone.value, // 客服名称
avatar: '' // 可后续加上头像
}
// 3. 建立连接
window.goeasy.connect({
id: agentId,
data: agentData,
onSuccess: () => {
console.log('客服GoEasy连接成功')
// 连接成功后通知Agent页面通过provide
proxy.$emitter.emit('goeasy-connected')
},
onFailed: (error) => {
console.error('GoEasy连接失败', error)
ElMessage.error('消息服务连接失败,请刷新重试')
},
onProgress: (attempts) => {
console.log('GoEasy正在重连第' + attempts + '次')
}
})
// 4. 保存客服标识到全局
window.__kefuAgentId = agentId
}
// 监听子组件传来的未读消息更新
const onAgentUnread = (count) => {
agentUnread.value = 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)
})
onUnmounted(() => {
if (abortController) abortController.abort()
proxy.$emitter?.off('refresh-stats', fetchStats)
proxy.$emitter?.off('agent-unread', onAgentUnread)
})
</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>