Bilibili Cloud E Office Vue+SpringBoot フロントエンド・バックエンド分離プロジェクト - ホームメニュー機能

 プロジェクトのフロントエンド学習ノート ディレクトリ

B ステーション クラウド E オフィス Vue+SpringBoot フロントエンドとバックエンド分離プロジェクト - vue.js プロジェクトを構築

Bilibili Cloud E Office Vue+SpringBoot フロントエンド・バックエンド分離プロジェクト - ホームメニュー機能

プロジェクトのバックエンド研究ノートのディレクトリ

B ステーション クラウド E オフィス Vue+SpringBoot フロントエンドとバックエンドの分離プロジェクト - バックグラウンド プロジェクトを構築するための MVC 3 層アーキテクチャ

1. ホームページのデザイン

前回の記事「Bilibili Cloud E Office Vue+SpringBoot フロントエンド分離プロジェクト - vue.js プロジェクトの構築」では、プロジェクトの背景、フロントエンド Vue プロジェクト構造の構築、ログイン モジュールの設計と実装について紹介しました。今回はホームページの開発に入り、主にカタログのダイナミックローディング機能を実現していきます。ここでいう動的ロードとは、ブラウザがログインユーザのユーザ名情報に基づいて、ログインユーザが有するメニューロールを判断し、動的にメニューをロードできることを意味する。

ルーティングモードによりバックエンドからデータメニューを取得し、対応するメニューを自動表示します。以下の図に示す階層構造を実現するには、コード内に対応するディレクトリを作成します。コンテンツは最初に単純なテキスト行にすることができ、最初にスケルトンを構築し、次にコンテンツを入力します。このページを実装する場合、バックエンド インターフェイスの設計は少し複雑になるため、バックエンド ロジックは今後の更新で追加されます。ブログをより論理的かつ階層的に書くために、バックエンドのインターフェース実装部分を別に書きます。フロントエンドを書き、バックエンドに取り組んでいる間も、時々大量のバグが発生し、頭を悩ませます。

1. コードのディレクトリ構造

まずはメニューの骨格を作り、以下のページを作成します(具体的な実装は後ほど追加します)。

views/emp 基本情報EmpBasic.vue を新規作成 EmpAdv.vue

ビュー/従業員あたりのプロファイル新しい PerEmp.vu PerEc.vue PerTrain.vue PerSalary.vue PerMv.vue

views/sal 工资账套 新增SalSob.vue SalSobcfg.vue SalTable.vue SalMonth.vue SalSearch.vue

views/sta総合情報統計追加 StaAll.vue StaScore.vue StaPers.vue StaRecord.vue

views/sys 系统管理 新增 SysBasic.vue SysConfig.vue SysLog.vue SysAdmin.vue SysData.vue SysInit.vue

2. バックエンドはメニュー インターフェイスに情報を返すように要求します。

私たちが設計したメニューは、ユーザー情報に従ってロードされたルーティング情報です。つまり、異なるユーザーは異なるメニュー権限を持つ可能性があります。インターフェースから返されるメニュー情報は次のとおりです。サブメニューは子によって表され、サブメニューのparentIdが親メニューのIDと等しい場合、特定の親子メニュー関係を示します。以下の関係は、「社員プロフィール/基本プロフィール」という階層メニューがあることを示している。

コンポーネントは、コンポーネントが配置されているファイル名を示します。

3. パッケージメニューリクエストツール src/utils/menus.js

メニュー データの管理を容易にするために、vuex のグローバル ステート管理を使用してメニュー データをstore.state に保存します。store.state.routes にデータがある場合は、それを直接使用します。それ以外の場合は、ルーティング メニューを初期化し、getRequest('/system/config/menu') メソッドを通じてバックエンドからルーティング データを取得し、階層関係に従って分割します。

import {getRequest} from "@/utils/api";

// 菜单请求工具类

// router 路由; store Vuex
export const initMenu = (router, store) => {
    // 如果有数据,初始化路由菜单
    if (store.state.routes.length > 0) {
        return;
    }

    getRequest('/system/config/menu').then(data => {
        // 如果数据存在 格式化路由
        if (data) {
            // 格式化好路由
            let fmtRoutes = formatRoutes(data)
            // 添加到 router
            router.addRoutes(fmtRoutes)
            // 将数据存入 Vuex
            store.commit('initRoutes',fmtRoutes)
            // 连接 WebSocket
            store.dispatch('connect')
        }
    })
}

