Files
template-10586/app/layouts/mp.vue
赵忠林 5e26fdc7fb feat(app): 初始化项目配置和页面结构
- 添加 .dockerignore 和 .env.example 配置文件
- 添加 .gitignore 忽略规则配置
- 创建服务端代理API路由(_file、_modules、_server)
- 集成 Ant Design Vue 组件库并配置SSR样式提取
- 定义API响应类型封装
- 创建基础布局组件(blank、console)
- 实现应用中心页面和组件(AppsCenter)
- 添加文章列表测试页面
- 配置控制台导航菜单结构
- 实现控制台头部组件
- 创建联系页面表单
2026-01-17 18:23:37 +08:00

247 lines
5.5 KiB
Vue

<template>
<a-layout class="min-h-screen layout-shell">
<a-layout class="w-full px-4 py-4">
<ConsoleHeader
product-label="小程序开发"
default-jump-key="mp"
:user="user"
:user-display-name="userDisplayName"
:user-menu-items="userMenuItems"
@user-menu-click="onUserMenuClick"
/>
<a-layout class="body">
<a-layout-sider
class="sider"
:width="240"
breakpoint="lg"
theme="light"
:trigger="null"
collapsible
v-model:collapsed="collapsed"
>
<div class="sider-brand" :class="{ collapsed }">
<a-avatar shape="square" :size="28" src="https://oss.wsdns.cn/20250304/e65ea719564e47a1b8da93d6eea8287a.png">
<template #icon>
<AppstoreOutlined />
</template>
</a-avatar>
<div v-if="!collapsed" class="sider-title">
小程序开发
</div>
</div>
<a-menu
mode="inline"
theme="light"
:selected-keys="selectedKeys"
:inline-collapsed="collapsed"
@click="onMenuClick"
>
<a-menu-item v-for="item in menu" :key="item.to">
{{ item.label }}
</a-menu-item>
</a-menu>
<div
class="sider-collapse-trigger"
role="button"
tabindex="0"
:aria-label="collapsed ? '展开菜单' : '收起菜单'"
@click="toggleCollapsed"
@keydown.enter.prevent="toggleCollapsed"
@keydown.space.prevent="toggleCollapsed"
>
<RightOutlined :style="{fontSize: '10px', paddingRight: '4px'}" v-if="collapsed" />
<LeftOutlined :style="{fontSize: '10px', paddingRight: '4px'}" v-else />
</div>
</a-layout-sider>
<a-layout class="main">
<a-layout-content class="content">
<a-spin v-if="!ready" size="large" tip="加载中..." class="spin" />
<template v-else>
<slot />
</template>
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { AppstoreOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue'
import { mpNav } from '@/config/mp-nav'
import { getUserInfo } from '@/api/layout'
import { getToken, removeToken } from '@/utils/token-util'
import { clearAuthz, setAuthzFromUser } from '@/utils/permission'
import type { User } from '@/api/system/user/model'
const route = useRoute()
const collapsed = ref(false)
const menu = computed(() => mpNav)
const user = ref<User | null>(null)
const userDisplayName = computed(() => {
const u = user.value
const nickname = u?.nickname?.trim()
const username = u?.username?.trim()
const phone = u?.phone?.trim()
const mobile = u?.mobile?.trim()
if (nickname) return nickname
if (username) return username
if (phone) return phone
if (mobile) return mobile
return '当前用户'
})
const userMenuItems = [
{ key: 'home', label: '返回首页' },
{ key: 'logout', label: '退出登录' },
]
const selectedKeys = computed(() => {
const match = menu.value.find((item) => route.path === item.to)
if (match) return [match.to]
if (route.path.startsWith('/mp')) return ['/mp']
return []
})
function onMenuClick(info: { key: string }) {
navigateTo(String(info.key))
}
function toggleCollapsed() {
collapsed.value = !collapsed.value
}
function logout() {
removeToken()
try {
localStorage.removeItem('TenantId')
localStorage.removeItem('UserId')
} catch {
// ignore
}
clearAuthz()
navigateTo('/')
}
function onUserMenuClick(key: string) {
if (key === 'home') navigateTo('/')
if (key === 'logout') logout()
}
const ready = ref(false)
onMounted(async () => {
const token = getToken()
if (!token) {
clearAuthz()
message.error('请先登录')
await navigateTo('/login')
ready.value = true
return
}
try {
const me = await getUserInfo()
user.value = me
setAuthzFromUser(me)
} catch {
user.value = null
clearAuthz()
}
ready.value = true
})
</script>
<style scoped>
.layout-shell {
background: #f5f5f5;
}
.sider {
border-radius: 12px;
overflow: visible;
position: relative;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.sider-brand {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 16px 10px;
}
.sider-brand.collapsed {
justify-content: center;
}
.sider-title {
color: rgba(0, 0, 0, 0.88);
font-size: 16px;
}
:deep(.ant-menu-inline) {
border-inline-end: 0;
}
.sider-collapse-trigger {
position: absolute;
top: 50%;
right: -16px;
width: 16px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-left: 0;
border-radius: 0 9999px 9999px 0;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
z-index: 2;
}
.sider-collapse-trigger:hover {
background: rgba(0, 0, 0, 0.02);
}
.sider-collapse-trigger:active {
background: rgba(0, 0, 0, 0.04);
}
.body {
margin: 16px 0;
}
.main {
margin-left: 16px;
background: transparent;
}
.content {
min-height: 520px;
background: #fff;
border-radius: 12px;
padding: 18px;
}
.spin {
display: flex;
align-items: center;
justify-content: center;
min-height: 380px;
}
</style>