好久没更新文章了,确实有AI后好像大家都不太关注技术了。好像大家的关注点都是那个AI 模型又又又增强。
但是我建议看到这篇文章 的同学,还是要持续学习,你面试的时候不能告诉面试官我会用某个AI?如果AI模型花钱包月就能使用那么你的优势又是哪里。 流水不争先,争的是滔滔不绝 持续学习吧,至少现在出去面试还是问你的技术决定你的待遇呢,等真到某天我去面试,面试官查看我使用消耗的TOKEN,那我觉得就可以不用学了哈哈哈~
博主之前更新了一篇java的学习,是的java有所小成了,java spring boot基本上都可以写了,因为有一定的编程思想,也就只有语法不熟练了。
最近看了好多后台管理系统,有免费的RuoYI、纯前端的Naive UI Pro,收费的丰富了大家可以自行查找。
客观评价一下优秀开源作品RuoYi(仅仅个人观点):
- 优点:功能成熟,可靠、稳定。
- 缺点:vue2(现在大多都vue3了) 界面不是那么美观,后台代码应该是写的非常不错(我毕竟刚入门不好评价),但是不适合学习,如果你是专业后端我觉得上手难度还可以,如果偏前端的我觉得还是我的这个框架更合适。
Sanyuan VS RuoYi (客观评价)
-
不足:后端 因为我java刚入门,有编程思想,毕竟没那么多的经验,肯定好多地方没那么完善,但是博主再这里说明下,后续会继续学习多多学习。前端:我经验很足,一些细节也都考虑到了,也借鉴了一些别的系统,技术选型啥的肯定没问题。
-
优点:因为我的java刚入门,没有引入什么特殊的插件,所以想学习或者写项目的同学更好使用。前端:支持Vue3、国际化、界面相对美观些吧(这个每人审美都不一样)
访问地址
访问地址: sanyuan.website/ 用户名:admin 密码:123456
仓库地址: gitee.com/hejingyuan0...
- 角色管理
- 日志管理
- 菜单管理
- 用户管理
- 字典管理
- 暗色主题适配切换
- 现支持2种布局方式,未来会支持更多常用的布局已经在规划中~
- 支持适配主题色、色弱模式、灰色模式
- 支持4种路由过渡动画
- 支持国际化,内涵轻松适配各种语言的方案
- 支持权限控制
- 支持路由缓存
- 支持菜单外链、内嵌自定义
- 支持两种标签栏定制
功能演示
- 主题切换

水平布局、垂直布局

国际化

还有一些别的自定义,大家自己去系统中使用吧
国际化
这里使用的是vite-auto-i18n-plugin 传送门 可以直接抽代码,配置好了自动翻译

使用方式如下图,别忘记再main.js引入生成的lang资源哈

动态菜单
其实做动态菜单有两种:
一种是再router.js中定义好code,然后根据后端的返回的code,在router.beforeEach中去拦截,遇到每权限的就next(/403)页面。这种实现简单,挺好用的,我们有些项目也在使用这种。
第二种是使用router.addRoute动态的去插入到路由里面,比如RuoYI等后台管理系统都是用的这种方式,再页面上选图标、输入路由路径、对应的前端组件路径。 然后把信息返回给前端,前端去router.addRoute。优点是可以在界面上自动配置,配置完后也可以给权限分配去使用了。如下图:

再用vue3 router.addRoute的时候也遇到过几个问题,第一点就是怎么匹配前端的组件资源:
js
const getRouterLoadView = (menu) => {
//VUE3 要使用import.meta.glob 获取使用 不能使用() => import(`@/views/**/*.vue`)
const modules = import.meta.glob('@/views/**/*.vue')
const filePath = `/src/views${menu.component}`
const componentLoader = modules[filePath]
return componentLoader
}
还遇到一个问题就是因为我router在没有获取到权限数据 的时候是就一些默认的(403、404、500、登录)。再访问https://sanyuan.website/home路径的时候,我在用router.beforeEach(async (to, _, next) => {}) 这个时候的router.to的信息已经是404的路由信息。 那么我怎么知道是Home 路由呢,通过URL的路径定位如下:
js
function getRoutePath(url = window.location.href) {
const urlObj = new URL(url)
return urlObj.pathname || '/'
}
完整的router如下:
js
router.beforeEach(async (to, _, next) => {
NProgress.start()
const { closeGlobalLoading } = useGlobalLoading()
if (to.path === '/login') {
next()
closeGlobalLoading()
return
}
// 没有token 去登录页面
if (!localStorage.getItem('token')) {
next('/login')
return
}
const userStore = useUserStore()
if (!userStore.userInfo.id) {
// 判断是不是首次登录,首次登录获取用户信息
try {
// 这里获取用户信息、用户权限、用户动态菜单
const { initPreposition } = usePreposition()
await initPreposition()
// ps: 因为to的信息这个时候已经变成404的路由信息了,用URL上的路由做为跳转
closeGlobalLoading()
next(getRoutePath(), { replace: true })
return
} catch (error) {
console.log(error, '获取用户信息失败')
localStorage.removeItem('token')
next('/login')
return
}
} else {
next()
}
})
路由缓存
使用keep-alive做缓存include动态做缓存,如果我不是动态的就比较好实现。例如我想要在User、Menu做缓存。我只需要在对应的页面文件定义 defineOptions({name: 'UserPage'})、defineOptions({name: 'MenuPage'}) 然后如下这么使用就生效:
js
<router-view v-slot="{ Component }">
<transition :name="appStore.appConfig.transitionName" mode="out-in">
<keep-alive :include="['UserPage', 'MenuPage']">
<component :is="Component" :key="currentComponentKey" />
</keep-alive>
</transition>
</router-view>
但是做成动态的就又有点不一样了如下图让用户自己在配置页面可选:

并且页面还有个刷新按钮