export const formatRoutes = (routes) => {
    let fmtRoutes = []
    routes.forEach(router => {
        let {
            path,
            component,
            name,
            iconCls,
            children
        } = router;
        // 如果有 children 并且类型是数组
        if (children && children instanceof Array) {
            // 递归
            children = formatRoutes(children)
        }
        // 单独对某一个路由格式化 component
        let fmRouter = {
            path: path,
            name: name,
            iconCls: iconCls,
            children: children,
            component(resolve) {
                // 判断组件以什么开头,到对应的目录去找
                if (component.startsWith('Home')) {
                    require(['@/views/' + component + '.vue'], resolve);
                }else if (component.startsWith('Emp')) {
                    require(['@/views/emp/' + component + '.vue'], resolve);
                }else if (component.startsWith('Per')) {
                    require(['@/views/per/' + component + '.vue'], resolve);
                }else if (component.startsWith('Sal')) {
                    require(['@/views/sal/' + component + '.vue'], resolve);
                }else if (component.startsWith('Sta')) {
                    require(['@/views/sta/' + component + '.vue'], resolve);
                }else if (component.startsWith('Sys')) {
                    require(['@/views/sys/' + component + '.vue'], resolve);
                }
            }
        }
        fmtRoutes.push(fmRouter)
    })
    return fmtRoutes
}

インターフェイスのコンポーネントフィールドに従って、対応するコードパスを見つけるにはどうすればよいですか?

インターフェイス オブジェクト内のコンポーネント フィールドを分類して検索します。たとえば、コンポーネントは Home で始まり、ソース コードは src/views/Home.vue にあります。

if (component.startsWith('Home')) {

require(['@/views/' + component + '.vue'], resolve);

}

initMenu メソッドはルーティング データをストアに保存します。ストアにデータがある場合は初期化する必要はありませんが、データがない場合は初期化されます。

いつ呼ばれますか? すべてのページで初期化メニュー メソッドを呼び出す必要があります。これをルートインターセプターに入れて、ルートにアクセスするたびに実行できます。次にルートインターセプタを設計します

4. ルーティング インターセプターを追加 - main.js を更新

router.beforeEach を使用して、グローバル beforeEach ガードを登録します。(to、from、next) 3 つのパラメーター: 行くルートへ、どこからのルート、next() リリース。ユーザーがログインに成功したら、トークンを sessionStorage に保存し、トークンが保持されている場合は、メニューを初期化して放します。

ユーザーの初回ログイン時に現在のユーザー情報をsessionStorageのユーザーに保存し、経路が切り替わるたびにユーザーのログイン情報を取得します。

// 使用 router.beforeEach 注册一个全局前置守卫
router.beforeEach((to, from, next) => {
  // to 要去的路由; from 来自哪里的路由 ; next() 放行
  // 用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行
  if (window.sessionStorage.getItem('tokenStr')) {
      initMenu(router, store)
      // 如果用户不存在
      if (!window.sessionStorage.getItem('user')
      ) {
          // 判断用户信息是否存在
          return getRequest('/admin/info').then(resp => {
              if (resp) {
                  // 存入用户信息,转字符串,存入 sessionStorage
                  window.sessionStorage.setItem('user', JSON.stringify(resp))
                  // 同步用户信息 编辑用户
                  store.commit('INIT_ADMIN',resp)
                  next();
              }
          })
      }
      next();
  } else {
      if (to.path === '/') {
          next()
      } else {
          next('/?redirect=' + to.path)
      }
  }
})

 5.store/index.jsの設定

vuex によるルーティング状態管理

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 导入 Vuex
const store = new Vuex.Store({
    state: {
        routes: []
    },
    mutations: { // 与 state 同步执行;可以改变 state 对应的值的方法
        // 初始化路由 菜单
        initRoutes(state, data) {
            state.routes = data
        },
    },
    // 异步执行
    actions: {
        }
})

export default store;

Main.jsでストアを導入

import store from './store'
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')

6. ホームページの機能とスタイルの実現

ナビゲーション メニュー機能は、element-ui に付属するコンテナ レイアウト コンテナを使用して実装されます。選択インターフェイスのスタイルは次のとおりです。

関連コンポーネントの紹介

エルコンテナ外容器

el-header トップバーコンテナ

el-aside サイドバー コンテナ el-menu ナビゲーション領域

el-main メインエリアコンテナ

エルフッターのボトムバーコンテナ

el-menu ナビゲーションに router 属性を追加して、メニュー ルーティングの動的なレンダリングを実現します

Home.vueは、ホームページ全体の左側のメニューの取得と表示、右上のパーソナルセンターの設定を実現します。現在のメニュー情報と現在のユーザーのログイン情報をstore.stateから取得します。

el-menu ナビゲーションに router 属性を追加して、メニュー ルーティングの動的なレンダリングを実現します。ホームページ ナビゲーション メニューは、element-ui の NavMenu ナビゲーション メニュー コントロールを使用します。メニューがクリックされるたびに 1 つのメニューのみが展開されるようにするには、属性 unique-opened: を使用します。router 属性を使用して、ナビゲーションをアクティブにするときにルーティング ジャンプのパスとしてインデックスを使用します。

el-dropdown-item のコマンドを、el-dropdown のメニュー項目 @command をクリックすることでトリガーされるイベント コールバック メソッドにバインドして、ログアウトとログインを実現し、パーソナル センター機能に入ります。要素の MessageBox ブレット ボックスは、ログアウトとログインのプロンプト ボックスを実現します。ログアウト後、vuex のメニュー情報をクリアします。

