阅读 1930

搭建后台管理系统的思路

从零开始搭建后台管理系统

当然,这是一个简易版本的,你可以在这两个基础上加以改造

搭建后台管理系统最基础的是什么呢?个人的体会是整体的基础框架,这个是指最基础的框架,比如根 router-view, 侧边栏以及侧边栏的router-view,以及顶部栏,等基础布局的控制。

root

首先 App.vue 只有展示 rouer-view, 这个就是 root, 所谓的根。

<template>
  <router-view></router-view>
</template>
复制代码

ok 有了根之后,我们需要有一个 layout 页面,这个页面来承载我们的侧边栏,顶部栏

layout 页面他是两栏布局的,一栏是我们的侧边导航栏,

侧边栏

如何完成这个两栏布局

  • 可以使用 float
  • 可以使用弹性布局 display: flex
  • 也可以使用定位

侧边导航栏,可能我们需要来研究 element-ui 的组件 NavMenu 导航菜单

侧边导航栏需要我们路由的一些信息,比如路由对应的组件,就像 router-link 对应的 router-view

如果菜单是二级菜单,三级菜单,需要怎么处理

如果需要折叠菜单,需要怎么处理,这里就需要阅读 折叠菜单组件

也就是说侧边菜单来其实就是一个个的 router-link

然后扩展菜单项是否有 icon 小图标,是否有标题存在,那么路由就需要设置 meta 属性了

顶部栏

接下来就是顶部栏,顶部导航栏有什么呢?

  • 面包屑
  • 消息通知
  • 下拉菜单
  • 关闭展开侧边栏按钮

面包屑

需要注意什么呢?需要注意是否需要点击跳转的,定位到那一级菜单的问题

需要研究 Breadcrumb 面包屑

关闭展开侧边栏按钮

需要使用 vuex 来存储打开与否的这个状态值,通过 vuex 来更改状态

AppMain.vue

<template>
  <section class="app-main">
    <!-- 内部应该显示子路由页面信息 -->
    <router-view v-slot="{ Component }">
      <component :is="Component" />
    </router-view>
  </section>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  name: "AppMain",
});
</script>

<style lang="scss" scoped>
.app-main {
  /*50 = navbar  */
  min-height: calc(100vh - 50px);
  width: 100%;
  position: relative;
  overflow: hidden;
}
</style>
复制代码

用来展示侧边栏的 router-link 对应的 router-view

所以,又回到 layout 页面整体框架如下:

<template>
  <div class="app-wrapper">
    <!-- 侧边栏 -->
    <side-bar class="sidebar-container"></side-bar>
    <!-- 内容容器 -->
    <div class="main-container">
      <!-- 顶部导航栏 -->
      <nav-bar />
      <!-- 内容区 -->
      <app-main />
    </div>
  </div>
</template>

<script setup>
import AppMain from "layout/components/AppMain.vue";
import NavBar from "layout/components/NavBar.vue";
import SideBar from "layout/components/Sidebar/index.vue";
</script>

<style lang="scss" scoped>
@import "styles/mixin.scss";
.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;
}
</style>
复制代码

请求封装

  • token 处理
  • 响应处理
  • 请求处理
import axios from 'axios';

import { Message, Msgbox } from 'element3';

import store from 'store/index.js';

// 创建 axios 实例

const service = axios.create({
    // 在请求地址前面加上 baseURL
    baseURL: import.meta.env.VITE_BASE_API,
    // 当前发送跨域请求时携带 cookie
    withCredentials: true,
    timeout: 5000
});

// 请求拦截
service.interceptors.request.use(
    (config) => {
        // 指定请求令牌
        // if (store.getters.token) {
        // // 自定义令牌的字段名为X-Token,根据咱们后台再做修改
        // config.headers["X-Token"] = store.getters.token;
        // }
        config.headers["X-Token"] = "my token";
        return config;
    },
    (error) => {
        // 请求错误的统一处理
        console.log(error); // for debug
        return Promise.reject(error);
    }
);

