朋友找我帮助

对,仍是那位朋友,一位接近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,整其他,我也不会,比方公众号啥的。。。哈哈哈哈)

帮一朋友从头搭办理后台,他直呼灵通,但基操勿六