el-breadcrumb ブレッドクラム コントロールを使用すると、現在のページのパスを表示し、前のページにすぐに戻ります。ホーム ページ以外の場合、v-if="this.$router.currentRoute.path!=='/home'" 表示レベル: 最初のページ/現在のページ。

ホームページ v-if="this.$router.currentRoute.path==='/home'" の場合、ウェルカム フォントを表示します。

7. Home.vue ファイル - ホームページ 

<template>
  <div>
    <el-container>
      <el-header class="homeHeader">
        <div class="title">云办公</div>
        <!-- 1-1 添加在线聊天入口 -->
        <div>
          <el-button type="text" icon="el-icon-bell" size="normal"
                     style="margin-right: 8px;color: black;" @click="goChar"></el-button>
          <el-dropdown class="userInfo" @command="commandHandler">
          <span class="el-dropdown-link">
            {
   
   { user.name }}<i><img :src="user.userFace"></i>
          </span>
            <el-dropdown-menu slot="dropdown">
              <el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
              <el-dropdown-item command="setting">设置</el-dropdown-item>
              <el-dropdown-item command="logout">注销登录</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      </el-header>
      <el-container>
        <el-aside width="200px">

          <!-- 1、添加 router  -->
          <el-menu router unique-opened>
            <!-- 2、循环整个路由组件,不展示 hidden: true 的路由组件 -->
            <el-submenu :index="index +''" v-for="(item,index) in routes"
                        :key="index" v-if="!item.hidden">
              <template slot="title"><i :class="item.iconCls" style="color: black;margin-right: 5px"></i>
                <span>{
   
   { item.name }}</span>
              </template>
              <!-- 3、循环遍历子路由 -->
              <el-menu-item :index="children.path"
                            v-for="(children,index) in item.children" :key="index">{
   
   { children.name }}
              </el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
        <el-main>
          <!-- 面包屑导航区域 -->
          <el-breadcrumb separator-class="el-icon-arrow-right"
                         v-if="this.$router.currentRoute.path!=='/home'">
            <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item>{
   
   { this.$router.currentRoute.name }}</el-breadcrumb-item>
          </el-breadcrumb>
          <div class="homeWelcome" v-if="this.$router.currentRoute.path==='/home'">
            欢迎来到云办公系统!
          </div>
          <!-- 路由点位符 -->
          <router-view class="homeRouterView"/>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
<script>
  export default {
    name: 'Home',
    data() {
      return {
        // 获取用户信息,将字符串转对象
        // user: JSON.parse(window.sessionStorage.getItem('user'))
      }
    },
    computed: {
      // 从 vuex 获取 routes
      routes() {
        return this.$store.state.routes
      },
      user() {
        return this.$store.state.currentAdmin
      }
    },
    methods: {
      // 1-2 进入在线聊天页面
      goChar() {
        this.$router.push('/chat')
      },
      // 注销登录
      commandHandler(command) {
        if (command === 'logout') {
          // 弹框提示用户是否要删除
          this.$confirm('此操作将注销登录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            // 注销登录
            this.postRequest('/logout')
            // 清空用户信息
            window.sessionStorage.removeItem('tokenStr')
            window.sessionStorage.removeItem('user')
            // 路由替换到登录页面
            // this.$router.replace('/')
            // 清空菜单信息;在src/utils/menus.js 中初始化菜单信息
            this.$store.commit('initRoutes', [])
            this.$router.replace('/')
          }).catch(() => {
            this.$message({
              type: 'info',
              message: '已取消注销登录'
            });
          });
        }
        if (command === 'userinfo') {
          this.$router.push('/userinfo')
        }
      }
    }
  }
</script>
<style scoped>
  .homeHeader {
    background: #3e9ef5;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 15px;
    box-sizing: border-box;

  }

  .homeHeader .title {
    font-size: 30px;
    /*font-family: 微软雅黑;*/
    font-family: 华文楷体;
    color: white;
  }

  .homeHeader .userInfo {
    cursor: pointer;
  }

  .el-dropdown-link img {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    margin-left: 8px;
  }

  .homeWelcome {
    text-align: center;
    font-size: 30px;
    font-family: 华文楷体;
    color: #409ef4;
    padding-top: 50px;
  }

  .homeRouterView {
    margin-top: 10px;
  }
</style>

8. ルート router/index.js を更新します。

router/index.js の hidden:true を無視します。

/home ルートはホームページから取得されます

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
Vue.use(VueRouter)

const routes = [
    {
        path: '/',
        name: 'Login',
        component: Login,
        hidden: true // 不会被循环遍历出来
    }
]

const router = new VueRouter({
    routes
})

export default router

9.index.htmlの余白をなくす

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>yeb-front</title>
  </head>
  <body style="margin:0px;padding:0px">
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

おすすめ

転載: blog.csdn.net/qq_36384657/article/details/124655783