布局框架搭建
This commit is contained in:
@@ -5,13 +5,51 @@
|
||||
const userRoutes = [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
name: 'Home',
|
||||
component: 'home',
|
||||
meta: {
|
||||
title: 'dashboard',
|
||||
icon: 'DashboardOutlined',
|
||||
role: ['admin']
|
||||
title: '首页',
|
||||
icon: 'HomeOutlined',
|
||||
affix: true,
|
||||
noCache: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
meta: {
|
||||
title: '系统管理',
|
||||
icon: 'SettingOutlined'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/system/user',
|
||||
name: 'SystemUser',
|
||||
component: 'system/user',
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'UserOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/role',
|
||||
name: 'SystemRole',
|
||||
component: 'system/role',
|
||||
meta: {
|
||||
title: '角色管理',
|
||||
icon: 'TeamOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/menu',
|
||||
name: 'SystemMenu',
|
||||
component: 'system/menu',
|
||||
meta: {
|
||||
title: '菜单管理',
|
||||
icon: 'MenuOutlined'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
9
src/layouts/components/setting.vue
Normal file
9
src/layouts/components/setting.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<a-drawer v-model:open="open" title="布局配置"></a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const open = ref(false)
|
||||
</script>
|
||||
174
src/layouts/components/sideMenu.vue
Normal file
174
src/layouts/components/sideMenu.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<a-menu mode="inline" :theme="theme" :collapsed="collapsed" :selected-keys="selectedKeys" :open-keys="openKeys"
|
||||
@select="handleSelect" @open-change="handleOpenChange" class="side-menu">
|
||||
<template v-for="item in menuList">
|
||||
<!-- 有子菜单 -->
|
||||
<a-sub-menu v-if="item.children && item.children.length > 0" :key="item.path + '-submenu'">
|
||||
<template #icon>
|
||||
<component :is="item.meta?.icon || 'MenuOutlined'" />
|
||||
</template>
|
||||
<template #title>{{ item.meta?.title || item.name }}</template>
|
||||
<a-menu-item v-for="child in item.children.filter(sub => !sub.children || sub.children.length === 0)"
|
||||
:key="child.path">
|
||||
<template #icon>
|
||||
<component :is="child.meta?.icon || 'FileOutlined'" />
|
||||
</template>
|
||||
{{ child.meta?.title || child.name }}
|
||||
</a-menu-item>
|
||||
<a-sub-menu v-for="child in item.children.filter(sub => sub.children && sub.children.length > 0)"
|
||||
:key="child.path">
|
||||
<template #icon>
|
||||
<component :is="child.meta?.icon || 'AppstoreOutlined'" />
|
||||
</template>
|
||||
<template #title>{{ child.meta?.title || child.name }}</template>
|
||||
<a-menu-item v-for="grandChild in child.children" :key="grandChild.path">
|
||||
<template #icon>
|
||||
<component :is="grandChild.meta?.icon || 'FileOutlined'" />
|
||||
</template>
|
||||
{{ grandChild.meta?.title || grandChild.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-sub-menu>
|
||||
<!-- 无子菜单 -->
|
||||
<a-menu-item v-else :key="item.path + '-item'">
|
||||
<template #icon>
|
||||
<component :is="item.meta?.icon || 'MenuOutlined'" />
|
||||
</template>
|
||||
{{ item.meta?.title || item.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import { getUserMenu } from '@/api/menu'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light'
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
const menuList = ref([])
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
|
||||
// 获取菜单数据
|
||||
const getMenuList = async () => {
|
||||
try {
|
||||
const res = await getUserMenu()
|
||||
if (res.code === 200) {
|
||||
menuList.value = res.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error)
|
||||
// 模拟数据
|
||||
menuList.value = [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
meta: { title: '首页', icon: 'HomeOutlined' }
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
meta: { title: '系统管理', icon: 'SettingOutlined' },
|
||||
children: [
|
||||
{
|
||||
path: '/system/user',
|
||||
name: 'User',
|
||||
meta: { title: '用户管理', icon: 'UserOutlined' }
|
||||
},
|
||||
{
|
||||
path: '/system/role',
|
||||
name: 'Role',
|
||||
meta: { title: '角色管理', icon: 'TeamOutlined' }
|
||||
},
|
||||
{
|
||||
path: '/system/menu',
|
||||
name: 'Menu',
|
||||
meta: { title: '菜单管理', icon: 'MenuOutlined' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中的菜单
|
||||
const updateSelectedKeys = () => {
|
||||
selectedKeys.value = [route.path]
|
||||
|
||||
// 获取父级菜单路径
|
||||
const matched = route.matched
|
||||
.filter(item => item.path !== '/' && item.path !== route.path)
|
||||
.map(item => item.path)
|
||||
|
||||
// 折叠时不自动展开
|
||||
if (!props.collapsed) {
|
||||
openKeys.value = matched
|
||||
}
|
||||
}
|
||||
|
||||
// 处理菜单选择
|
||||
const handleSelect = ({ key }) => {
|
||||
router.push(key)
|
||||
}
|
||||
|
||||
// 处理菜单展开/收起
|
||||
const handleOpenChange = (keys) => {
|
||||
openKeys.value = keys
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
updateSelectedKeys()
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听折叠状态
|
||||
watch(() => props.collapsed, (val) => {
|
||||
if (val) {
|
||||
openKeys.value = []
|
||||
} else {
|
||||
updateSelectedKeys()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getMenuList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.side-menu {
|
||||
height: calc(100% - 64px);
|
||||
border-right: none;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
274
src/layouts/components/tags.vue
Normal file
274
src/layouts/components/tags.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="tags-view" v-show="showTags">
|
||||
<a-dropdown :trigger="['contextmenu']">
|
||||
<div class="tags-wrapper">
|
||||
<a-space :size="4">
|
||||
<a-tag v-for="tag in visitedViews" :key="tag.fullPath" :closable="!tag.meta?.affix"
|
||||
@close="closeSelectedTag(tag)" @click="clickTag(tag)"
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)" class="tag-item"
|
||||
:class="{ active: isActive(tag) }">
|
||||
{{ tag.meta?.title || tag.name }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleMenuClick">
|
||||
<a-menu-item key="refresh">
|
||||
<ReloadOutlined />
|
||||
<span>刷新</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="close" v-if="!selectedTag.meta?.affix">
|
||||
<CloseOutlined />
|
||||
<span>关闭</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="closeOthers">
|
||||
<ColumnWidthOutlined />
|
||||
<span>关闭其他</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="closeAll">
|
||||
<CloseCircleOutlined />
|
||||
<span>关闭所有</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<div class="tags-actions">
|
||||
<a-tooltip title="刷新当前页">
|
||||
<a-button type="text" size="small" @click="refreshSelectedTag">
|
||||
<ReloadOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="关闭其他">
|
||||
<a-button type="text" size="small" @click="closeOthersTags">
|
||||
<ColumnWidthOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="关闭所有">
|
||||
<a-button type="text" size="small" @click="closeAllTags">
|
||||
<CloseCircleOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
ColumnWidthOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
const showTags = ref(true)
|
||||
const selectedTag = ref({})
|
||||
const visitedViews = computed(() => layoutStore.viewTags)
|
||||
|
||||
// 判断是否是当前激活的标签
|
||||
const isActive = (tag) => {
|
||||
return tag.fullPath === route.fullPath
|
||||
}
|
||||
|
||||
// 添加标签
|
||||
const addTags = () => {
|
||||
const { name } = route
|
||||
if (name && !route.meta?.noCache) {
|
||||
layoutStore.updateViewTags({
|
||||
fullPath: route.fullPath,
|
||||
path: route.path,
|
||||
name: name,
|
||||
query: route.query,
|
||||
params: route.params,
|
||||
meta: route.meta
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 移除标签
|
||||
const closeSelectedTag = (view) => {
|
||||
layoutStore.removeViewTags(view.fullPath)
|
||||
if (isActive(view)) {
|
||||
toLastView(visitedViews.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭其他标签
|
||||
const closeOthersTags = () => {
|
||||
router.push(selectedTag.value)
|
||||
layoutStore.viewTags = visitedViews.value.filter(tag => tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath)
|
||||
}
|
||||
|
||||
// 关闭所有标签
|
||||
const closeAllTags = () => {
|
||||
const affixTags = visitedViews.value.filter(tag => tag.meta?.affix)
|
||||
layoutStore.viewTags = affixTags
|
||||
if (affixTags.length > 0) {
|
||||
router.push(affixTags[0].fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到最后一个标签
|
||||
const toLastView = (visitedViews) => {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
router.push(latestView.fullPath)
|
||||
} else {
|
||||
// 如果没有标签,跳转到首页
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
// 点击标签
|
||||
const clickTag = (tag) => {
|
||||
selectedTag.value = tag
|
||||
router.push(tag.fullPath)
|
||||
}
|
||||
|
||||
// 刷新当前标签
|
||||
const refreshSelectedTag = () => {
|
||||
const { fullPath } = route
|
||||
router.replace({
|
||||
path: '/redirect' + fullPath
|
||||
})
|
||||
}
|
||||
|
||||
// 右键菜单处理
|
||||
const handleContextMenu = (event, tag) => {
|
||||
event.preventDefault()
|
||||
selectedTag.value = tag
|
||||
}
|
||||
|
||||
// 菜单点击处理
|
||||
const handleMenuClick = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'refresh':
|
||||
refreshSelectedTag()
|
||||
break
|
||||
case 'close':
|
||||
if (!selectedTag.value.meta?.affix) {
|
||||
closeSelectedTag(selectedTag.value)
|
||||
}
|
||||
break
|
||||
case 'closeOthers':
|
||||
closeOthersTags()
|
||||
break
|
||||
case 'closeAll':
|
||||
closeAllTags()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
addTags()
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听路由更新
|
||||
onBeforeRouteUpdate((to) => {
|
||||
addTags()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
addTags()
|
||||
// 初始化选中的标签
|
||||
selectedTag.value = visitedViews.value.find(tag => isActive(tag)) || {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tags-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 0 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.tags-wrapper {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 12px;
|
||||
margin: 0 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #fafafa;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background-color: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tag-close-icon) {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.ant-btn) {
|
||||
font-size: 14px;
|
||||
padding: 2px 6px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
91
src/layouts/components/topMenu.vue
Normal file
91
src/layouts/components/topMenu.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<a-menu mode="horizontal" :selected-keys="selectedKeys" @select="handleSelect" class="top-menu">
|
||||
<a-menu-item v-for="item in menuList" :key="item.path">
|
||||
<template #icon>
|
||||
<component :is="item.meta?.icon || 'AppstoreOutlined'" />
|
||||
</template>
|
||||
{{ item.meta?.title || item.name }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getUserMenu } from '@/api/menu'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const menuList = ref([])
|
||||
const selectedKeys = ref([])
|
||||
|
||||
// 获取菜单数据
|
||||
const getMenuList = async () => {
|
||||
try {
|
||||
const res = await getUserMenu()
|
||||
if (res.code === 200) {
|
||||
// 只显示一级菜单
|
||||
menuList.value = (res.data || []).filter(item => !item.meta?.hidden)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error)
|
||||
// 模拟数据
|
||||
menuList.value = [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
meta: { title: '首页', icon: 'HomeOutlined' }
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
meta: { title: '系统管理', icon: 'SettingOutlined' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中的菜单
|
||||
const updateSelectedKeys = () => {
|
||||
// 查找当前路由对应的顶级菜单
|
||||
const path = route.path
|
||||
const matched = route.matched
|
||||
if (matched.length > 0) {
|
||||
const topPath = matched[0].path
|
||||
selectedKeys.value = [topPath]
|
||||
}
|
||||
}
|
||||
|
||||
// 处理菜单选择
|
||||
const handleSelect = ({ key }) => {
|
||||
router.push(key)
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
updateSelectedKeys()
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
getMenuList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.top-menu {
|
||||
border-bottom: none;
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
|
||||
:deep(.ant-menu-item) {
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
251
src/layouts/components/userbar.vue
Normal file
251
src/layouts/components/userbar.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="userbar">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<div class="user-info">
|
||||
<a-avatar :size="32" :src="userStore.user?.avatar || ''">
|
||||
{{ userStore.user?.username?.charAt(0)?.toUpperCase() || 'U' }}
|
||||
</a-avatar>
|
||||
<span class="username">{{ userStore.user?.username || 'Admin' }}</span>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleMenuClick">
|
||||
<a-menu-item key="profile">
|
||||
<UserOutlined />
|
||||
<span>个人中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<SettingOutlined />
|
||||
<span>系统设置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">
|
||||
<LogoutOutlined />
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-tooltip title="全屏">
|
||||
<a-button type="text" @click="toggleFullscreen" class="action-btn">
|
||||
<FullscreenOutlined v-if="!isFullscreen" />
|
||||
<FullscreenExitOutlined v-else />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip title="布局设置">
|
||||
<a-button type="text" @click="showSetting = true" class="action-btn">
|
||||
<SettingOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 布局设置抽屉 -->
|
||||
<a-drawer v-model:open="showSetting" title="布局设置" placement="right" :width="280">
|
||||
<div class="setting-content">
|
||||
<div class="setting-item">
|
||||
<div class="setting-title">布局模式</div>
|
||||
<a-radio-group v-model:value="layoutStore.layoutMode" @change="handleLayoutChange">
|
||||
<a-radio value="default">默认布局</a-radio>
|
||||
<a-radio value="menu">菜单布局</a-radio>
|
||||
<a-radio value="top">顶部布局</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-title">主题颜色</div>
|
||||
<div class="color-list">
|
||||
<div v-for="color in themeColors" :key="color" class="color-item"
|
||||
:class="{ active: themeColor === color }" :style="{ backgroundColor: color }"
|
||||
@click="changeThemeColor(color)">
|
||||
<CheckOutlined v-if="themeColor === color" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import {
|
||||
DownOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
CheckOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
const showSetting = ref(false)
|
||||
const themeColor = ref('#1890ff')
|
||||
|
||||
const themeColors = [
|
||||
'#1890ff',
|
||||
'#f5222d',
|
||||
'#fa541c',
|
||||
'#faad14',
|
||||
'#13c2c2',
|
||||
'#52c41a',
|
||||
'#2f54eb',
|
||||
'#722ed1'
|
||||
]
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen()
|
||||
isFullscreen.value = true
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
isFullscreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听全屏变化
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||
})
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
showSetting.value = true
|
||||
break
|
||||
case 'logout':
|
||||
handleLogout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
Modal.confirm({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await userStore.logout()
|
||||
message.success('退出成功')
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
message.error('退出失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换布局
|
||||
const handleLayoutChange = (e) => {
|
||||
layoutStore.setLayoutMode(e.target.value)
|
||||
message.success(`已切换到${e.target.value === 'default' ? '默认' : e.target.value === 'menu' ? '菜单' : '顶部'}布局`)
|
||||
}
|
||||
|
||||
// 切换主题颜色
|
||||
const changeThemeColor = (color) => {
|
||||
themeColor.value = color
|
||||
// 这里可以实现主题切换逻辑
|
||||
message.success('主题颜色已更新')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.userbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 16px;
|
||||
padding: 4px 8px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
.setting-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.setting-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.color-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.color-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
.anticon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,33 +2,131 @@
|
||||
<a-layout class="app-wrapper" :class="layoutClass">
|
||||
<!-- 默认布局:左侧双栏布局 -->
|
||||
<template v-if="layoutMode === 'default'">
|
||||
<a-layout-sider theme="dark" width="70"></a-layout-sider>
|
||||
<a-layout-sider theme="light"></a-layout-sider>
|
||||
<a-layout>
|
||||
<a-layout-sider theme="dark" width="70" class="logo-sidebar">
|
||||
<div class="logo-box">
|
||||
<span class="logo-text">VUE</span>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
<a-layout-sider theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse"
|
||||
class="menu-sidebar" width="200" :collapsed-width="64">
|
||||
<side-menu :collapsed="sidebarCollapsed" />
|
||||
</a-layout-sider>
|
||||
<a-layout class="main-layout">
|
||||
<a-layout-header class="app-header">
|
||||
<div class="header-left">
|
||||
<a-button type="text" :icon="sidebarCollapsed ? 'menu-unfold' : 'menu-fold'"
|
||||
@click="toggleSidebar" class="collapse-btn" />
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<userbar />
|
||||
</a-layout-header>
|
||||
<a-layout-content><router-view></router-view></a-layout-content>
|
||||
<tags />
|
||||
<a-layout-content class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<!-- Menu布局:左侧菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'menu'">
|
||||
<a-layout-sider theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse"
|
||||
class="full-menu-sidebar" width="200" :collapsed-width="64">
|
||||
<div class="logo-box-full">
|
||||
<span v-if="!sidebarCollapsed" class="logo-text">VUE ADMIN</span>
|
||||
<span v-else class="logo-text-mini">V</span>
|
||||
</div>
|
||||
<side-menu :collapsed="sidebarCollapsed" />
|
||||
</a-layout-sider>
|
||||
<a-layout class="main-layout">
|
||||
<a-layout-header class="app-header">
|
||||
<div class="header-left">
|
||||
<a-button type="text" :icon="sidebarCollapsed ? 'menu-unfold' : 'menu-fold'"
|
||||
@click="toggleSidebar" class="collapse-btn" />
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<userbar />
|
||||
</a-layout-header>
|
||||
<tags />
|
||||
<a-layout-content class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<!-- Top布局:顶部菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'top'">
|
||||
<a-layout-header class="app-header top-header">
|
||||
<div class="top-header-left">
|
||||
<div class="logo-box-top">
|
||||
<span class="logo-text">VUE ADMIN</span>
|
||||
</div>
|
||||
<topMenu />
|
||||
</div>
|
||||
<userbar />
|
||||
</a-layout-header>
|
||||
<tags />
|
||||
<a-layout-content class="app-main top-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</template>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import breadcrumb from './components/breadcrumb.vue';
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
|
||||
const layoutMode = ref('default')
|
||||
const layoutClass = ref('layout-default')
|
||||
import userbar from './components/userbar.vue'
|
||||
import breadcrumb from './components/breadcrumb.vue'
|
||||
import tags from './components/tags.vue'
|
||||
import topMenu from './components/topMenu.vue'
|
||||
import sideMenu from './components/sideMenu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
const layoutMode = computed(() => layoutStore.layoutMode)
|
||||
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed)
|
||||
|
||||
// 缓存的视图列表
|
||||
const cachedViews = computed(() => {
|
||||
return layoutStore.viewTags
|
||||
.filter(tag => !tag.meta?.noCache)
|
||||
.map(tag => tag.name)
|
||||
})
|
||||
|
||||
// 布局类名
|
||||
const layoutClass = computed(() => {
|
||||
return {
|
||||
'layout-default': layoutMode.value === 'default',
|
||||
'layout-menu': layoutMode.value === 'menu',
|
||||
'layout-top': layoutMode.value === 'top',
|
||||
'is-collapse': sidebarCollapsed.value
|
||||
}
|
||||
})
|
||||
|
||||
// 切换侧边栏
|
||||
const toggleSidebar = () => {
|
||||
layoutStore.toggleSidebar()
|
||||
}
|
||||
|
||||
// 处理折叠
|
||||
const handleCollapse = (collapsed) => {
|
||||
layoutStore.sidebarCollapsed = collapsed
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -37,97 +135,159 @@ const layoutClass = ref('layout-default')
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
padding-inline: 20px;
|
||||
height: 50px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 9;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.collapse-btn {
|
||||
font-size: 16px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 16px;
|
||||
background-color: #f0f2f5;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
height: calc(100vh - 106px);
|
||||
}
|
||||
|
||||
/* 默认布局 */
|
||||
&.layout-default {
|
||||
flex-direction: row;
|
||||
.logo-sidebar {
|
||||
background-color: #001529;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.main-container {
|
||||
margin-left: 200px;
|
||||
.logo-box {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.has-sidebar {
|
||||
margin-left: 200px;
|
||||
.logo-text {
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
width: calc(100% - 200px);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapse {
|
||||
.main-container {
|
||||
margin-left: 64px;
|
||||
.menu-sidebar {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
width: calc(100% - 64px);
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
height: calc(100vh - 106px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu布局 */
|
||||
&.layout-menu {
|
||||
.main-container {
|
||||
margin-left: 200px;
|
||||
.full-menu-sidebar {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
z-index: 10;
|
||||
|
||||
&.has-sidebar {
|
||||
margin-left: 200px;
|
||||
.logo-box-full {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.logo-text {
|
||||
color: #1890ff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
width: calc(100% - 200px);
|
||||
.logo-text-mini {
|
||||
color: #1890ff;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapse {
|
||||
.main-container {
|
||||
margin-left: 64px;
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
width: calc(100% - 64px);
|
||||
}
|
||||
.app-header {
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
height: calc(100vh - 106px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Top布局 */
|
||||
&.layout-top {
|
||||
.top-layout-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.top-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1002;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.top-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.logo-box-top {
|
||||
.logo-text {
|
||||
color: #1890ff;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-main {
|
||||
padding-top: 50px;
|
||||
|
||||
.top-userbar {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
z-index: 1001;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding-top: 60px;
|
||||
}
|
||||
}
|
||||
.top-content {
|
||||
height: calc(100vh - 116px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
193
src/pages/system/menu/index.vue
Normal file
193
src/pages/system/menu/index.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="system-menu">
|
||||
<a-card title="菜单管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined />
|
||||
新增菜单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="false" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'icon'">
|
||||
<component :is="record.icon || 'MenuOutlined'" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleAddChild(record)">新增子菜单</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, MenuOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '菜单名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title'
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
dataIndex: 'icon',
|
||||
key: 'icon',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '路径',
|
||||
dataIndex: 'path',
|
||||
key: 'path'
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
key: 'sort',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220
|
||||
}
|
||||
]
|
||||
|
||||
const dataSource = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
title: '首页',
|
||||
path: '/home',
|
||||
icon: 'HomeOutlined',
|
||||
type: 1,
|
||||
sort: 1,
|
||||
status: 1,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '系统管理',
|
||||
path: '/system',
|
||||
icon: 'SettingOutlined',
|
||||
type: 1,
|
||||
sort: 2,
|
||||
status: 1,
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
title: '用户管理',
|
||||
path: '/system/user',
|
||||
icon: 'UserOutlined',
|
||||
type: 2,
|
||||
sort: 1,
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
title: '角色管理',
|
||||
path: '/system/role',
|
||||
icon: 'TeamOutlined',
|
||||
type: 2,
|
||||
sort: 2,
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
title: '菜单管理',
|
||||
path: '/system/menu',
|
||||
icon: 'MenuOutlined',
|
||||
type: 2,
|
||||
sort: 3,
|
||||
status: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
// 模拟接口请求
|
||||
setTimeout(() => {
|
||||
dataSource.value = mockData
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
1: 'blue',
|
||||
2: 'green',
|
||||
3: 'orange'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type) => {
|
||||
const textMap = {
|
||||
1: '目录',
|
||||
2: '菜单',
|
||||
3: '按钮'
|
||||
}
|
||||
return textMap[type] || '未知'
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
message.info('新增菜单功能开发中')
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
message.info('编辑菜单功能开发中')
|
||||
}
|
||||
|
||||
const handleAddChild = (record) => {
|
||||
message.info(`新增 ${record.title} 的子菜单功能开发中`)
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
message.info('删除菜单功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-menu {
|
||||
// 样式
|
||||
}
|
||||
</style>
|
||||
141
src/pages/system/role/index.vue
Normal file
141
src/pages/system/role/index.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="system-role">
|
||||
<a-card title="角色管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined />
|
||||
新增角色
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination">
|
||||
<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-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handlePermission(record)">权限</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'roleName',
|
||||
key: 'roleName'
|
||||
},
|
||||
{
|
||||
title: '角色标识',
|
||||
dataIndex: 'roleCode',
|
||||
key: 'roleCode'
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180
|
||||
}
|
||||
]
|
||||
|
||||
const dataSource = ref([])
|
||||
const loading = ref(false)
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
roleName: '管理员',
|
||||
roleCode: 'admin',
|
||||
description: '系统管理员',
|
||||
status: 1,
|
||||
createTime: '2024-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
roleName: '普通用户',
|
||||
roleCode: 'user',
|
||||
description: '普通用户角色',
|
||||
status: 1,
|
||||
createTime: '2024-01-02 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
// 模拟接口请求
|
||||
setTimeout(() => {
|
||||
dataSource.value = mockData
|
||||
pagination.value.total = mockData.length
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
message.info('新增角色功能开发中')
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
message.info('编辑角色功能开发中')
|
||||
}
|
||||
|
||||
const handlePermission = (record) => {
|
||||
message.info('权限配置功能开发中')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
message.info('删除角色功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-role {
|
||||
// 样式
|
||||
}
|
||||
</style>
|
||||
172
src/pages/system/user/index.vue
Normal file
172
src/pages/system/user/index.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="system-user">
|
||||
<a-card title="用户管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined />
|
||||
新增用户
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="search-form">
|
||||
<a-form :model="searchForm" layout="inline">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model:value="searchForm.username" 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">查询</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination">
|
||||
<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-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username'
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150
|
||||
}
|
||||
]
|
||||
|
||||
const searchForm = ref({
|
||||
username: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const dataSource = ref([])
|
||||
const loading = ref(false)
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nickname: '管理员',
|
||||
status: 1,
|
||||
createTime: '2024-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user',
|
||||
nickname: '普通用户',
|
||||
status: 1,
|
||||
createTime: '2024-01-02 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
// 模拟接口请求
|
||||
setTimeout(() => {
|
||||
dataSource.value = mockData
|
||||
pagination.value.total = mockData.length
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.value = {
|
||||
username: '',
|
||||
status: ''
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
message.info('新增用户功能开发中')
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
message.info('编辑用户功能开发中')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
message.info('删除用户功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-user {
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user