// 响应拦截器
service.interceptors.response.use(
    /**
     * If you want to get http information such as headers or status
     * Please return  response => response
     */

    /**
     * 通过判断状态码统一处理响应,根据情况修改
     * 同时也可以通过HTTP状态码判断请求结果
     */
    (response) => {
        const res = response.data;

        // 如果状态码不是20000则认为有错误
        if (res.code !== 20000) {
            Message.error({
                message: res.message || "Error",
                duration: 5 * 1000,
            });

            // 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期;
            if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
                // 重新登录
                Msgbox.confirm("您已登出, 请重新登录", "确认", {
                    confirmButtonText: "重新登录",
                    cancelButtonText: "取消",
                    type: "warning",
                }).then(() => {
                    store.dispatch("user/resetToken").then(() => {
                        location.reload();
                    });
                });
            }
            return Promise.reject(new Error(res.message || "Error"));
        } else {
            return res;
        }
    },
    (error) => {
        console.log("err" + error); // for debug
        Message({
            message: error.message,
            type: "error",
            duration: 5 * 1000,
        });
        return Promise.reject(error);
    }
);

export default service;
复制代码

多语言

多语言会用到 vue-i18n 这样的插件

就需要研究官网文档了 vue-i18n

有一个 vite 多语言插件

intlify/vite-plugin-vue-i18n

vite.config.js 配置

import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueI18n from '@intlify/vite-plugin-vue-i18n'

export default defineConfig({
  plugins: [
    vue(), // you need to install `@vitejs/plugin-vue`
    vueI18n({
      // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
      // compositionOnly: false,

      // you need to set i18n resource including paths !
      include: path.resolve(__dirname, './path/to/src/locales/**')
    })
  ]
})
复制代码

模板这样使用多语言

<template>
  <form>
    <label>{{ t('language') }}</label>
    <select v-model="locale">
      <option value="en">en</option>
      <option value="ja">ja</option>
    </select>
  </form>
  <p>{{ t('hello') }}</p>
</template>

<script>
import { useI18n } from 'vue-i18n'

export default {
  name: 'App',
  setup() {
    const { locale, t } = useI18n({
      inheritLocale: true
    })

    return { locale, t }
  }
}
</script>

<i18n>
{
  "en": {
    "language": "Language",
    "hello": "hello, world!"
  },
  "ja": {
    "language": "言語",
    "hello": "こんにちは、世界!"
  }
}
</i18n>
复制代码

当然,可以在 main.js 引入多语言

import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
/*
 * The i18n resources in the path specified in the plugin `include` option can be read
 * as vue-i18n optimized locale messages using the import syntax
 */
import en from './src/locales/en.json'
import ja from './src/locales/ja.yaml'
import fr from './src/locales/fr.json5'

const i18n = createI18n({
  locale: 'en',
  messages: {
    en,
    ja,
    fr
  }
})

const app = createApp()
app.use(i18n).mount('#app)
复制代码

element3 组件

// 完整引入
import element3 from "element3";
import "element3/lib/theme-chalk/index.css";

export default function (app) {
    // 完整引入
    app.use(element3);
};
复制代码

我们要如何组建自己的样式目录

  • var.scss 用于提取颜色值,字体大小值,字体权重值等
  • mixin.scss 写一些公用的样式

目录的重定向问题

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { viteMockServe } from 'vite-plugin-mock';
import path from 'path';
import vueI18n from '@intlify/vite-plugin-vue-i18n'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    viteMockServe({ supportTs: false }),
    vueI18n({
      // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
      // compositionOnly: false,

      // you need to set i18n resource including paths !
      include: path.resolve(__dirname, './src/locales/**')
    })
  ],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
      "comps": path.resolve(__dirname, "src/components"),
      "api": path.resolve(__dirname, "src/api"),
      "views": path.resolve(__dirname, "src/views"),
      "styles": path.resolve(__dirname, "src/styles"),
      "locales": path.resolve(__dirname, "src/locales"),
      "layout": path.resolve(__dirname, "src/layout"),
      "utils": path.resolve(__dirname, "src/utils"),
      "dirs": path.resolve(__dirname, "src/dirs"),
      "plugins": path.resolve(__dirname, "src/plugins"),
      "config": path.resolve(__dirname, "src/config"),
      "router": path.resolve(__dirname, "src/router"),
      "store": path.resolve(__dirname, "src/store"),
      "model": path.resolve(__dirname, "src/model")
    }
  }
});
复制代码
文章分类
前端
文章标签