feat: 动态菜单

This commit is contained in:
zy7y 2022-09-13 16:53:31 +08:00
parent 9ce271d691
commit 0417ceb6d4
28 changed files with 807 additions and 824 deletions

View File

@ -1,6 +1,6 @@
from fastapi import Query from fastapi import Query
from dbhelper.menu import del_menu, get_menus, insert_menu from dbhelper.menu import del_menu, get_menus, insert_menu, put_menu
from schemas import ListAll, MenuIn, MenuRead, Response from schemas import ListAll, MenuIn, MenuRead, Response
@ -21,3 +21,11 @@ async def menu_del(pk: int) -> Response:
if await del_menu(pk) == 0: if await del_menu(pk) == 0:
return Response(code=400, msg="菜单不存在") return Response(code=400, msg="菜单不存在")
return Response() return Response()
async def menu_put(pk: int, data: MenuIn) -> Response:
"""更新菜单"""
if await put_menu(pk, data) == 0:
return Response(code=400, msg="菜单不存在")
return Response()

View File

@ -67,3 +67,8 @@ async def get_apis(pk: int):
AND srm.rid = (?) and m.status != 9""", AND srm.rid = (?) and m.status != 9""",
[pk], [pk],
) )
async def put_menu(pk: int, data):
"""更新菜单"""
return await MenuModel.filter(id=pk).update(**data.dict())

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,7 +3,7 @@ from typing import Any, Callable, get_type_hints
from fastapi import Depends, routing from fastapi import Depends, routing
from controller.common import about, login from controller.common import about, login
from controller.menu import menu_add, menu_arr, menu_del from controller.menu import menu_add, menu_arr, menu_del, menu_put
from controller.role import (assigned_menu, role_add, role_arr, role_del, from controller.role import (assigned_menu, role_add, role_arr, role_del,
role_has_menu, role_put, role_query) role_has_menu, role_put, role_query)
from controller.user import (user_add, user_arr, user_del, user_info, from controller.user import (user_add, user_arr, user_del, user_info,
@ -107,7 +107,9 @@ class Route(routing.APIRoute):
) )
has_perm = {"dependencies": [Depends(check_permissions)]} has_perm = {
# "dependencies": [Depends(check_permissions)]
}
routes = [ routes = [
Route.post("/login", endpoint=login, tags=["公共"], summary="登录"), Route.post("/login", endpoint=login, tags=["公共"], summary="登录"),
@ -157,6 +159,9 @@ routes = [
Route.delete( Route.delete(
"/menu/{pk}", endpoint=menu_del, tags=["菜单管理"], summary="菜单删除", **has_perm "/menu/{pk}", endpoint=menu_del, tags=["菜单管理"], summary="菜单删除", **has_perm
), ),
Route.put(
"/menu/{pk}", endpoint=menu_put, tags=["菜单管理"], summary="菜单更新", **has_perm
),
] ]
__all__ = [routes] __all__ = [routes]

View File

@ -43,7 +43,7 @@ params = [
"/menu", "/menu",
MenuIn( # id 1 MenuIn( # id 1
name="系统管理", name="系统管理",
meta={"icon": "Group"}, meta={"icon": "AppstoreOutlined"},
path="/system", path="/system",
type=0, type=0,
component=None, component=None,
@ -57,7 +57,7 @@ params = [
"/menu", "/menu",
MenuIn( # id 2 MenuIn( # id 2
name="系统设置", name="系统设置",
meta={"icon": "setting"}, meta={"icon": "SettingOutlined"},
path="/system", path="/system",
type=0, type=0,
component=None, component=None,
@ -72,7 +72,7 @@ params = [
"/menu", "/menu",
MenuIn( # id 3 MenuIn( # id 3
name="用户管理", name="用户管理",
meta={"icon": "User"}, meta={"icon": "TeamOutlined"},
path="/system/user", path="/system/user",
type=1, type=1,
component="/system/user.vue", component="/system/user.vue",
@ -86,7 +86,7 @@ params = [
"/menu", "/menu",
MenuIn( # id 4 MenuIn( # id 4
name="角色管理", name="角色管理",
meta={"icon": "Role"}, meta={"icon": "UserOutlined"},
path="/system/role", path="/system/role",
type=1, type=1,
component="/system/role.vue", component="/system/role.vue",
@ -100,7 +100,7 @@ params = [
"/menu", "/menu",
MenuIn( # id 5 MenuIn( # id 5
name="菜单管理", name="菜单管理",
meta={"icon": "Menu"}, meta={"icon": "MenuOutlined"},
path="/system/menu", path="/system/menu",
type=1, type=1,
component="/system/menu.vue", component="/system/menu.vue",
@ -114,7 +114,7 @@ params = [
"/menu", "/menu",
MenuIn( # id 6 MenuIn( # id 6
name="关于", name="关于",
meta={"icon": "Menu"}, meta={"icon": "DashboardOutlined"},
path="/setting/about", path="/setting/about",
type=1, type=1,
component="/setting/about.vue", component="/setting/about.vue",
@ -309,6 +309,20 @@ params = [
method="DELETE", method="DELETE",
).dict(), ).dict(),
), ),
(
"/menu",
MenuIn(
name="修改菜单",
meta={"icon": "Update"},
path=None,
type=2,
component=None,
pid=5,
identifier="menu:update",
api="/menu/{pk}",
method="PUT",
).dict(),
),
# 分配权限 # 分配权限
( (
"/role/assigned/menu", "/role/assigned/menu",

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,8 @@
"preview": "vite preview --port 4173" "preview": "vite preview --port 4173"
}, },
"dependencies": { "dependencies": {
"ant-design-vue": "^3.2.12",
"axios": "^0.27.2", "axios": "^0.27.2",
"element-plus": "^2.2.16",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.0.21", "pinia": "^2.0.21",
"pinia-plugin-persistedstate": "^2.2.0", "pinia-plugin-persistedstate": "^2.2.0",
@ -17,8 +17,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^3.0.3", "@vitejs/plugin-vue": "^3.0.3",
"unplugin-auto-import": "^0.11.2", "unplugin-vue-components": "^0.22.7",
"unplugin-vue-components": "^0.22.4",
"vite": "^3.0.9" "vite": "^3.0.9"
} }
} }

View File

@ -1,12 +1,19 @@
<script setup> <script setup>
import { RouterView } from 'vue-router' import { RouterView } from "vue-router";
import { Spin } from "ant-design-vue";
import { userStore } from "./stores/user";
</script> </script>
<template> <template>
<RouterView /> <Spin :spinning="userStore().isLoading" tip="数据请求中" size="large">
<RouterView />
</Spin>
</template> </template>
<style scoped> <style>
.ant-spin-nested-loading,
.ant-spin-container {
width: 100%;
height: 100%;
}
</style> </style>

View File

@ -1,6 +0,0 @@
body,html, #app{
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,4 @@
html,#app{
height: 100%;
width: 100%;
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

Before

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,44 @@
<script setup>
import { useRouter } from "vue-router";
import { userStore } from "@/stores/user";
import { loadIconCpn } from "@/utils/loadCpn";
const store = userStore();
</script>
<template>
<div class="sider-menu">
<div class="logo"></div>
<a-menu theme="dark" mode="inline">
<template v-for="menu in store.userMenus" :key="menu.id">
<!-- 0 目录 顶层菜单 -->
<template v-if="menu.type === 0">
<a-sub-menu :key="`${menu.id}`">
<template #icon>
<component :is="loadIconCpn(menu.meta.icon)"></component>
</template>
<template #title>{{ menu.name }}</template>
<!-- 1 组件 子菜单项 -->
<template v-for="sub in menu.children" :key="sub.id">
<a-menu-item>
<template #icon>
<component :is="loadIconCpn(sub.meta.icon)"></component>
</template>
<span>{{ sub.name }}</span>
</a-menu-item>
</template>
</a-sub-menu>
</template>
</template>
</a-menu>
</div>
</template>
<style scoped>
.logo {
height: 32px;
background: rgba(255, 255, 255, 0.3);
margin: 16px;
background-size: 100% 100%;
}
</style>

View File

@ -1,17 +1,17 @@
import { createApp } from 'vue' import { createApp } from "vue";
import App from './App.vue' import App from "./App.vue";
import router from './router' import router from "./router";
import store from './stores' import store from "./stores";
import 'normalize.css' import "normalize.css";
import '@/assets/base.css' import "@/assets/css/base.css";
import 'element-plus/theme-chalk/el-message.css'
import 'element-plus/theme-chalk/el-loading.css'
const app = createApp(App) import "ant-design-vue/dist/antd.css";
app.use(store) const app = createApp(App);
app.use(router)
app.mount('#app') app.use(store);
app.use(router);
app.mount("#app");

View File

@ -1,44 +1,43 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from "vue-router";
import { userStore } from '@/stores/user' import { message } from "ant-design-vue";
import { ElMessage } from 'element-plus' import { userStore } from "@/stores/user";
const routes = [ const routes = [
{ {
path: '/', path: "/",
redirect: '/main' redirect: "/main",
}, },
{ {
path: '/login', path: "/login",
meta: {title: '登录页'}, meta: { title: "登录页" },
component: () => import('@/views/login.vue') component: () => import("@/views/login.vue"),
}, },
{ {
path: '/main', path: "/main",
meta: {title: '主页'}, meta: { title: "主页" },
component: () => import('@/views/main.vue') component: () => import("@/views/main.vue"),
} },
];
]
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: routes routes: routes,
}) });
// 导航守卫 // 导航守卫
router.beforeEach((to) => { router.beforeEach((to) => {
// 修改页面标题 // 修改页面标题
if(to.meta.title) { if (to.meta.title) {
document.title = to.meta.title document.title = to.meta.title;
} }
if (to.path !== "/login") { if (to.path !== "/login") {
if (userStore().token){ if (userStore().token) {
return return;
} }
ElMessage.warning("请登录") message.warning("请登录");
return '/login' return "/login";
} }
}) });
export default router export default router;

View File

@ -1,37 +0,0 @@
import axios from "axios";
import { ElMessage, ElLoading } from 'element-plus'
import {userStore} from '@/stores/user'
let loading
export default (config) => {
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 10000,
})
instance.interceptors.request.use(config => {
loading = ElLoading.service({
lock: true,
text: '请求中...',
background: 'rabg(0,0,0,0.7)'
})
config.headers.Authorization = userStore().accessToken
return config
})
instance.interceptors.response.use(res => {
if (res.data.code !== 200 ){
ElMessage.error(res.data.msg)
}
loading.close()
return res.data
}, err => {
ElMessage.error(err)
loading.close()
return Promise.reject(err)
})
return instance(config)
}

View File

@ -1,23 +1,23 @@
import request from "./request"; import request from "@/utils/request";
export function login(data) { export function login(data) {
return request({ return request({
url: "/login", url: "/login",
method: 'post', method: "post",
data data,
}); });
} }
// 获取用户信息 // 获取用户信息
export function getUserInfo(uid){ export function getUserInfo(uid) {
return request({ return request({
url: `/user/${uid}` url: `/user/${uid}`,
}) });
} }
// 获取权限信息 // 获取权限信息
export function getMenus(rid){ export function getMenus(rid) {
return request({ return request({
url: `/role/${rid}/menu` url: `/role/${rid}/menu`,
}) });
} }

View File

@ -1,52 +1,64 @@
import { ref, computed } from 'vue' import { ref, computed } from "vue";
import { defineStore } from 'pinia' import { defineStore } from "pinia";
import { ElMessage } from 'element-plus' import { message } from "ant-design-vue";
import {getMenus, getUserInfo, login} from '@/service/user' import { getMenus, getUserInfo, login } from "@/service/user";
import router from '@/router' import router from "@/router";
export const userStore = defineStore('user', () => { export const userStore = defineStore(
const token = ref("") "user",
const userInfo = ref({}) () => {
const userMenus = ref([]) const token = ref("");
const userInfo = ref({});
const userMenus = ref([]);
// getter const isLoading = ref(false);
const accessToken = computed(() => 'Bearer ' + token.value)
// setup store 不提供$reset 需要自己重置 // getter
// https://github.com/vuejs/pinia/issues/1056 const accessToken = computed(() => "Bearer " + token.value);
const $reset = () => {
token.value = ""
userInfo.value = {}
userMenus.value = []
}
// 非setup语法时的actions // setup store 不提供$reset 需要自己重置
const loginAction = async (data) => { // https://github.com/vuejs/pinia/issues/1056
const $reset = () => {
token.value = "";
userInfo.value = {};
userMenus.value = [];
};
// 1. 登录 // 非setup语法时的actions
const res = await login(data) const loginAction = async (data) => {
token.value = res.data.token // 1. 登录
const res = await login(data);
token.value = res.data.token;
// 2. 获取用户信息 // 2. 获取用户信息
const info = await getUserInfo(res.data.id) const info = await getUserInfo(res.data.id);
userInfo.value = info.data userInfo.value = info.data;
// 3. 获取权限信息 // 3. 获取权限信息
const menus = await getMenus(info.data.roles[0].id) const menus = await getMenus(info.data.roles[0].id);
userMenus.value = menus.data userMenus.value = menus.data;
// 4. 跳转 // 4. 跳转
router.push("/main") router.push("/main");
// 弹框提示登录成功 // 弹框提示登录成功
ElMessage.success("登录成功.") message.success("登录成功.");
} };
return { token, accessToken, userInfo, userMenus, return {
$reset, loginAction } token,
}, { accessToken,
userInfo,
userMenus,
isLoading,
$reset,
loginAction,
};
},
{
persist: true, // 解决pinia刷新时数据丢失问题 persist: true, // 解决pinia刷新时数据丢失问题
}) }
);
// export const userStore = defineStore('user',{ // export const userStore = defineStore('user',{
// state: () => ({ // state: () => ({

View File

@ -0,0 +1,16 @@
// 动态加载组件
import { h } from "vue";
import * as icons from "@ant-design/icons-vue";
/**
* 动态加载antd icon
* @param {*} iconName
* @returns 组件对象
* jsx使用 h(loadIconCpn('UserField'))
* template: 使用 <component :is="loadIconCpn("UserField")">
*/
function loadIconCpn(iconName) {
return icons[iconName];
}
export { loadIconCpn };

View File

@ -0,0 +1,34 @@
import axios from "axios";
import { message } from "ant-design-vue";
import { userStore } from "@/stores/user";
export default (config) => {
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 10000,
});
instance.interceptors.request.use((config) => {
userStore().isLoading = !userStore().isLoading;
config.headers.Authorization = userStore().accessToken;
return config;
});
instance.interceptors.response.use(
(res) => {
userStore().isLoading = !userStore().isLoading;
if (res.data.code !== 200) {
message.error(res.data.msg);
}
return res.data;
},
(err) => {
userStore().isLoading = !userStore().isLoading;
message.error(err);
return Promise.reject(err);
}
);
return instance(config);
};

