feat: 重构AdminLayout布局组件,拆分为独立子组件并实现全新导航体系
This commit is contained in:
parent
10819fc69a
commit
ecb78e5982
|
|
@ -17,8 +17,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"echarts": "^5.6.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
|
|
|
|||
|
|
@ -11,12 +11,18 @@ importers:
|
|||
'@ant-design/icons-vue':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(vue@3.5.16(typescript@5.8.3))
|
||||
'@types/nprogress':
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
ant-design-vue:
|
||||
specifier: ^4.2.6
|
||||
version: 4.2.6(vue@3.5.16(typescript@5.8.3))
|
||||
echarts:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
pinia:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
|
||||
|
|
@ -750,6 +756,9 @@ packages:
|
|||
'@types/node@22.15.31':
|
||||
resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
|
||||
|
||||
'@types/nprogress@0.2.3':
|
||||
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
|
||||
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
|
|
@ -1743,6 +1752,9 @@ packages:
|
|||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nprogress@0.2.0:
|
||||
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
|
||||
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
|
|
@ -2945,6 +2957,8 @@ snapshots:
|
|||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/nprogress@0.2.3': {}
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||
|
|
@ -4042,6 +4056,8 @@ snapshots:
|
|||
path-key: 4.0.0
|
||||
unicorn-magic: 0.3.0
|
||||
|
||||
nprogress@0.2.0: {}
|
||||
|
||||
nth-check@2.1.1:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ declare module 'vue' {
|
|||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SideMenu: typeof import('./components/layout/SideMenu.vue')['default']
|
||||
TabsBar: typeof import('./components/layout/TabsBar.vue')['default']
|
||||
YourComponent: typeof import('./components/YourComponent.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,363 +1,50 @@
|
|||
<template>
|
||||
<a-layout class="admin-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" class="admin-side" collapsible>
|
||||
<div class="logo">
|
||||
<img alt="Logo" src="@/assets/logo.svg" />
|
||||
<h1 v-show="!collapsed">Admin System</h1>
|
||||
</div>
|
||||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline">
|
||||
<template v-for="menu in menus" :key="menu.key">
|
||||
<!-- 有子菜单的情况 -->
|
||||
<template v-if="menu.children && menu.children.length > 0">
|
||||
<a-sub-menu :key="menu.key">
|
||||
<template #title>
|
||||
<span>
|
||||
<component :is="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.key"
|
||||
@click="() => navigateTo(child.path)"
|
||||
>
|
||||
<component :is="child.icon" />
|
||||
<span>{{ child.title }}</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
<!-- 没有子菜单的情况 -->
|
||||
<template v-else>
|
||||
<a-menu-item :key="menu.key" @click="() => navigateTo(menu.path)">
|
||||
<component :is="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<SideMenu v-model:collapsed="collapsed" :menus="menus" @navigate="navigateTo" />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<a-layout class="admin-right-layout">
|
||||
<!-- 右侧布局 -->
|
||||
<a-layout :class="['admin-right-layout', { 'layout-collapsed': collapsed }]">
|
||||
<!-- 头部 -->
|
||||
<a-layout-header class="admin-header">
|
||||
<div class="header-left">
|
||||
<menu-unfold-outlined
|
||||
v-if="collapsed"
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-dropdown>
|
||||
<a class="user-dropdown" @click.prevent>
|
||||
<a-avatar>
|
||||
<template #icon>
|
||||
<user-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<span class="username">管理员</span>
|
||||
</a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="profile">
|
||||
<user-outlined />
|
||||
个人中心
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<setting-outlined />
|
||||
个人设置
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" @click="handleLogout">
|
||||
<logout-outlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
<HeaderNav v-model:collapsed="collapsed" @logout="handleLogout" />
|
||||
|
||||
<!-- 标签页 -->
|
||||
<div class="admin-tabs">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTab"
|
||||
:hide-add="true"
|
||||
type="editable-card"
|
||||
@edit="onTabEdit"
|
||||
@tabClick="onTabClick"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.path"
|
||||
:closable="tabs.length > 1"
|
||||
:tab="tab.title"
|
||||
>
|
||||
<template #closeIcon>
|
||||
<a-dropdown :trigger="['hover']">
|
||||
<CloseOutlined />
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }: any) => handleTabAction(key, tab)">
|
||||
<a-menu-item key="current">关闭当前标签</a-menu-item>
|
||||
<a-menu-item key="others">关闭其它标签</a-menu-item>
|
||||
<a-menu-item key="left">关闭左侧标签</a-menu-item>
|
||||
<a-menu-item key="right">关闭右侧标签</a-menu-item>
|
||||
<a-menu-item key="all">关闭所有标签</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<TabsBar ref="tabsBarRef" />
|
||||
|
||||
<!-- 内容 -->
|
||||
<a-layout-content class="admin-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition mode="out-in" name="fade">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<a-spin :spinning="loading" class="global-loading" tip="加载中...">
|
||||
<div class="spin-content"></div>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
<!-- 主<EFBFBD><EFBFBD><EFBFBD>容区域 -->
|
||||
<MainContent />
|
||||
|
||||
<!-- 页脚 -->
|
||||
<a-layout-footer class="admin-footer">
|
||||
Admin System ©{{ new Date().getFullYear() }} Created by Your Company
|
||||
</a-layout-footer>
|
||||
<FooterBar />
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
CloseOutlined,
|
||||
DashboardOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import Breadcrumb from '@/components/common/Breadcrumb.vue'
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
// 导入拆分的组件
|
||||
import SideMenu from './SideMenu.vue'
|
||||
import HeaderNav from './HeaderNav.vue'
|
||||
import TabsBar from './TabsBar.vue'
|
||||
import MainContent from './MainContent.vue'
|
||||
import FooterBar from './FooterBar.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tabsBarRef = ref<InstanceType<typeof TabsBar> | null>(null)
|
||||
|
||||
// 侧边栏折叠状态
|
||||
const collapsed = ref(false)
|
||||
|
||||
// 页面加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 标签页相关
|
||||
interface TabItem {
|
||||
title: string
|
||||
path: string
|
||||
name: string
|
||||
query?: Record<string, any>
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
const tabs = ref<TabItem[]>([])
|
||||
const activeTab = ref('')
|
||||
|
||||
// 添加标签页
|
||||
const addTab = (route: any) => {
|
||||
const { path, meta, name, query, params } = route
|
||||
const title = meta?.title || '未命名页面'
|
||||
|
||||
// 检查标签是否已存在
|
||||
const isExist = tabs.value.some((tab) => tab.path === path)
|
||||
|
||||
if (!isExist) {
|
||||
tabs.value.push({
|
||||
title,
|
||||
path,
|
||||
name,
|
||||
query,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
activeTab.value = path
|
||||
}
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (targetPath: string) => {
|
||||
const targetIndex = tabs.value.findIndex((tab) => tab.path === targetPath)
|
||||
|
||||
if (targetIndex === -1) return
|
||||
|
||||
// 如果关闭的是当前激活的标签,则需要激活其他标签
|
||||
if (activeTab.value === targetPath) {
|
||||
// 优先激活右侧标签,如果没有则激活左侧标签
|
||||
if (targetIndex < tabs.value.length - 1) {
|
||||
activeTab.value = tabs.value[targetIndex + 1].path
|
||||
router.push(tabs.value[targetIndex + 1])
|
||||
} else if (targetIndex > 0) {
|
||||
activeTab.value = tabs.value[targetIndex - 1].path
|
||||
router.push(tabs.value[targetIndex - 1])
|
||||
}
|
||||
}
|
||||
|
||||
tabs.value.splice(targetIndex, 1)
|
||||
saveTabsToStorage()
|
||||
}
|
||||
|
||||
// 处理标签页编辑事件
|
||||
const onTabEdit = (targetKey: string, action: 'add' | 'remove') => {
|
||||
if (action === 'remove') {
|
||||
closeTab(targetKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页点击事件
|
||||
const onTabClick = (key: string) => {
|
||||
// 找到对应的标签页
|
||||
const tab = tabs.value.find((item) => item.path === key)
|
||||
if (tab) {
|
||||
// 导航到对应的路由
|
||||
router.push({
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页操作
|
||||
const handleTabAction = (action: string, tab: TabItem) => {
|
||||
const currentIndex = tabs.value.findIndex((item) => item.path === tab.path)
|
||||
|
||||
switch (action) {
|
||||
case 'current':
|
||||
closeTab(tab.path)
|
||||
break
|
||||
case 'others':
|
||||
tabs.value = tabs.value.filter((item) => item.path === tab.path)
|
||||
break
|
||||
case 'left':
|
||||
if (currentIndex > 0) {
|
||||
tabs.value = tabs.value.filter(
|
||||
(item, index) => index >= currentIndex || item.path === '/dashboard',
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'right':
|
||||
if (currentIndex < tabs.value.length - 1) {
|
||||
tabs.value = tabs.value.filter(
|
||||
(item, index) => index <= currentIndex || item.path === '/dashboard',
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'all':
|
||||
// 保留首页
|
||||
const homePage = tabs.value.find((item) => item.path === '/dashboard')
|
||||
tabs.value = homePage ? [homePage] : []
|
||||
if (homePage) {
|
||||
activeTab.value = homePage.path
|
||||
router.push(homePage)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
router.beforeEach((to, from, next) => {
|
||||
loading.value = true
|
||||
|
||||
// 如果路由有meta.title,则添加标签页
|
||||
if (to.meta?.title) {
|
||||
addTab(to)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
// 添加一个小延迟,让加载动画更流畅
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
}, 300)
|
||||
})
|
||||
|
||||
// 保存标签页到本地存储
|
||||
const saveTabsToStorage = () => {
|
||||
localStorage.setItem('admin-tabs', JSON.stringify(tabs.value))
|
||||
localStorage.setItem('admin-active-tab', activeTab.value)
|
||||
}
|
||||
|
||||
// 从本地存储加载标签页
|
||||
const loadTabsFromStorage = () => {
|
||||
const savedTabs = localStorage.getItem('admin-tabs')
|
||||
const savedActiveTab = localStorage.getItem('admin-active-tab')
|
||||
|
||||
if (savedTabs) {
|
||||
try {
|
||||
tabs.value = JSON.parse(savedTabs)
|
||||
|
||||
// 如果有保存的激活标签,则设置它
|
||||
if (savedActiveTab) {
|
||||
activeTab.value = savedActiveTab
|
||||
|
||||
// 导航到激活的标签页
|
||||
const activeTabData = tabs.value.find((tab) => tab.path === savedActiveTab)
|
||||
if (activeTabData && activeTabData.path !== route.path) {
|
||||
router.push({
|
||||
path: activeTabData.path,
|
||||
query: activeTabData.query,
|
||||
})
|
||||
return true // 表示已经从存储中恢复了标签页
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved tabs:', e)
|
||||
}
|
||||
}
|
||||
return false // 表示没有从存储中恢复标签页
|
||||
}
|
||||
|
||||
// 监听标签页变化,保存到本地存储
|
||||
watch(
|
||||
tabs,
|
||||
() => {
|
||||
saveTabsToStorage()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(activeTab, () => {
|
||||
localStorage.setItem('admin-active-tab', activeTab.value)
|
||||
})
|
||||
|
||||
// 初始化标签页
|
||||
onMounted(() => {
|
||||
// 尝试从本地存储加载标签页
|
||||
const restored = loadTabsFromStorage()
|
||||
|
||||
// 如果没有恢复标签页或者标签页为空,则添加当前路由作为初始标签
|
||||
if (!restored || tabs.value.length === 0) {
|
||||
if (route.meta?.title) {
|
||||
addTab(route)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 菜单选中状态
|
||||
const selectedKeys = ref<string[]>([])
|
||||
const openKeys = ref<string[]>([])
|
||||
|
||||
// 模拟菜单数据
|
||||
const menus = reactive([
|
||||
{
|
||||
|
|
@ -394,20 +81,6 @@ const menus = reactive([
|
|||
},
|
||||
])
|
||||
|
||||
// 根据当前路由设置选中的菜单项
|
||||
const updateSelectedMenu = () => {
|
||||
const paths = route.path.split('/')
|
||||
const currentPath = paths[paths.length - 1]
|
||||
|
||||
// 设置选中的菜单项
|
||||
selectedKeys.value = [currentPath]
|
||||
|
||||
// 设置展开的子菜单
|
||||
if (paths.length > 2) {
|
||||
openKeys.value = [paths[1]]
|
||||
}
|
||||
}
|
||||
|
||||
// 导航到指定路径
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
|
|
@ -421,180 +94,36 @@ const handleLogout = () => {
|
|||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
updateSelectedMenu()
|
||||
},
|
||||
)
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start()
|
||||
|
||||
onMounted(() => {
|
||||
updateSelectedMenu()
|
||||
// 如果路由有meta.title,添加标签页
|
||||
if (to.meta?.title && tabsBarRef.value) {
|
||||
tabsBarRef.value.addTab(to)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-side {
|
||||
background: white;
|
||||
box-shadow: 2px 0 6px rgb(238, 238, 238);
|
||||
z-index: 10;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.admin-right-layout {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 64px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
padding: 0 24px 0 0;
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-dropdown:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
margin: 8px 16px 16px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
height: 0; /* 让flex: 1生效 */
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
|
||||
.admin-footer {
|
||||
text-align: center;
|
||||
padding: 16px 50px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.global-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spin-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
background: #fff;
|
||||
padding: 6px 16px 0;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 路由过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<a-layout-footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="copyright">
|
||||
Admin Template ©{{ currentYear }} Created by Your Company
|
||||
Admin Template ©{{ currentYear }} Created by FallingCliff
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="#">帮助</a>
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,24 @@
|
|||
<template>
|
||||
|
||||
<a-layout-header class="header">
|
||||
<a-layout-header class="header">
|
||||
<div class="header-left">
|
||||
<menu-unfold-outlined
|
||||
v-if="collapsed"
|
||||
class="trigger"
|
||||
@click="toggleCollapsed"
|
||||
/>
|
||||
<menu-fold-outlined
|
||||
v-else
|
||||
class="trigger"
|
||||
@click="toggleCollapsed"
|
||||
/>
|
||||
<span class="logo">Admin Template</span>
|
||||
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="toggleCollapsed" />
|
||||
<menu-fold-outlined v-else class="trigger" @click="toggleCollapsed" />
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-space>
|
||||
<a-badge count="5">
|
||||
<a-button type="text">
|
||||
<template #icon><BellOutlined /></template>
|
||||
<template #icon>
|
||||
<BellOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
<a-dropdown>
|
||||
<a-button type="text">
|
||||
<template #icon><UserOutlined /></template>
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
Admin User
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
|
|
@ -37,7 +32,7 @@
|
|||
设置
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">
|
||||
<a-menu-item key="logout" @click="handleLogout">
|
||||
<logout-outlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
|
|
@ -49,23 +44,27 @@
|
|||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
BellOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
BellOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps<{ collapsed: boolean }>()
|
||||
|
||||
const emit = defineEmits(['update:collapsed'])
|
||||
const emit = defineEmits(['update:collapsed', 'logout'])
|
||||
|
||||
function toggleCollapsed() {
|
||||
emit('update:collapsed', !props.collapsed)
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
emit('logout')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -76,7 +75,10 @@ function toggleCollapsed() {
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 1;
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
<template>
|
||||
<a-layout-content class="main-content">
|
||||
<div class="content-header">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item>首页</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>系统管理</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>用户管理</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<div class="content-title">
|
||||
<h2>用户管理</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
|
@ -21,16 +11,69 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 组件逻辑将在后续添加
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
interface BreadcrumbItem {
|
||||
title: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
const breadcrumbs = ref<BreadcrumbItem[]>([])
|
||||
const pageTitle = ref('')
|
||||
|
||||
// 更新面包屑和页面标题
|
||||
const updatePageInfo = () => {
|
||||
const { matched, meta } = route
|
||||
|
||||
// 更新面包屑
|
||||
breadcrumbs.value = matched
|
||||
.filter(item => item.meta?.title) // 只显示有标题的路由
|
||||
.map(item => ({
|
||||
title: item.meta?.title as string,
|
||||
path: item.path
|
||||
}))
|
||||
|
||||
// 如果面包屑为空,至少显示首页
|
||||
if (breadcrumbs.value.length === 0) {
|
||||
breadcrumbs.value = [{ title: '首页', path: '/dashboard' }]
|
||||
}
|
||||
|
||||
// 更新页面标题
|
||||
pageTitle.value = meta?.title as string || '未命名页面'
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
updatePageInfo()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-content {
|
||||
margin: 24px;
|
||||
padding: 24px;
|
||||
flex: 1 1 0;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
margin: 16px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
min-height: 280px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.main-content {
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
|
|
|
|||
|
|
@ -13,70 +13,44 @@
|
|||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon>
|
||||
<dashboard-outlined />
|
||||
<template v-for="menu in menus" :key="menu.key">
|
||||
<!-- 有子菜单的情况 -->
|
||||
<template v-if="menu.children && menu.children.length > 0">
|
||||
<a-sub-menu :key="menu.key">
|
||||
<template #icon>
|
||||
<component :is="menu.icon" />
|
||||
</template>
|
||||
<template #title>{{ menu.title }}</template>
|
||||
<a-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.key"
|
||||
@click="() => navigateTo(child.path)"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="child.icon" />
|
||||
</template>
|
||||
<span>{{ child.title }}</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
<span>仪表盘</span>
|
||||
</a-menu-item>
|
||||
|
||||
<a-sub-menu key="system">
|
||||
<template #icon>
|
||||
<setting-outlined />
|
||||
<!-- 没有子菜单的情况 -->
|
||||
<template v-else>
|
||||
<a-menu-item :key="menu.key" @click="() => navigateTo(menu.path)">
|
||||
<template #icon>
|
||||
<component :is="menu.icon" />
|
||||
</template>
|
||||
<span>{{ menu.title }}</span>
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<template #title>系统管理</template>
|
||||
<a-menu-item key="user">
|
||||
<template #icon>
|
||||
<user-outlined />
|
||||
</template>
|
||||
<span>用户管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="role">
|
||||
<template #icon>
|
||||
<team-outlined />
|
||||
</template>
|
||||
<span>角色管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="menu">
|
||||
<template #icon>
|
||||
<menu-outlined />
|
||||
</template>
|
||||
<span>菜单管理</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<a-sub-menu key="components">
|
||||
<template #icon>
|
||||
<appstore-outlined />
|
||||
</template>
|
||||
<template #title>组件示例</template>
|
||||
<a-menu-item key="table">
|
||||
<template #icon>
|
||||
<table-outlined />
|
||||
</template>
|
||||
<span>表格</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="form">
|
||||
<template #icon>
|
||||
<form-outlined />
|
||||
</template>
|
||||
<span>表单</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="chart">
|
||||
<template #icon>
|
||||
<bar-chart-outlined />
|
||||
</template>
|
||||
<span>图表</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
SettingOutlined,
|
||||
|
|
@ -93,17 +67,114 @@ const props = defineProps<{
|
|||
collapsed: boolean
|
||||
}>()
|
||||
|
||||
const { collapsed } = toRefs(props);
|
||||
|
||||
defineEmits(['update:collapsed'])
|
||||
|
||||
const selectedKeys = ref(['dashboard'])
|
||||
const openKeys = ref(['system'])
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 菜单选中状态
|
||||
const selectedKeys = ref<string[]>([])
|
||||
const openKeys = ref<string[]>([])
|
||||
|
||||
// 菜单数据
|
||||
const menus = reactive([
|
||||
{
|
||||
key: 'dashboard',
|
||||
title: '仪表盘',
|
||||
icon: DashboardOutlined,
|
||||
path: '/dashboard',
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
title: '系统管理',
|
||||
icon: SettingOutlined,
|
||||
children: [
|
||||
{
|
||||
key: 'user',
|
||||
title: '用户管理',
|
||||
icon: UserOutlined,
|
||||
path: '/system/user',
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
title: '角色管理',
|
||||
icon: TeamOutlined,
|
||||
path: '/system/role',
|
||||
},
|
||||
{
|
||||
key: 'menu',
|
||||
title: '菜单管理',
|
||||
icon: MenuOutlined,
|
||||
path: '/system/menu',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'components',
|
||||
title: '组件示例',
|
||||
icon: AppstoreOutlined,
|
||||
children: [
|
||||
{
|
||||
key: 'table',
|
||||
title: '表格',
|
||||
icon: TableOutlined,
|
||||
path: '/components/table',
|
||||
},
|
||||
{
|
||||
key: 'form',
|
||||
title: '表单',
|
||||
icon: FormOutlined,
|
||||
path: '/components/form',
|
||||
},
|
||||
{
|
||||
key: 'chart',
|
||||
title: '图表',
|
||||
icon: BarChartOutlined,
|
||||
path: '/components/chart',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
// 导航到指定路径
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 根据当前路由设置选中的菜单项
|
||||
const updateSelectedMenu = () => {
|
||||
const paths = route.path.split('/')
|
||||
const currentPath = paths[paths.length - 1]
|
||||
|
||||
// 设置选中的菜单项
|
||||
selectedKeys.value = [currentPath]
|
||||
|
||||
// 设置展开的子菜单
|
||||
if (paths.length > 2) {
|
||||
openKeys.value = [paths[1]]
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
updateSelectedMenu()
|
||||
},
|
||||
)
|
||||
|
||||
// 初始化时设置选中的菜单项
|
||||
onMounted(() => {
|
||||
updateSelectedMenu()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.side-menu {
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
background: white;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
@ -113,17 +184,15 @@ const openKeys = ref(['system'])
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 32px;
|
||||
height: 28px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
<template>
|
||||
<div class="admin-tabs">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTab"
|
||||
:hide-add="true"
|
||||
type="editable-card"
|
||||
@edit="onTabEdit"
|
||||
@tabClick="onTabClick"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.path"
|
||||
:closable="tabs.length > 1"
|
||||
:tab="tab.title"
|
||||
>
|
||||
<template #closeIcon>
|
||||
<a-dropdown :trigger="['hover']">
|
||||
<CloseOutlined />
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }: any) => handleTabAction(key, tab)">
|
||||
<a-menu-item key="current">关闭当前标签</a-menu-item>
|
||||
<a-menu-item key="others">关闭其它标签</a-menu-item>
|
||||
<a-menu-item key="left">关闭左侧标签</a-menu-item>
|
||||
<a-menu-item key="right">关闭右侧标签</a-menu-item>
|
||||
<a-menu-item key="all">关闭所有标签</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { CloseOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 标签页相关
|
||||
interface TabItem {
|
||||
title: string
|
||||
path: string
|
||||
name: string
|
||||
query?: Record<string, any>
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
const tabs = ref<TabItem[]>([])
|
||||
const activeTab = ref('')
|
||||
|
||||
// 添加标签页
|
||||
const addTab = (route: any) => {
|
||||
const { path, meta, name, query, params } = route
|
||||
const title = meta?.title || '未命名页面'
|
||||
|
||||
// 检查标签是否已存在
|
||||
const isExist = tabs.value.some((tab) => tab.path === path)
|
||||
|
||||
if (!isExist) {
|
||||
tabs.value.push({
|
||||
title,
|
||||
path,
|
||||
name,
|
||||
query,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
activeTab.value = path
|
||||
}
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (targetPath: string) => {
|
||||
const targetIndex = tabs.value.findIndex((tab) => tab.path === targetPath)
|
||||
|
||||
if (targetIndex === -1) return
|
||||
|
||||
// 如果关闭的是当前激活的标签,则需要激活其他标签
|
||||
if (activeTab.value === targetPath) {
|
||||
// 优先激活右侧标签,如果没有则激活左侧标签
|
||||
if (targetIndex < tabs.value.length - 1) {
|
||||
activeTab.value = tabs.value[targetIndex + 1].path
|
||||
router.push(tabs.value[targetIndex + 1])
|
||||
} else if (targetIndex > 0) {
|
||||
activeTab.value = tabs.value[targetIndex - 1].path
|
||||
router.push(tabs.value[targetIndex - 1])
|
||||
}
|
||||
}
|
||||
|
||||
tabs.value.splice(targetIndex, 1)
|
||||
saveTabsToStorage()
|
||||
}
|
||||
|
||||
// 处理标签页编辑事件
|
||||
const onTabEdit = (targetKey: string, action: 'add' | 'remove') => {
|
||||
if (action === 'remove') {
|
||||
closeTab(targetKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页点击事件
|
||||
const onTabClick = (key: string) => {
|
||||
// 找到对应的标签页
|
||||
const tab = tabs.value.find((item) => item.path === key)
|
||||
if (tab) {
|
||||
// 导航到对应的路由
|
||||
router.push({
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页操作
|
||||
const handleTabAction = (action: string, tab: TabItem) => {
|
||||
const currentIndex = tabs.value.findIndex((item) => item.path === tab.path)
|
||||
|
||||
switch (action) {
|
||||
case 'current':
|
||||
closeTab(tab.path)
|
||||
break
|
||||
case 'others':
|
||||
tabs.value = tabs.value.filter((item) => item.path === tab.path)
|
||||
break
|
||||
case 'left':
|
||||
if (currentIndex > 0) {
|
||||
tabs.value = tabs.value.filter(
|
||||
(item, index) => index >= currentIndex || item.path === '/dashboard',
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'right':
|
||||
if (currentIndex < tabs.value.length - 1) {
|
||||
tabs.value = tabs.value.filter(
|
||||
(item, index) => index <= currentIndex || item.path === '/dashboard',
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'all':
|
||||
// 保留首页
|
||||
const homePage = tabs.value.find((item) => item.path === '/dashboard')
|
||||
tabs.value = homePage ? [homePage] : []
|
||||
if (homePage) {
|
||||
activeTab.value = homePage.path
|
||||
router.push(homePage)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 保存标签页到本地存储
|
||||
const saveTabsToStorage = () => {
|
||||
localStorage.setItem('admin-tabs', JSON.stringify(tabs.value))
|
||||
localStorage.setItem('admin-active-tab', activeTab.value)
|
||||
}
|
||||
|
||||
// 从本地存储加载标签页
|
||||
const loadTabsFromStorage = () => {
|
||||
const savedTabs = localStorage.getItem('admin-tabs')
|
||||
const savedActiveTab = localStorage.getItem('admin-active-tab')
|
||||
|
||||
if (savedTabs) {
|
||||
try {
|
||||
tabs.value = JSON.parse(savedTabs)
|
||||
|
||||
// 如果有保存的激活标签,则设置它
|
||||
if (savedActiveTab) {
|
||||
activeTab.value = savedActiveTab
|
||||
|
||||
// 导航到激活的标签页
|
||||
const activeTabData = tabs.value.find((tab) => tab.path === savedActiveTab)
|
||||
if (activeTabData && activeTabData.path !== route.path) {
|
||||
router.push({
|
||||
path: activeTabData.path,
|
||||
query: activeTabData.query,
|
||||
})
|
||||
return true // 表示已经从存储中恢复了标签页
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved tabs:', e)
|
||||
}
|
||||
}
|
||||
return false // 表示没有从存储中恢复标签页
|
||||
}
|
||||
|
||||
// 监听标签页变化,保存到本地存储
|
||||
watch(
|
||||
tabs,
|
||||
() => {
|
||||
saveTabsToStorage()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(activeTab, () => {
|
||||
localStorage.setItem('admin-active-tab', activeTab.value)
|
||||
})
|
||||
|
||||
// 初始化标签页
|
||||
onMounted(() => {
|
||||
// 尝试从本地存储加载标签页
|
||||
const restored = loadTabsFromStorage()
|
||||
|
||||
// 如果没有恢复标签页或者标签页为空,则添加当前路由作为初始标签
|
||||
if (!restored || tabs.value.length === 0) {
|
||||
if (route.meta?.title) {
|
||||
addTab(route)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
addTab,
|
||||
closeTab
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-tabs {
|
||||
padding: 5px 10px 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,504 @@
|
|||
<template>
|
||||
<div class="role-container">
|
||||
<a-card>
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="table-operations">
|
||||
<a-button type="primary" @click="showModal('add')">
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增角色
|
||||
</a-button>
|
||||
<a-button danger :disabled="!hasSelected" @click="handleBatchDelete">
|
||||
<template #icon><delete-outlined /></template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="roleData"
|
||||
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '1' ? 'green' : 'red'">
|
||||
{{ record.status === '1' ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="showModal('edit', record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="showPermissionModal(record)">权限设置</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此角色吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a class="danger-link">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 角色表单弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="modalType === 'add' ? '新增角色' : '编辑角色'"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="角色名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入角色名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入角色编码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入角色描述"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number
|
||||
v-model:value="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio value="1">启用</a-radio>
|
||||
<a-radio value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 权限设置弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="permissionModalVisible"
|
||||
title="权限设置"
|
||||
width="600px"
|
||||
@ok="handlePermissionOk"
|
||||
@cancel="handlePermissionCancel"
|
||||
>
|
||||
<div v-if="currentRole">
|
||||
<div class="permission-title">
|
||||
<span>当前角色: {{ currentRole.name }}</span>
|
||||
</div>
|
||||
<a-tree
|
||||
v-model:checkedKeys="checkedKeys"
|
||||
:tree-data="permissionTree"
|
||||
checkable
|
||||
:default-expand-all="true"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '角色编码',
|
||||
dataIndex: 'code',
|
||||
key: 'code'
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
key: 'sort',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 250
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟角色数据
|
||||
const generateMockRoleData = () => {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
name: '超级管理员',
|
||||
code: 'ADMIN',
|
||||
description: '系统最高权限,可以操作系统所有功能',
|
||||
sort: 1,
|
||||
status: '1',
|
||||
createTime: '2023-05-01 12:00:00',
|
||||
permissions: ['1', '1-1', '1-2', '1-3', '2', '2-1', '2-2', '3', '3-1', '3-2']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '运营人员',
|
||||
code: 'OPERATOR',
|
||||
description: '负责系统日常运营工作',
|
||||
sort: 2,
|
||||
status: '1',
|
||||
createTime: '2023-05-02 12:00:00',
|
||||
permissions: ['1', '1-1', '1-2', '2', '2-1']
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '财务人员',
|
||||
code: 'FINANCE',
|
||||
description: '负责财务相关工作',
|
||||
sort: 3,
|
||||
status: '1',
|
||||
createTime: '2023-05-03 12:00:00',
|
||||
permissions: ['1', '1-1', '3', '3-1']
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '客服人员',
|
||||
code: 'CUSTOMER_SERVICE',
|
||||
description: '负责客户服务工作',
|
||||
sort: 4,
|
||||
status: '1',
|
||||
createTime: '2023-05-04 12:00:00',
|
||||
permissions: ['1', '1-1', '2', '2-1']
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '访客',
|
||||
code: 'VISITOR',
|
||||
description: '系统访客,只有查看权限',
|
||||
sort: 5,
|
||||
status: '0',
|
||||
createTime: '2023-05-05 12:00:00',
|
||||
permissions: ['1', '1-1']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟权限树数据
|
||||
const permissionTree = [
|
||||
{
|
||||
title: '系统管理',
|
||||
key: '1',
|
||||
children: [
|
||||
{
|
||||
title: '用户管理',
|
||||
key: '1-1',
|
||||
children: [
|
||||
{ title: '查询用户', key: '1-1-1' },
|
||||
{ title: '新增用户', key: '1-1-2' },
|
||||
{ title: '编辑用户', key: '1-1-3' },
|
||||
{ title: '删除用户', key: '1-1-4' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '角色管理',
|
||||
key: '1-2',
|
||||
children: [
|
||||
{ title: '查询角色', key: '1-2-1' },
|
||||
{ title: '新增角色', key: '1-2-2' },
|
||||
{ title: '编辑角色', key: '1-2-3' },
|
||||
{ title: '删除角色', key: '1-2-4' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '菜单管理',
|
||||
key: '1-3',
|
||||
children: [
|
||||
{ title: '查询菜单', key: '1-3-1' },
|
||||
{ title: '新增菜单', key: '1-3-2' },
|
||||
{ title: '编辑菜单', key: '1-3-3' },
|
||||
{ title: '删除菜单', key: '1-3-4' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '内容管理',
|
||||
key: '2',
|
||||
children: [
|
||||
{
|
||||
title: '文章管理',
|
||||
key: '2-1',
|
||||
children: [
|
||||
{ title: '查询文章', key: '2-1-1' },
|
||||
{ title: '新增文章', key: '2-1-2' },
|
||||
{ title: '编辑文章', key: '2-1-3' },
|
||||
{ title: '删除文章', key: '2-1-4' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '分类管理',
|
||||
key: '2-2',
|
||||
children: [
|
||||
{ title: '查询分类', key: '2-2-1' },
|
||||
{ title: '新增分类', key: '2-2-2' },
|
||||
{ title: '编辑分类', key: '2-2-3' },
|
||||
{ title: '删除分类', key: '2-2-4' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '订单管理',
|
||||
key: '3',
|
||||
children: [
|
||||
{
|
||||
title: '订单列表',
|
||||
key: '3-1',
|
||||
children: [
|
||||
{ title: '查询订单', key: '3-1-1' },
|
||||
{ title: '订单详情', key: '3-1-2' },
|
||||
{ title: '订单导出', key: '3-1-3' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '退款管理',
|
||||
key: '3-2',
|
||||
children: [
|
||||
{ title: '查询退款', key: '3-2-1' },
|
||||
{ title: '审核退款', key: '3-2-2' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 状态变量
|
||||
const loading = ref(false)
|
||||
const roleData = ref(generateMockRoleData())
|
||||
const selectedRowKeys = ref<string[]>([])
|
||||
const modalVisible = ref(false)
|
||||
const modalType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const permissionModalVisible = ref(false)
|
||||
const currentRole = ref<any>(null)
|
||||
const checkedKeys = ref<string[]>([])
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: '1'
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: roleData.value.length,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasSelected = computed(() => selectedRowKeys.value.length > 0)
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
fetchRoleData()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const fetchRoleData = () => {
|
||||
loading.value = true
|
||||
// 模拟API请求
|
||||
setTimeout(() => {
|
||||
pagination.total = roleData.value.length
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchRoleData()
|
||||
}
|
||||
|
||||
const onSelectChange = (keys: string[]) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
|
||||
const showModal = (type: 'add' | 'edit', record?: any) => {
|
||||
modalType.value = type
|
||||
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && record) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key in record) {
|
||||
formData[key as keyof typeof formData] = record[key]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 新增时设置默认值
|
||||
formData.status = '1'
|
||||
formData.sort = roleData.value.length + 1
|
||||
}
|
||||
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleModalOk = () => {
|
||||
formRef.value?.validate().then(() => {
|
||||
if (modalType.value === 'add') {
|
||||
// 模拟添加角色
|
||||
const newRole = {
|
||||
id: (roleData.value.length + 1).toString(),
|
||||
...formData,
|
||||
createTime: new Date().toLocaleString(),
|
||||
permissions: []
|
||||
}
|
||||
roleData.value.push(newRole)
|
||||
message.success('添加角色成功')
|
||||
} else {
|
||||
// 模拟编辑角色
|
||||
const index = roleData.value.findIndex(item => item.id === formData.id)
|
||||
if (index !== -1) {
|
||||
roleData.value[index] = {
|
||||
...roleData.value[index],
|
||||
...formData
|
||||
}
|
||||
message.success('编辑角色成功')
|
||||
}
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
fetchRoleData()
|
||||
}).catch(error => {
|
||||
console.log('表单验证失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const showPermissionModal = (record: any) => {
|
||||
currentRole.value = record
|
||||
checkedKeys.value = [...record.permissions]
|
||||
permissionModalVisible.value = true
|
||||
}
|
||||
|
||||
const handlePermissionOk = () => {
|
||||
if (currentRole.value) {
|
||||
// 模拟保存权限
|
||||
const index = roleData.value.findIndex(item => item.id === currentRole.value.id)
|
||||
if (index !== -1) {
|
||||
roleData.value[index].permissions = [...checkedKeys.value]
|
||||
message.success('权限设置成功')
|
||||
}
|
||||
}
|
||||
permissionModalVisible.value = false
|
||||
}
|
||||
|
||||
const handlePermissionCancel = () => {
|
||||
permissionModalVisible.value = false
|
||||
}
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
// 模拟删除角色
|
||||
roleData.value = roleData.value.filter(item => item.id !== record.id)
|
||||
message.success('删除角色成功')
|
||||
|
||||
// 如果删除的是已选中的行,需要更新selectedRowKeys
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter(key => key !== record.id)
|
||||
|
||||
fetchRoleData()
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
// 模拟批量删除角色
|
||||
roleData.value = roleData.value.filter(item => !selectedRowKeys.value.includes(item.id))
|
||||
message.success(`成功删除 ${selectedRowKeys.value.length} 个角色`)
|
||||
selectedRowKeys.value = []
|
||||
fetchRoleData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.role-container {
|
||||
padding: 0;
|
||||
|
||||
.table-operations {
|
||||
margin-bottom: 16px;
|
||||
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-title {
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.danger-link {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
<template>
|
||||
<div class="user-container">
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<a-form layout="inline" :model="searchForm" class="search-form">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model:value="searchForm.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号">
|
||||
<a-input v-model:value="searchForm.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="1">启用</a-select-option>
|
||||
<a-select-option value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><search-outlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch">
|
||||
<template #icon><reload-outlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="table-operations">
|
||||
<a-button type="primary" @click="showModal('add')">
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增用户
|
||||
</a-button>
|
||||
<a-button danger :disabled="!hasSelected" @click="handleBatchDelete">
|
||||
<template #icon><delete-outlined /></template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="userData"
|
||||
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '1' ? 'green' : 'red'">
|
||||
{{ record.status === '1' ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="showModal('edit', record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此用户吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a class="danger-link">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 用户表单弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="modalType === 'add' ? '新增用户' : '编辑用户'"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formData.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="密码"
|
||||
name="password"
|
||||
:rules="modalType === 'add' ? formRules.password : undefined"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="formData.password"
|
||||
placeholder="请输入密码"
|
||||
:disabled="modalType === 'edit'"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="姓名" name="realName">
|
||||
<a-input v-model:value="formData.realName" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色" name="roleIds">
|
||||
<a-select
|
||||
v-model:value="formData.roleIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="role in roleOptions" :key="role.value" :value="role.value">
|
||||
{{ role.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio value="1">启用</a-radio>
|
||||
<a-radio value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'realName',
|
||||
key: 'realName'
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone'
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email'
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'roleName',
|
||||
key: 'roleName'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150
|
||||
}
|
||||
]
|
||||
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ label: '管理员', value: '1' },
|
||||
{ label: '运营', value: '2' },
|
||||
{ label: '财务', value: '3' },
|
||||
{ label: '客服', value: '4' }
|
||||
]
|
||||
|
||||
// 模拟用户数据
|
||||
const generateMockData = () => {
|
||||
const data = []
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
data.push({
|
||||
id: i.toString(),
|
||||
username: `user${i}`,
|
||||
realName: `用户${i}`,
|
||||
phone: `1381234${i.toString().padStart(4, '0')}`,
|
||||
email: `user${i}@example.com`,
|
||||
roleIds: i % 3 === 0 ? ['1'] : i % 3 === 1 ? ['2'] : ['3', '4'],
|
||||
roleName: i % 3 === 0 ? '管理员' : i % 3 === 1 ? '运营' : '财务, 客服',
|
||||
status: i % 4 === 0 ? '0' : '1',
|
||||
createTime: `2023-05-${i.toString().padStart(2, '0')} 12:00:00`
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 状态变量
|
||||
const loading = ref(false)
|
||||
const userData = ref(generateMockData())
|
||||
const selectedRowKeys = ref<string[]>([])
|
||||
const modalVisible = ref(false)
|
||||
const modalType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
phone: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
roleIds: [] as string[],
|
||||
status: '1'
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||||
],
|
||||
roleIds: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasSelected = computed(() => selectedRowKeys.value.length > 0)
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
fetchUserData()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const fetchUserData = () => {
|
||||
loading.value = true
|
||||
// 模拟API请求
|
||||
setTimeout(() => {
|
||||
const filteredData = userData.value.filter(item => {
|
||||
return (
|
||||
(searchForm.username ? item.username.includes(searchForm.username) : true) &&
|
||||
(searchForm.phone ? item.phone.includes(searchForm.phone) : true) &&
|
||||
(searchForm.status ? item.status === searchForm.status : true)
|
||||
)
|
||||
})
|
||||
|
||||
pagination.total = filteredData.length
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchUserData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.keys(searchForm).forEach(key => {
|
||||
searchForm[key as keyof typeof searchForm] = ''
|
||||
})
|
||||
pagination.current = 1
|
||||
fetchUserData()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchUserData()
|
||||
}
|
||||
|
||||
const onSelectChange = (keys: string[]) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
|
||||
const showModal = (type: 'add' | 'edit', record?: any) => {
|
||||
modalType.value = type
|
||||
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && record) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key in record) {
|
||||
formData[key as keyof typeof formData] = record[key]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 新增时设置默认值
|
||||
formData.status = '1'
|
||||
formData.roleIds = []
|
||||
}
|
||||
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleModalOk = () => {
|
||||
formRef.value?.validate().then(() => {
|
||||
if (modalType.value === 'add') {
|
||||
// 模拟添加用户
|
||||
const newUser = {
|
||||
id: (userData.value.length + 1).toString(),
|
||||
...formData,
|
||||
roleName: formData.roleIds.map(id =>
|
||||
roleOptions.find(role => role.value === id)?.label || ''
|
||||
).join(', '),
|
||||
createTime: new Date().toLocaleString()
|
||||
}
|
||||
userData.value.unshift(newUser)
|
||||
message.success('添加用户成功')
|
||||
} else {
|
||||
// 模拟编辑用户
|
||||
const index = userData.value.findIndex(item => item.id === formData.id)
|
||||
if (index !== -1) {
|
||||
userData.value[index] = {
|
||||
...userData.value[index],
|
||||
...formData,
|
||||
roleName: formData.roleIds.map(id =>
|
||||
roleOptions.find(role => role.value === id)?.label || ''
|
||||
).join(', ')
|
||||
}
|
||||
message.success('编辑用户成功')
|
||||
}
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
fetchUserData()
|
||||
}).catch(error => {
|
||||
console.log('表单验证失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
// 模拟删除用户
|
||||
userData.value = userData.value.filter(item => item.id !== record.id)
|
||||
message.success('删除用户成功')
|
||||
|
||||
// 如果删除的是已选中的行,需要更新selectedRowKeys
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter(key => key !== record.id)
|
||||
|
||||
fetchUserData()
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
// 模拟批量删除用户
|
||||
userData.value = userData.value.filter(item => !selectedRowKeys.value.includes(item.id))
|
||||
message.success(`成功删除 ${selectedRowKeys.value.length} 个用户`)
|
||||
selectedRowKeys.value = []
|
||||
fetchUserData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-container {
|
||||
padding: 0;
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-operations {
|
||||
margin-bottom: 16px;
|
||||
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-link {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue