更新政府端和银行端

This commit is contained in:
2025-09-17 18:04:28 +08:00
parent f35ceef31f
commit e4287b83fe
185 changed files with 78320 additions and 189 deletions

View File

@@ -0,0 +1,138 @@
<template>
<a-menu
:default-selected-keys="[currentRoute]"
:default-open-keys="openKeys"
mode="inline"
theme="dark"
:inline-collapsed="collapsed"
@click="handleMenuClick"
@openChange="handleOpenChange"
>
<template v-for="item in menuItems" :key="item.key">
<a-menu-item v-if="!item.children" :key="item.key">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a-menu-item>
<a-sub-menu v-else :key="item.key">
<template #title>
<component :is="item.icon" />
<span>{{ item.title }}</span>
</template>
<a-menu-item
v-for="child in item.children"
:key="child.key"
>
{{ child.title }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { getMenuItems } from '@/router/routes'
import routes from '@/router/routes'
const props = defineProps({
collapsed: {
type: Boolean,
default: false
}
})
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 当前路由
const currentRoute = computed(() => route.name)
// 展开的菜单项
const openKeys = ref([])
// 菜单项
const menuItems = computed(() => {
const userRole = userStore.getUserRoleName()
return getMenuItems(routes, userRole)
})
// 处理菜单点击
const handleMenuClick = ({ key }) => {
const menuItem = findMenuItem(menuItems.value, key)
if (menuItem && menuItem.path) {
router.push(menuItem.path)
}
}
// 处理子菜单展开/收起
const handleOpenChange = (keys) => {
openKeys.value = keys
}
// 查找菜单项
const findMenuItem = (items, key) => {
for (const item of items) {
if (item.key === key) {
return item
}
if (item.children) {
const found = findMenuItem(item.children, key)
if (found) return found
}
}
return null
}
// 监听路由变化,自动展开对应的子菜单
watch(
() => route.path,
(newPath) => {
const pathSegments = newPath.split('/').filter(Boolean)
if (pathSegments.length > 1) {
const parentKey = pathSegments[0]
if (!openKeys.value.includes(parentKey)) {
openKeys.value = [parentKey]
}
}
},
{ immediate: true }
)
</script>
<style scoped>
.ant-menu {
border-right: none;
}
.ant-menu-item,
.ant-menu-submenu-title {
display: flex;
align-items: center;
}
.ant-menu-item .anticon,
.ant-menu-submenu-title .anticon {
margin-right: 8px;
font-size: 16px;
}
.ant-menu-item-selected {
background-color: #1890ff !important;
}
.ant-menu-item-selected::after {
display: none;
}
.ant-menu-submenu-selected > .ant-menu-submenu-title {
color: #1890ff;
}
.ant-menu-submenu-open > .ant-menu-submenu-title {
color: #1890ff;
}
</style>

View File

@@ -0,0 +1,279 @@
<template>
<a-layout-header class="mobile-header">
<div class="mobile-header-content">
<a-button
type="text"
@click="toggleDrawer"
class="menu-button"
>
<menu-outlined />
</a-button>
<div class="header-title">
银行管理系统
</div>
<a-dropdown>
<a-button type="text" class="user-button">
<user-outlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="goToProfile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 移动端抽屉菜单 -->
<a-drawer
v-model:open="drawerVisible"
placement="left"
:width="280"
:body-style="{ padding: 0 }"
>
<div class="drawer-content">
<div class="drawer-header">
<div class="user-info">
<a-avatar :size="48" :src="userData?.avatar">
<user-outlined />
</a-avatar>
<div class="user-details">
<div class="user-name">{{ userData?.real_name || userData?.username }}</div>
<div class="user-role">{{ userData?.role?.display_name || '用户' }}</div>
</div>
</div>
</div>
<a-menu
:default-selected-keys="[currentRoute]"
mode="inline"
theme="light"
@click="handleMenuClick"
>
<template v-for="item in menuItems" :key="item.key">
<a-menu-item v-if="!item.children" :key="item.key">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a-menu-item>
<a-sub-menu v-else :key="item.key">
<template #title>
<component :is="item.icon" />
<span>{{ item.title }}</span>
</template>
<a-menu-item
v-for="child in item.children"
:key="child.key"
>
{{ child.title }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</div>
</a-drawer>
</a-layout-header>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { getMenuItems } from '@/router/routes'
import routes from '@/router/routes'
import {
MenuOutlined,
UserOutlined,
LogoutOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 抽屉显示状态
const drawerVisible = ref(false)
// 当前路由
const currentRoute = computed(() => route.name)
// 用户数据
const userData = computed(() => userStore.userData)
// 菜单项
const menuItems = computed(() => {
const userRole = userStore.getUserRoleName()
return getMenuItems(routes, userRole)
})
// 切换抽屉
const toggleDrawer = () => {
drawerVisible.value = !drawerVisible.value
}
// 处理菜单点击
const handleMenuClick = ({ key }) => {
const menuItem = findMenuItem(menuItems.value, key)
if (menuItem && menuItem.path) {
router.push(menuItem.path)
drawerVisible.value = false // 点击后关闭抽屉
}
}
// 查找菜单项
const findMenuItem = (items, key) => {
for (const item of items) {
if (item.key === key) {
return item
}
if (item.children) {
const found = findMenuItem(item.children, key)
if (found) return found
}
}
return null
}
// 跳转到个人中心
const goToProfile = () => {
router.push('/profile')
drawerVisible.value = false
}
// 登出处理
const handleLogout = async () => {
try {
await userStore.logout()
router.push('/login')
} catch (error) {
console.error('登出失败:', error)
}
}
</script>
<style scoped>
.mobile-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: #001529;
padding: 0;
height: 56px;
line-height: 56px;
}
.mobile-header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 16px;
}
.menu-button,
.user-button {
color: white !important;
border: none !important;
background: transparent !important;
font-size: 18px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.menu-button:hover,
.user-button:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.header-title {
color: white;
font-size: 16px;
font-weight: 600;
flex: 1;
text-align: center;
margin: 0 16px;
}
.drawer-content {
height: 100%;
display: flex;
flex-direction: column;
}
.drawer-header {
padding: 24px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.user-details {
flex: 1;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 4px;
}
.user-role {
font-size: 14px;
color: #8c8c8c;
}
.ant-menu {
flex: 1;
border-right: none;
}
.ant-menu-item,
.ant-menu-submenu-title {
display: flex;
align-items: center;
height: 48px;
line-height: 48px;
}
.ant-menu-item .anticon,
.ant-menu-submenu-title .anticon {
margin-right: 12px;
font-size: 16px;
}
.ant-menu-item-selected {
background-color: #e6f7ff !important;
color: #1890ff !important;
}
.ant-menu-item-selected::after {
display: none;
}
.ant-menu-submenu-selected > .ant-menu-submenu-title {
color: #1890ff;
}
.ant-menu-submenu-open > .ant-menu-submenu-title {
color: #1890ff;
}
</style>