View File

@ -1,59 +1,82 @@
<script setup> <script setup>
import { User, Lock } from '@element-plus/icons-vue' import { UserOutlined, LockOutlined } from "@ant-design/icons-vue";
import {ref,reactive} from 'vue' import { ref, reactive, computed } from "vue";
import { userStore } from '@/stores/user'; import { userStore } from "@/stores/user";
const store = userStore() const store = userStore();
// //
const rules = { const rules = {
username: [ username: [
{required: true, message: '请输入用户名', trigger: 'blur'}, { required: true, message: "请输入用户名", trigger: "blur" },
{min:5, max:20, message: '5~20', trigger: 'blur'} { min: 5, max: 20, message: "5~20", trigger: "blur" },
], ],
password: [ password: [
{required: true, message: '请输入密码', trigger: 'blur'}, { required: true, message: "请输入密码", trigger: "blur" },
{min:6, max:12, message: '6~12', trigger: 'blur'} { min: 6, max: 12, message: "6~12", trigger: "blur" },
] ],
} };
// //
const formRef = ref() const formRef = ref();
const formData = reactive({ const formData = reactive({
username: 'admin', username: "admin",
password: '123456' password: "123456",
}) });
//
// const disabled = computed(() => {
const submitForm = (formEl) => { return !(formData.username && formData.password);
if (!formEl) return });
formEl.validate( valid => {
if (valid) {
//
store.loginAction(formData)
}
})
}
//
const submitForm = (formEl) => {
if (!formEl) return;
formEl.validate().then(
(res) => {
store.loginAction(formData);
},
(err) => err
);
};
</script> </script>
<template> <template>
<div class="login"> <div class="login">
<div class="continer"> <div class="continer">
<h1>Mini RBAC</h1> <h1>Mini RBAC</h1>
<el-form ref="formRef" :model="formData" :rules="rules" status-icon>
<el-form-item prop="username"> <a-form ref="formRef" :model="formData" :rules="rules">
<el-input placeholder="用户名" clearable :prefix-icon="User" <a-form-item has-feedback name="username">
v-model.trim="formData.username"/> <a-input
</el-form-item> v-model:value.trim="formData.username"
<el-form-item prop="password"> placeholder="Username"
<el-input placeholder="密码" show-password :prefix-icon="Lock" >
v-model.trim="formData.password"/> <template #prefix>
</el-form-item> <UserOutlined style="color: rgba(0, 0, 0, 0.25)" />
<el-form-item> </template>
<el-button type="primary" @click="submitForm(formRef)" >登录</el-button> </a-input>
</el-form-item> </a-form-item>
</el-form> <a-form-item has-feedback name="password">
<a-input-password
v-model:value.trim="formData.password"
placeholder="Password"
autocomplete="on"
>
<template #prefix>
<LockOutlined style="color: rgba(0, 0, 0, 0.25)" />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:disabled="disabled"
@click="submitForm(formRef)"
>登录</a-button
>
</a-form-item>
</a-form>
</div> </div>
</div> </div>
</template> </template>
@ -73,10 +96,10 @@
width: 300px; width: 300px;
height: 300px; height: 300px;
} }
.continer h1{ .continer h1 {
color: #fff; color: #fff;
} }
.continer .el-button { .continer .ant-btn {
width: 100%; width: 100%;
} }
</style> </style>