原本做刷新很简单只需要给component绑定一个动态的KEY,再点击的时候刷新这个动态key就行了。但是需要做keep-alive那这个key 就不能是纯动态了,比如缓存了2个页面,你刷新之后把key变了,做的缓存也失效了。
完整的实现如下: 1、路由中在meta保留缓存标识,因为我的是页面配置的,是在服务端直接处理好返回的noCache如下
json
{
"name": "home",
"path": "/home",
"hidden": false,
"component": "/home/index.vue",
"alwaysShow": null,
"meta": {
"title": "首页",
"titleEn": "Home",
"frameType": null,
"icon": "AppstoreOutlined",
"noCache": true,
"isFrame": false
}
}
2、使用router.beforeResolve的to里面收集到需要缓存的PageName如下:
js
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { defineStore } from 'pinia'
import { uniq } from 'lodash'
function getRouteComponentName(route) {
const currentRoute = route.matched[route.matched.length - 1]
const currentRouteComponent = currentRoute?.components?.default
return currentRouteComponent?.name
}
export const useRouteCacheStore = defineStore('route-cache', () => {
const router = useRouter()
const cachedViews = ref([])
const setRouteCachedViews = (route) => {
if (!route.meta.noCache) {
// 需要缓存
const componentName = getRouteComponentName(route)
cachedViews.value = uniq([...cachedViews.value, componentName])
}
}
// ps: 【缺陷】 如果首次刷新刚好在缓存的页面, 不会被收集到
router.beforeResolve((to) => {
setRouteCachedViews(to)
})
return { cachedViews }
})
3、解决刷新影响到keep-alive的问题,做缓存如下:
vue
<template>
<div class="app-content">
<router-view v-slot="{ Component }">
<transition :name="appStore.appConfig.transitionName" mode="out-in">
// cachedViews 就是缓存收集到的需要 keep-alive的page
<keep-alive :include="routeCacheStore.cachedViews">
<component :is="Component" :key="currentComponentKey" />
</keep-alive>
</transition>
</router-view>
<slot name="footer"></slot>
</div>
</template>
<script setup>
import { RouterView, useRouter } from 'vue-router'
import { computed, defineOptions, watch, ref } from 'vue'
import { useLayoutStore } from '@/stores/layout'
import { useAppStore } from '@/stores/app.js'
import { useRefreshStore } from '@/stores/refresh'
import { useRouteCacheStore } from '@/stores/route-cache.js'
import { useThemeToken } from '../hooks/use-theme-token.js'
defineOptions({name: 'AppContent',})
const router = useRouter()
const routeCacheStore = useRouteCacheStore()
const { colorBgLayout } = useThemeToken()
const layoutStore = useLayoutStore()
const appStore = useAppStore()
const refreshStore = useRefreshStore()
// 缓存每次的key, 在刷新的时候只更新当前的key
const cachedMap = ref({})
const currentComponentKey = computed(() => {
const name = router.currentRoute.value.name
if (cachedMap.value[name]) {
return cachedMap.value[name]
}
cachedMap.value[name] = name
return name
})
watch(
() => refreshStore.refreshKey,
() => {
const name = router.currentRoute.value.name
cachedMap.value[name] = `${name}_${refreshStore.refreshKey}`
},
)
</script>
外链内嵌
可以看到我菜单配置页面有一个外链内嵌也是动态可配置的,这个要怎么实现呢?
1、实现一个iframe内嵌的组件:
vue
<template>
<a-spin tip="加载中..." :spinning="loading">
<ProLayout>
<ProLayoutMain>
<iframe class="size-full border-none overflow-hidden" :src="url" @load="loading = false" />
</ProLayoutMain>
</ProLayout>
</a-spin>
</template>
<script setup>
import { ProLayout, ProLayoutMain } from '@/components/pro-layout/index'
import { ref } from 'vue'
defineProps({
url: {
type: String,
required: true,
},
})
const loading = ref(true)
</script>
2、在处理菜单路由的时候把components改成这个使用vue render包裹的,然后把url传给ifrmae组件
js
import { ref, h } from 'vue'
const renderIframe = (url) => {
return h(Iframe, { url })
}
function generateDynamicRoutes(menus) {
if (!menus || !menus.length) return []
return menus.map((menu) => {
// 【iframe 外链】 内嵌模式的数据 isFrame = true, frameType = 2
/**
* 【iframe 外链】
* 处理path 默认生成使用/iframe/xxxx 模式
*/
const isFrameAdnInline = menu?.meta?.frameType === '2' && menu?.meta?.isFrame
const route = {
...menu,
path: isFrameAdnInline ? `/iframe/${menu.meta.titleEn}` : menu.path,
name: menu.name || menu.path,
meta: {
...menu.meta,
title: isEN ? menu.meta?.titleEn : menu.meta?.title,
icon: menu.meta?.icon ? h(ProIcon, { name: menu.meta.icon }) : '',
isFrame: isFrameAdnInline ? false : menu.meta.isFrame,
},
component: isFrameAdnInline ? renderIframe(menu.path) : getRouterLoadView(menu),
}
if (menu.children && menu.children.length > 0) {
route.children = generateDynamicRoutes(menu.children)
}
return route
})
}
总结
做这个后台管理还是有不少知识点的,像路由过渡动画、缓存、动态菜单路由等等,大家具体可以查询源码。
问答
Q: 现在是vue3 + js写的,为啥不用ts?
A: 因为现在好多单位还是使用的js,相比较来说还是js更方便,编码更快, 只想做简单好用的框架,不增加负担
Q: 现在用的Ant Design Vue UI,后续会不会出Element Plus、Naiveui?
A: 这个要看系统使用的情况,如果比较受大家欢迎,后续会考虑出别的UI的后台管理系统