This commit is contained in:
ktianc 2022-09-22 17:33:49 +08:00
parent 782ba23435
commit c07ccdd3ef
15 changed files with 141 additions and 241 deletions

View File

@ -1,12 +1,8 @@
import request from '@/config/axios'
import type { UserLoginType, UserType } from './types'
interface RoleParams {
roleName: string
}
export const loginApi = (data: UserLoginType): Promise<IResponse> => {
return request.post({ url: '/api/auth/login', data })
return request.post({ url: '/auth/login/', data })
}
export const loginOutApi = (): Promise<IResponse> => {
@ -20,12 +16,6 @@ export const getUserListApi = ({ params }: AxiosConfig) => {
}>({ url: '/user/list', params })
}
export const getAdminRoleApi = (
params: RoleParams
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
return request.get({ url: '/role/list', params })
}
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/role/list', params })
export const getRoleMenusApi = (): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
return request.get({ url: '/auth/getMenuList/' })
}

View File

@ -5,8 +5,8 @@ export type UserLoginType = {
export type UserType = {
telephone: string
password: string
role: string
roleId: string
permissions: string | string[]
nickname: string
id: number
avatar: string
name: string
}

View File

@ -48,7 +48,7 @@ export interface AppState {
}
export const appModules: AppState = {
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
userInfo: 'UserInfo', // 登录信息保存的存储字段名称-建议每个项目换一个字段,避免与其他项目冲突,方便后面直接使用这个名称来获取对应的登录用户信息
sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题

View File

@ -14,22 +14,22 @@ const config: {
*/
base_url: {
// 开发环境接口前缀
base: '',
base: '/api',
// 打包开发环境接口前缀
dev: '',
dev: '/api',
// 打包生产环境接口前缀
pro: '',
pro: '/api',
// 打包测试环境接口前缀
test: ''
test: '/api'
},
/**
*
*/
result_code: '200',
result_code: 200,
/**
*

View File

@ -1,4 +1,5 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import qs from 'qs'
@ -10,18 +11,24 @@ const { result_code, base_url } = config
export const PATH_URL = base_url[import.meta.env.VITE_API_BASEPATH]
const authStore = useAuthStoreWithOut()
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: PATH_URL, // api 的 base_url
timeout: config.request_timeout // 请求超时时间
timeout: config.request_timeout, // 请求超时时间
headers: {} // 请求头信息
})
// request拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (authStore.token !== '') {
config.headers['Authorization'] = authStore.token // 让每个请求携带自定义token 请根据实际情况自行修改
}
if (
config.method === 'post' &&
(config.headers as any)['Content-Type'] === 'application/x-www-form-urlencoded'
config.headers['Content-Type'] === 'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(config.data)
}

View File

@ -6,15 +6,13 @@ import { loginApi } from '@/api/login'
export interface AuthState {
token: string
is_reset_password: boolean
user_id: number
}
export const useAuthStore = defineStore({
id: 'auth',
state: (): AuthState => ({
token: '',
is_reset_password: false,
user_id: 0
is_reset_password: false
}),
persist: {
// 开启持久化存储
@ -24,9 +22,6 @@ export const useAuthStore = defineStore({
getToken(): string {
return this.token
},
getUserId(): number {
return this.user_id
},
getIsResetPassword(): boolean {
return this.is_reset_password
}
@ -35,13 +30,9 @@ export const useAuthStore = defineStore({
async login(formData: UserLoginType) {
const res = await loginApi(formData)
if (res) {
console.log('登录成功', res)
} else {
console.log('登录失败', res)
this.token = `${res.data.token_type} ${res.data.access_token}`
this.is_reset_password = res.data.is_reset_password
}
// this.token = token
// this.is_reset_password = is_reset_password
// this.user_id = user_id
return res
}
}

View File

@ -38,22 +38,10 @@ export const usePermissionStore = defineStore({
}
},
actions: {
generateRoutes(
type: 'admin' | 'test' | 'none',
routers?: AppCustomRouteRecordRaw[] | string[]
): Promise<unknown> {
generateRoutes(routers?: AppCustomRouteRecordRaw[]): Promise<unknown> {
return new Promise<void>((resolve) => {
let routerMap: AppRouteRecordRaw[] = []
if (type === 'admin') {
// 模拟后端过滤菜单
routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[])
} else if (type === 'test') {
// 模拟前端过滤菜单
routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap), routers as string[])
} else {
// 直接读取静态路由表
routerMap = cloneDeep(asyncRouterMap)
}
routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[])
// 动态路由404一定要放到最后面
this.addRouters = routerMap.concat([
{

View File

@ -1,127 +0,0 @@
<script setup lang="ts">
import PanelGroup from './components/PanelGroup.vue'
import { ElRow, ElCol, ElCard, ElSkeleton } from 'element-plus'
import { Echart } from '@/components/Echart'
import { pieOptions, barOptions, lineOptions } from './echarts-data'
import { ref, reactive } from 'vue'
import {
getUserAccessSourceApi,
getWeeklyUserActivityApi,
getMonthlySalesApi
} from '@/api/dashboard/analysis'
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const loading = ref(true)
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
//
const getUserAccessSource = async () => {
const res = await getUserAccessSourceApi().catch(() => {})
if (res) {
set(
pieOptionsData,
'legend.data',
res.data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = res.data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
//
const getWeeklyUserActivity = async () => {
const res = await getWeeklyUserActivityApi().catch(() => {})
if (res) {
set(
barOptionsData,
'xAxis.data',
res.data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: res.data.map((v) => v.value),
type: 'bar'
}
])
}
}
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
//
const getMonthlySales = async () => {
const res = await getMonthlySalesApi().catch(() => {})
if (res) {
set(
lineOptionsData,
'xAxis.data',
res.data.map((v) => t(v.name))
)
set(lineOptionsData, 'series', [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: res.data.map((v) => v.estimate),
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: res.data.map((v) => v.actual),
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
])
}
}
const getAllApi = async () => {
await Promise.all([getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
loading.value = false
}
getAllApi()
</script>
<template>
<PanelGroup />
<ElRow :gutter="20" justify="space-between">
<ElCol :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="300" />
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="300" />
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :span="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="4">
<Echart :options="lineOptionsData" :height="350" />
</ElSkeleton>
</ElCard>
</ElCol>
</ElRow>
</template>

View File

@ -4,14 +4,14 @@ import { Form } from '@/components/Form'
import { useI18n } from '@/hooks/web/useI18n'
import { ElButton, ElCheckbox, ElLink } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { getTestRoleApi, getAdminRoleApi } from '@/api/login'
import { getRoleMenusApi } from '@/api/login'
import { useCache } from '@/hooks/web/useCache'
import { useAppStore } from '@/store/modules/app'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { UserType, UserLoginType } from '@/api/login/types'
import { UserLoginType } from '@/api/login/types'
import { useValidator } from '@/hooks/web/useValidator'
const { required } = useValidator()
@ -57,7 +57,7 @@ const schema = reactive<FormSchema[]>([
{
field: 'password',
label: t('login.password'),
value: 'admin',
value: '430559',
component: 'InputPassword',
colProps: {
span: 24
@ -132,18 +132,10 @@ const signIn = async () => {
const res = await authStore.login(formData)
if (res) {
wsCache.set(appStore.getUserInfo, res.data)
//
wsCache.set(appStore.getUserInfo, res.data.user)
// 使
if (appStore.getDynamicRouter) {
getRole()
} else {
await permissionStore.generateRoutes('none').catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
getMenu()
}
} finally {
loading.value = false
@ -152,26 +144,15 @@ const signIn = async () => {
})
}
//
const getRole = async () => {
const { getFormData } = methods
const formData = await getFormData<UserType>()
const params = {
roleName: formData.telephone
}
// admin -
// test -
const res =
formData.telephone === 'admin' ? await getAdminRoleApi(params) : await getTestRoleApi(params)
//
const getMenu = async () => {
const res = await getRoleMenusApi()
console.log('菜单信息', res)
if (res) {
const { wsCache } = useCache()
const routers = res.data || []
wsCache.set('roleRouters', routers)
formData.telephone === 'admin'
? await permissionStore.generateRoutes('admin', routers).catch(() => {})
: await permissionStore.generateRoutes('test', routers).catch(() => {})
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
})

View File

@ -0,0 +1,46 @@
"""update
Revision ID: 2d8939b3a228
Revises: ecb50546debd
Create Date: 2022-09-22 16:34:00.663906
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2d8939b3a228'
down_revision = 'ecb50546debd'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('vadmin_auth_menu', sa.Column('name', sa.String(length=50), nullable=False, comment='名称'))
op.add_column('vadmin_auth_menu', sa.Column('noCache', sa.Boolean(), nullable=True, comment='如果设置为true则不会被 <keep-alive> 缓存(默认 false)'))
op.add_column('vadmin_auth_menu', sa.Column('breadcrumb', sa.Boolean(), nullable=True, comment='如果设置为false则不会在breadcrumb面包屑中显示(默认 true)'))
op.add_column('vadmin_auth_menu', sa.Column('affix', sa.Boolean(), nullable=True, comment='如果设置为true则会一直固定在tag项中(默认 false)'))
op.add_column('vadmin_auth_menu', sa.Column('noTagsView', sa.Boolean(), nullable=True, comment='如果设置为true则不会出现在tag中(默认 false)'))
op.add_column('vadmin_auth_menu', sa.Column('canTo', sa.Boolean(), nullable=True, comment='设置为true即使hidden为true也依然可以进行路由跳转(默认 false)'))
op.drop_index('ix_vadmin_auth_menu_title', table_name='vadmin_auth_menu')
op.create_index(op.f('ix_vadmin_auth_menu_name'), 'vadmin_auth_menu', ['name'], unique=False)
op.drop_column('vadmin_auth_menu', 'title_zh')
op.drop_column('vadmin_auth_menu', 'title')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('vadmin_auth_menu', sa.Column('title', mysql.VARCHAR(length=50), nullable=False, comment='名称'))
op.add_column('vadmin_auth_menu', sa.Column('title_zh', mysql.VARCHAR(length=50), nullable=True, comment='中文名称'))
op.drop_index(op.f('ix_vadmin_auth_menu_name'), table_name='vadmin_auth_menu')
op.create_index('ix_vadmin_auth_menu_title', 'vadmin_auth_menu', ['title'], unique=False)
op.drop_column('vadmin_auth_menu', 'canTo')
op.drop_column('vadmin_auth_menu', 'noTagsView')
op.drop_column('vadmin_auth_menu', 'affix')
op.drop_column('vadmin_auth_menu', 'breadcrumb')
op.drop_column('vadmin_auth_menu', 'noCache')
op.drop_column('vadmin_auth_menu', 'name')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""update
Revision ID: d37b76a689c1
Revises: 2d8939b3a228
Create Date: 2022-09-22 16:35:48.607099
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'd37b76a689c1'
down_revision = '2d8939b3a228'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('vadmin_auth_menu', sa.Column('title', sa.String(length=50), nullable=False, comment='名称'))
op.drop_index('ix_vadmin_auth_menu_name', table_name='vadmin_auth_menu')
op.create_index(op.f('ix_vadmin_auth_menu_title'), 'vadmin_auth_menu', ['title'], unique=False)
op.drop_column('vadmin_auth_menu', 'name')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('vadmin_auth_menu', sa.Column('name', mysql.VARCHAR(length=50), nullable=False, comment='名称'))
op.drop_index(op.f('ix_vadmin_auth_menu_title'), table_name='vadmin_auth_menu')
op.create_index('ix_vadmin_auth_menu_name', 'vadmin_auth_menu', ['name'], unique=False)
op.drop_column('vadmin_auth_menu', 'title')
# ### end Alembic commands ###

View File

@ -71,18 +71,13 @@ class MenuDal(DalBase):
async def get_routers(self, user: models.VadminUser):
"""
获取路由表
export interface Menu {
name: string; // 菜单名
icon?: string; // 菜单图标,如果没有则会尝试使用route.meta.icon
path: string; // 菜单路径
disabled?: boolean; // 是否禁用
children?: Menu[]; // 子菜单
tag: { // 菜单标签设置
dot: boolean; // 为true则显示小圆点
content: string'; // 内容
type: 'error' | 'primary' | 'warn' | 'success'; // 类型
};
hideMenu?: boolean; // 是否隐藏菜单
declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string
meta: RouteMeta
component: string
path: string
redirect: string
children?: AppCustomRouteRecordRaw[]
}
"""
if any([i.is_admin for i in user.roles]):
@ -94,6 +89,7 @@ class MenuDal(DalBase):
for role in user.roles:
role_obj = await RoleDal(self.db).get_data(role.id, options=[models.VadminRole.menus])
for menu in role_obj.menus:
# 该路由没有被禁用,并且菜单不是按钮
if not menu.disabled and menu.menu_type != "2":
menus.add(menu)
roots = filter(lambda i: not i.parent_id, menus)
@ -116,7 +112,7 @@ class MenuDal(DalBase):
for root in nodes:
router = schemas.RouterOut.from_orm(root)
router.name = router.path.split("/")[-1].capitalize()
router.meta = schemas.Meta(title=root.title, icon=root.icon)
router.meta = schemas.Meta(title=root.title, icon=root.icon, hidden=root.hidden)
if root.menu_type == "0":
sons = filter(lambda i: i.parent_id == root.id, menus)
router.children = self.generate_router_tree(menus, sons)

View File

@ -23,7 +23,6 @@ class VadminMenu(BaseModel):
# button = "2"
title = Column(String(50), index=True, nullable=False, comment="名称")
title_zh = Column(String(50), comment="中文名称") # 选择框时使用
icon = Column(String(50), comment="菜单图标")
redirect = Column(String(100), comment="重定向地址")
component = Column(String(50), comment="前端组件地址")
@ -34,5 +33,10 @@ class VadminMenu(BaseModel):
menu_type = Column(String(8), comment="菜单类型")
parent_id = Column(ForeignKey("vadmin_auth_menu.id", ondelete='CASCADE'), comment="父菜单")
perms = Column(String(50), comment="权限标识", unique=False, nullable=True, index=True)
noCache = Column(Boolean, comment="如果设置为true则不会被 <keep-alive> 缓存(默认 false)", default=False)
breadcrumb = Column(Boolean, comment="如果设置为false则不会在breadcrumb面包屑中显示(默认 true)", default=True)
affix = Column(Boolean, comment="如果设置为true则会一直固定在tag项中(默认 false)", default=False)
noTagsView = Column(Boolean, comment="如果设置为true则不会出现在tag中(默认 false)", default=False)
canTo = Column(Boolean, comment="设置为true即使hidden为true也依然可以进行路由跳转(默认 false)", default=False)
roles = relationship("VadminRole", back_populates='menus', secondary=vadmin_role_menus)

View File

@ -40,6 +40,12 @@ class MenuSimpleOut(Menu):
class Meta(BaseModel):
title: str
icon: Optional[str] = None
hidden: bool = False
noCache: Optional[bool] = True
breadcrumb: Optional[bool] = True
affix: Optional[bool] = False
noTagsView: Optional[bool] = False
canTo: Optional[bool] = False
# 路由展示
@ -48,10 +54,7 @@ class RouterOut(BaseModel):
component: str
path: str
redirect: Optional[str] = None
perms: Optional[str] = None
meta: Optional[Meta] = None
disabled: bool = False
hidden: bool = Field(False, alias='hideMenu')
children: List['RouterOut'] = []
class Config:

View File

@ -25,8 +25,7 @@ from utils.response import SuccessResponse, ErrorResponse
from application import settings
from .auth_util import authenticate_user, create_access_token
from apps.vadmin.record.models import VadminLoginRecord
from apps.vadmin.auth.crud import RoleDal, MenuDal
from apps.vadmin.auth.models import VadminRole
from apps.vadmin.auth.crud import MenuDal
from .current import AdminAuth, full_admin
app = APIRouter()
@ -53,7 +52,8 @@ async def login_for_access_token(request: Request, data: dict = Depends(authenti
"telephone": user.telephone,
"name": user.name,
"nickname": user.nickname,
"avatar": user.avatar
"avatar": user.avatar,
"roles": [{"name": i.name, "value": i.role_key} for i in user.roles]
}
}
await VadminLoginRecord.create_login_record(telephone=user.telephone, status=data.get("status"), request=request,
@ -61,19 +61,6 @@ async def login_for_access_token(request: Request, data: dict = Depends(authenti
return SuccessResponse(result)
@app.get("/getUserInfo/", summary="获取当前登录用户基本信息")
async def get_user_info(auth: AdminAuth = Depends(full_admin)):
result = {
"id": auth.admin.id,
"telephone": auth.admin.telephone,
"name": auth.admin.name,
"nickname": auth.admin.nickname,
"avatar": auth.admin.avatar,
"roles": [{"name": i.name, "value": i.role_key} for i in auth.admin.roles]
}
return SuccessResponse(result)
@app.get("/getMenuList/", summary="获取当前用户菜单树")
async def get_menu_list(auth: AdminAuth = Depends(full_admin)):
datas = await MenuDal(auth.db).get_routers(auth.admin)