View File

@ -1,53 +1,72 @@
<script setup> <script setup>
import router from '@/router'; import { ref } from "vue";
import { userStore } from '@/stores/user'; import router from "@/router";
const store = userStore()
const logout = () => { import { userStore } from "@/stores/user";
store.$reset()
router.push('/login') import SiderMenu from "@/components/layout/sider-menu.vue";
} const store = userStore();
const collapsed = ref(false);
const logout = () => {
store.$reset();
router.push("/login");
};
</script> </script>
<template> <template>
<div class="main"> <div class="main">
<el-container> <a-layout>
<el-aside width="200px">Aside</el-aside> <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<el-container> <!-- 动态菜单 -->
<el-header>Header <el-button @click="logout"> <SiderMenu />
注销 </a-layout-sider>
</el-button></el-header> <a-layout>
<el-main>Main</el-main> <a-layout-header style="background: #fff; padding: 0">
<el-footer>Footer</el-footer> <!-- 页头 -->
</el-container> <a-button @click="logout"></a-button>
</el-container> </a-layout-header>
</div> <!-- 面包屑 -->
<a-layout-content
:style="{
margin: '24px 16px',
background: '#F0F2F5',
minHeight: '280px',
}"
>
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
</div>
</template> </template>
<style scoped> <style scoped>
.main { .main,
height: 100%; .ant-layout {
}
.layout-container-demo .el-header {
position: relative;
background-color: var(--el-color-primary-light-7);
color: var(--el-text-color-primary);
}
.layout-container-demo .el-aside {
color: var(--el-text-color-primary);
background: var(--el-color-primary-light-8);
}
.layout-container-demo .el-menu {
border-right: none;
}
.layout-container-demo .el-main {
padding: 0;
}
.layout-container-demo .toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%; height: 100%;
right: 20px; }
#components-layout-demo-custom-trigger .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
background: rgba(255, 255, 255, 0.3);
margin: 16px;
}
.site-layout .site-layout-background {
background: #fff;
} }
</style> </style>

View File

@ -1,34 +1,35 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from "node:url";
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import vue from '@vitejs/plugin-vue' import vue from "@vitejs/plugin-vue";
import AutoImport from 'unplugin-auto-import/vite' import Components from "unplugin-vue-components/vite";
import Components from 'unplugin-vue-components/vite' import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue(), AutoImport({ plugins: [
resolvers: [ElementPlusResolver()], vue(),
}), Components({ Components({
resolvers: [ElementPlusResolver()], resolvers: [AntDesignVueResolver()],
}),], }),
],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) "@": fileURLToPath(new URL("./src", import.meta.url)),
} },
}, },
server: { server: {
proxy: { // 代理 proxy: {
'/api': { // 代理
target: 'http://localhost:8000', "/api": {
target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, ""),
}, },
'/socket.io': { "/socket.io": {
target: 'ws://localhost:5000', target: "ws://localhost:5000",
ws: true ws: true,
} },
} },
} },
}) });