朋友找我帮助
对,仍是那位朋友,一位接近40的澳洲老哥,找我帮助搞一个办理后台,他刚找到编程的作业,作业内容会用到办理后台,可是他找了一圈开源项目,直接被搞的晕头转向,完全不知怎样下手,所以找我帮助。
经过了解,有几点是最困扰他的:
- 项目太大,东西太多,不知啥有用啥没用,看着模糊
- 想完成一个功用,但可能项目中现已完成了却不知,而存在重复做轮子的风险
- 集成度太高,关于他的黑魔法太多,很多未知技能没解锁,看文档一脸懵。
- 配套的后端项目往往没有,有的又太难,比方ruoyi整体很强,可是后端和前端联系太亲近,光看前端看不懂。
而他的初衷又是那么的简略合理,他就想有一个自己玩理解的办理后台项目,这样他便有把控感,能够更有底气的应对以后的作业。
所以,我便开端着手为他从头搭办理后台,尽量朴实无华的让他无压上手。
我让朋友列个愿望清单
我问了一下他希望都用什么技能,列出来,我好整在一起,所以咱们针对几个问题进行了商议。
- React和Vue选一个?
- Vue3,传闻赢麻了
- 那组件库用啥element,antd?
- Arco,美观,并且仍是字节的,有潜力。
- webpack仍是vite?
- Vite,传闻秒开,开发体验好
- 状况办理用Vuex仍是Pinia?
- Pinia,用新的
- 后端怎样安排?
- Koa2起手项目学习,最终一点一点变成Java
那么技能方案就定下来了:
Vue3+Vite+Pinia+koa2&Java搞一全栈项目。
我精心挑选出几个重要的功用点,先做出来
- 权限要管
- 这个对应了人物办理,人物用来装备权限
- 登录要做
- 这个对应了用户办理,再把人物配给用户
- 表单要封
- 办理后台中,表单用的最为频繁 ,需要进行封装,节省力气,防止重复。
- 联系要理
- 路由&菜单&操作严密关联,并经过权限驱动,逻辑理清
那么初步要做的功用点就定下来了:
- 人物办理
- 用户办理
- 封装表单
- 权限驱动
开端着手
光配路由就够了,直接生成菜单数据
路由数据先装备好
export const routesData = [
{
name: index.name,
meta: {
title: "首页",
icon: "system",
noCache: false,
link: null,
},
},
{
name: sys.name,
meta: {
title: "系统办理",
icon: "system",
noCache: false,
link: null,
},
children: [
{
name: user.name,
meta: {
title: "办理",
icon: "user",
noCache: false,
link: null,
},
},
{
name: role.name,
meta: {
title: "人物办理",
icon: "role",
noCache: false,
link: null,
},
},
],
},
]
发现没,装备数据中没有path,也便是匹配路由的地址,由于树状结构都定下来了,那么path就经过算法生成就好了啊,省事儿。
写个算法,补全路由装备中的path
const processRoute = (
children: RouteRecordRaw[],
routesData: RouteItemDataT[],
prefix: string
) => {
routesData.forEach((routeItem, index) => {
const { name } = routeItem;
if (persmissions.includes(name)) {
let routeData = routesConfig[name] as RouteRecordRaw;
routeData.name = name;
// 沿途记载,然后拼接成path
routeData.path = prefix + "/" + name;
children!.push(routeData);
if (routeItem.children!?.length > 0) {
routeData.children = [];
processRoute(routeData.children, routeItem.children!, routeData.path);
}
}
});
};
递归一下,沿途记载,然后拼接成path,搞定~~~,省的再去保护了,直接主动生成。
菜单直接依据路由数据生成就好了
路由数据是树状的,那么就把菜单组件写一个递归组件就好了
// 引入自己,然后在模版里递归调用
import MenuItem from './index.vue'
...
<template>
<template v-if="hasOneShowingChild(itemData.children, itemData)">
<a-menu-item :key="itemData.name">{{ itemData.meta!.title }}</a-menu-item>
</template>
<a-sub-menu v-else :key="itemData.name">
<template #title>
<IconCalendar></IconCalendar> {{ itemData.meta!.title }}
</template>
<MenuItem v-for="child in itemData.children" :itemData="child" :key="child.name">
</MenuItem>
</a-sub-menu>
</template>
递归组件,便是自己递归调用自己,这样给一个树状数据,直接就能递归生成出菜单
标签页安排上,并且是那种有回忆的
标签页经过路由切换进行办理,同时,确保能够正确的关闭和回忆操作 回忆操作,便是实用的vue的keep-alive。
可是话说,React怎样keep-alive你知道么? 嘿嘿,我也完成了,完成的原理是经过useOutlet和结合封装一个keep-alive组件就行了。
import React, { useRef, useEffect, useReducer, useMemo, memo } from 'react'
import { useLocation, useOutlet } from 'react-router-dom'
const KeepAlive = (props: any) => {
const outlet = useOutlet()
const { include, keys, children } = props
const { pathname } = useLocation()
const componentList = useRef(new Map())
const forceUpdate = useReducer((bool: any) => !bool, true)[1] // 强制渲染
const cacheKey = useMemo(
() => pathname + '__' + keys[pathname],
[pathname, keys]
) // eslint-disable-line
const activeKey = useRef<string>('')
useEffect(() => {
componentList.current.forEach(function (value, key) {
const _key = key.split('__')[0]
if (!include.includes(_key) || _key === pathname) {
this.delete(key)
}
}, componentList.current)
activeKey.current = cacheKey
if (!componentList.current.has(activeKey.current)) {
componentList.current.set(activeKey.current, outlet)
}
forceUpdate()
}, [cacheKey, include]) // eslint-disable-line
return (
<div>
{Array.from(componentList.current).map(([key, component]) => (
<div key={key}>
{key === activeKey.current ? (
<div>{component}</div>
) : (
<div style={{ display: 'none' }}>{component}</div>
)}
</div>
))}
</div>
)
}
export default memo(KeepAlive)
代码贴出来了,有爱好的同学能够自行实验,这算是一个不错的技巧,共享给我们。
表单搞起来
表单的场景非常的多,并且,一写便是多个,但不同的表单项大体模式也差不多,所以,假如能够仅仅经过装备数据就显现表单,岂不是很不错,又简略,又灵敏,便利扩展。
fromData: [
{
id: "测验Input表单",
title: "测验Input",
type: FORM_INPUT,
span: 12,
config: {}
},
{
id: "测验Tree表单",
title: "测验Tree",
type: FORM_TREE,
span: 24,
config: {
treeOptions: {
checkStrictly: true,
defaultCheckedKeys: permissions.value!
},
initValue: allPermissionsData.value,
}
},
{
id: "测验Select表单",
title: "测验Select",
type: FORM_SELECT,
span: 12,
config: {
options: [
{ name: "test1", value: "test1" },
{ name: "test2", value: "test2" },
]
}
}
]
然后驱动的作用
能完成的基础,便是我封装好了FormItem
<template>
<a-form-item :rules="config?.rules" label-col-flex="70px" :field=id :label="title">
<SelectItem :formValues="formValues" :form-data="formData" v-if="type == FORM_SELECT"></SelectItem>
<TreeItem :formValues="formValues" :form-data="formData" v-else-if="type == FORM_TREE" />
<a-input :placeholder="config?.placeholder" v-else v-model="formValues[id]" />
</a-form-item>
</template>
经过传入的参数,判断渲染什么表单项目,经过ts类型束缚一下,就很nice了。
登录完成了
首要开发一下登录页
登录页便是简略的供给账户密码登录就好,不过也不能太单调,加点花吧~~~
作用是我从开源社区中学着搞的,我感觉作用仍是不错的,代码贴出,直接用。
<template>
<div class="content">
<pre ref="container" class="container" id="container"></pre>
<pre ref="container2" ></pre>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, toRefs } from 'vue';
const props = defineProps<{ texts: string[] }>()
const { texts } = toRefs(props)
let container = ref()
let container2 = ref()
let defaultRun: boolean = true;
let infinite: boolean = true;
let frameTime: number = 75;
let endWaitStep = 3
let prefixString = "";
let runTexts = [""];
let colorTextLength = 5;
let step = 1;
let colors = [
"rgb(110,64,170)",
"rgb(150,61,179)",
"rgb(191,60,175)",
"rgb(228,65,157)",
"rgb(254,75,131)",
"rgb(255,94,99)",
"rgb(255,120,71)",
"rgb(251,150,51)",
"rgb(226,183,47)",
"rgb(198,214,60)",
"rgb(175,240,91)",
"rgb(127,246,88)",
"rgb(82,246,103)",
"rgb(48,239,130)",
"rgb(29,223,163)",
"rgb(26,199,194)",
"rgb(35,171,216)",
"rgb(54,140,225)",
"rgb(76,110,219)",
"rgb(96,84,200)",
];
let inst = {
text: "",
prefix: -(prefixString.length + colorTextLength),
skillI: 0,
skillP: 0,
step: step,
direction: "forward",
delay: endWaitStep,
};
function randomNum(minNum: number, maxNum: number): number {
switch (arguments.length) {
case 1:
return parseInt((Math.random() * minNum + 1).toString(), 10);
case 2:
return parseInt((Math.random() * (maxNum - minNum + 1) + minNum).toString(), 10);
default:
return 0;
}
}
let randomTime: number = randomNum(15, 150);
let destroyed: boolean = false;
let continue2: boolean = false;
let infinite0: boolean = true;
onMounted(() => {
runTexts = texts.value;
continue2 = defaultRun;
infinite0 = infinite;
inst.delay = endWaitStep;
if (!infinite0) {
if (runTexts.length > 1) {
console.warn(
"在设置infinite=false的情况下,仅第一个字符串收效,后续字符串不再显现。"
);
}
}
init();
})
function init(): void {
setTimeout(() => {
if (destroyed) {
return;
}
container.value && loop();
}, randomTime);
}
function render(dom: HTMLDivElement, t: string, ut?: string): void {
if (inst.step) {
inst.step--;
} else {
inst.step = step;
if (inst.prefix < prefixString.length) {
inst.prefix >= 0 &&
(inst.text += prefixString[inst.prefix]);
inst.prefix++;
} else {
switch (inst.direction) {
case "forward":
if (inst.skillP < t.length) {
inst.text += t[inst.skillP];
inst.skillP++;
} else {
if (inst.delay) {
inst.delay--;
} else {
inst.direction = "backward";
inst.delay = endWaitStep;
}
}
break;
case "backward":
if (inst.skillP > 0) {
inst.text = inst.text.slice(0, -1);
inst.skillP--;
} else {
inst.skillI =
(inst.skillI + 1) % runTexts.length;
inst.direction = "forward";
}
break;
default:
break;
}
}
}
if (ut != null) {
inst.text = ut.substring(0, inst.skillP);
if (inst.skillP > ut.length) {
inst.skillP = ut.length;
}
}
dom.textContent = inst.text;
let value;
if (inst.prefix < prefixString.length) {
value = Math.min(
colorTextLength,
colorTextLength + inst.prefix
);
} else {
value = Math.min(colorTextLength, t.length - inst.skillP);
}
dom.appendChild(fragment(value));
}
function getNextColor(): string {
return colors[Math.floor(Math.random() * colors.length)];
}
function getNextChar(): string {
return String.fromCharCode(94 * Math.random() + 33);
}
function fragment(value: number): DocumentFragment {
let f = document.createDocumentFragment();
for (let i = 0; value > i; i++) {
let span = document.createElement("span");
span.textContent = getNextChar();
span.style.color = getNextColor();
f.appendChild(span);
}
return f;
}
function loop(): void {
if (destroyed) {
return;
}
setTimeout(() => {
if (continue2 && container.value != null) {
if (destroyed) {
return;
}
let dom = container.value;
let index = inst.skillI;
let originText = texts.value[index];
let currentText = runTexts[index];
if (originText != currentText) {
render(dom, currentText, originText);
runTexts[index] = originText;
} else {
render(dom, currentText);
}
}
if (infinite0) {
loop();
} else {
if (inst.skillP < runTexts[0].length) {
loop();
}
}
}, frameTime);
}
</script>
<style scoped>
.content {
color: black;
height: 100%;
width: 100%;
}
.container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
koa2完成JWT,完善登录服务
const jwt = require('jsonwebtoken');
const secret = 'moderate-vue-admin'; // 秘钥
function getJWTPayload(token) {
// 验证并解析JWT
return jwt.verify(token.split(' ')[1], secret);
}
function getToken(payload = {}) {
return jwt.sign(payload, secret, { expiresIn: '4h' });
}
module.exports = {
getJWTPayload,
getToken
}
// 登录
router.post("/login", async (ctx, next) => {
const { name } = ctx.request.body;
let payload = { name }; // 加密的数据
let permissions
await new Promise((resolve) => {
fs.readFile(path.resolve('db/', `${name}_permissions.json`), (err, dataStr) => {
permissions = JSON.parse(dataStr.toString());
resolve()
})
})
ctx.response.body = {
status: 1,
code: "200",
data: { token: getToken(payload) },
}
})
ok,登录搞定了~~~
权限办理才是重头戏
你了解什么是权限办理?他是干什么的?
- 权限操控的便是路由是否显现
- 权限操控的便是按钮等交互是否能够操作
所以,怎样完成一个简略的权限操控,能够让我的朋友很明晰直接了解权限办理是怎样回事,是很有必要的。
权限办理没必要那么杂乱
有一说一,权限办理,我研讨好几天,我看了若依的,antd pro等等项目的,我都不太喜欢,首要在于我觉得都过于杂乱了,当然他们那么做肯定是有原因的,只不过我都用过,我在用下来之后,对权限能干什么的感知,都是“那些事儿”,那么为了完成“这些事儿”,没必要那么杂乱,所以我希望用最少的代码,去完成大部分权限办理的事儿,便是我的目标。
前端和后端在权限办理的分工,用几句话就能说清楚
- 前端装备自己事儿,后端保存这些事儿能不能做
- 最终办理员用户,操作前端装备权限,将装备的权限数据保存在后端。
所以说前端装备的无论是菜单仍是按钮,关于后端来讲都是事儿,那么装备出来的权限给到后端便是平铺的
[
"index",
"user",
"role",
"index:Add",
"index:EDIT",
"index:DELETE",
"index:IMPORT",
"index:EXPORT",
"user:Add",
"user:EDIT",
"user:DELETE",
"user:IMPORT",
"user:EXPORT",
"role:Add",
"role:EDIT",
"role:DELETE",
"role:IMPORT",
"role:EXPORT"
]
“index”和”user”这样的便是路由。(也能够说成是菜单,这俩一体双面。) “index:Add”和”user:IMPORT”这些便是页面对应的操作 将路由权限和菜单权限装备在一起,简略明了。
不需要写那么多数据,后端不关心前端的内容是啥,你前端自己负责内容是啥,后端只关心的是前端的事儿能不能做,就完了,只能怎样装备,需要前端供给装备页面,也便是人物办理页面。
做个人物办理页面
页面大体是这样的。
装备的页面。
比方我现在把“用户办理”这个权限勾选了,然后就能看到权限的数据结构。
那么用户办理就呈现了。
然后经过接口,传到Koa2服务上,进行保存。
后端接收到前端的数据,依据用户存起来,用户是经过解析token得到用户信息。
为啥我用fs写入文件?由于我担心我朋友不会mongodb数据库的装置装备,我就先用写入的文件的方式代替,简略直接,辅佐学习为主。
然后在登录的时候,再去获取权限。
这样就形成了,前端供给装备项目->办理员用户装备权限->权限传给后端保存->前端再获取权限然后被驱动,形成了一个闭环,这样一个权限办理就做好了。
作用如下:
项目结构
项目地址 github.com/moderateMan…
结束
至此,一个对新手友好的办理后台项目就构建好了,并且还在不断完善中,未来会补全Java后端服务项目,敬请期待,有问题能够随时咨询我,或许留言,这是我整的群,结识一帮情投意合的小伙伴,沟通技能,欢迎水群(我就会玩qq,整其他,我也不会,比方公众号啥的。。。哈哈哈哈)