- 新增 .env.production.example 环境变量配置模板 - 添加 compose.production.yaml Docker Compose 部署配置 - 创建 web.Dockerfile 前端构建部署文件 - 编写详细的 README.md 部署文档,涵盖架构、配置、步骤等内容 - 添加离线推送架构设计文档 - 更新 IM 多平台进度跟踪文档
278 行
6.1 KiB
Vue
278 行
6.1 KiB
Vue
<template>
|
|
<el-container class="layout-container">
|
|
<el-aside v-if="!isMobile" width="220px" class="sidebar">
|
|
<div class="logo">
|
|
<span>XuqmGroup</span>
|
|
</div>
|
|
<el-menu
|
|
:default-active="$route.path"
|
|
router
|
|
background-color="#1d2129"
|
|
text-color="#c9d1d9"
|
|
active-text-color="#409eff"
|
|
class="nav-menu"
|
|
>
|
|
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
<span>{{ item.label }}</span>
|
|
</el-menu-item>
|
|
</el-menu>
|
|
</el-aside>
|
|
|
|
<el-drawer
|
|
v-model="drawerVisible"
|
|
direction="ltr"
|
|
:with-header="false"
|
|
size="250px"
|
|
class="mobile-drawer"
|
|
>
|
|
<div class="drawer-brand">
|
|
<span>XuqmGroup</span>
|
|
</div>
|
|
<el-menu
|
|
:default-active="$route.path"
|
|
router
|
|
background-color="#1d2129"
|
|
text-color="#c9d1d9"
|
|
active-text-color="#409eff"
|
|
class="nav-menu"
|
|
@select="handleNavSelect"
|
|
>
|
|
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
<span>{{ item.label }}</span>
|
|
</el-menu-item>
|
|
</el-menu>
|
|
</el-drawer>
|
|
|
|
<el-container class="content-shell">
|
|
<el-header class="header">
|
|
<div v-if="isMobile" class="mobile-header-left">
|
|
<el-button text circle class="menu-button" @click="drawerVisible = true">
|
|
<el-icon><Menu /></el-icon>
|
|
</el-button>
|
|
<span class="mobile-title">XuqmGroup</span>
|
|
</div>
|
|
<div v-else class="header-spacer" />
|
|
<div class="header-right">
|
|
<el-tooltip content="开发者文档" placement="bottom">
|
|
<a href="/docs/" target="_blank" class="docs-link">
|
|
<el-icon :size="18"><Document /></el-icon>
|
|
<span>文档</span>
|
|
</a>
|
|
</el-tooltip>
|
|
<el-dropdown @command="handleCommand">
|
|
<span class="user-info">
|
|
<el-avatar :size="32" style="background:#409eff">
|
|
{{ auth.user?.nickname?.charAt(0) ?? 'U' }}
|
|
</el-avatar>
|
|
<span class="nickname">{{ auth.user?.nickname }}</span>
|
|
</span>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</el-header>
|
|
|
|
<el-main class="main-content">
|
|
<router-view />
|
|
</el-main>
|
|
</el-container>
|
|
</el-container>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { Document, Grid, Menu, Odometer, User } from '@element-plus/icons-vue'
|
|
|
|
const auth = useAuthStore()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const isMobile = ref(false)
|
|
const drawerVisible = ref(false)
|
|
|
|
const navItems = computed(() => {
|
|
const items = [
|
|
{ path: '/dashboard', label: '控制台', icon: Odometer },
|
|
{ path: '/apps', label: '我的应用', icon: Grid },
|
|
{ path: '/operation-logs', label: '操作日志', icon: Document },
|
|
]
|
|
if (auth.user?.type === 'MAIN') {
|
|
items.push({ path: '/accounts', label: '子账号管理', icon: User })
|
|
}
|
|
return items
|
|
})
|
|
|
|
function updateViewport() {
|
|
isMobile.value = window.innerWidth < 768
|
|
if (!isMobile.value) {
|
|
drawerVisible.value = false
|
|
}
|
|
}
|
|
|
|
function handleNavSelect() {
|
|
drawerVisible.value = false
|
|
}
|
|
|
|
function handleCommand(cmd: string) {
|
|
if (cmd === 'logout') {
|
|
auth.logout()
|
|
router.push('/login')
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => route.path,
|
|
() => {
|
|
drawerVisible.value = false
|
|
},
|
|
)
|
|
|
|
onMounted(() => {
|
|
updateViewport()
|
|
window.addEventListener('resize', updateViewport)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', updateViewport)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.layout-container {
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
background: linear-gradient(180deg, #f7f9fc 0%, #eef2f7 100%);
|
|
}
|
|
.sidebar {
|
|
background: #1d2129;
|
|
flex: 0 0 220px;
|
|
box-shadow: 8px 0 24px rgba(15, 23, 42, 0.08);
|
|
}
|
|
.logo {
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
border-bottom: 1px solid #2d3142;
|
|
}
|
|
.nav-menu {
|
|
border-right: none;
|
|
}
|
|
.content-shell {
|
|
min-width: 0;
|
|
}
|
|
.header {
|
|
background: #fff;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 16px;
|
|
gap: 16px;
|
|
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 9;
|
|
}
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
.header-spacer {
|
|
flex: 1;
|
|
}
|
|
.mobile-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
.mobile-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
}
|
|
.menu-button {
|
|
color: #111827;
|
|
}
|
|
.docs-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: #555;
|
|
font-size: 13px;
|
|
text-decoration: none;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
transition: background 0.15s;
|
|
}
|
|
.docs-link:hover {
|
|
background: #f5f5f5;
|
|
color: #409eff;
|
|
}
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
}
|
|
.nickname {
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
.main-content {
|
|
padding: 16px;
|
|
overflow: auto;
|
|
}
|
|
.drawer-brand {
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
border-bottom: 1px solid #2d3142;
|
|
background: #1d2129;
|
|
}
|
|
.mobile-drawer :deep(.el-drawer__body) {
|
|
padding: 0;
|
|
background: #1d2129;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.layout-container {
|
|
display: block;
|
|
height: auto;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
padding: 0 12px;
|
|
}
|
|
|
|
.header-right {
|
|
gap: 10px;
|
|
}
|
|
|
|
.docs-link span,
|
|
.nickname {
|
|
display: none;
|
|
}
|
|
|
|
.main-content {
|
|
padding: 12px;
|
|
}
|
|
}
|
|
</style>
|