八、数据大屏和权限控制

136 阅读6分钟

数据大屏

功能展示

先安上下分

image-20230902233231284.png 下 buttom 又分为左中右

image-20230902233453485.png

而在左中右又分为上中下,或者上下

image-20230902233717911.png

代码实现

  1. 在project\src\views\screen\images引入images

  2. 安装相关组件

    # 安装时间组件(用于动态显示当前的时间)
    pnpm i moment
    # 安装 echarts
    pnpm i echarts
    # 安装echarts 扩展插件之水球图
    pnpm i echarts-liquidfill
    
  1. 封装对应的组件 (代码见码云)

    1. project\src\views\screen\components\age\index.vue
    2. project\src\views\screen\components\couter\index.vue
    3. project\src\views\screen\components\line\index.vue
    4. project\src\views\screen\components\map\china.json
    5. project\src\views\screen\components\map\index.vue
    6. project\src\views\screen\components\rank\index.vue
    7. project\src\views\screen\components\sex\index.vue
    8. project\src\views\screen\components\top\index.vue
    9. project\src\views\screen\components\tourist\index.vue
    10. project\src\views\screen\components\year\index.vue
  1. 在大屏的index.vue中引入project\src\views\screen\index.vue
<template>
    <div class="container">
        <!-- 数据大屏展示内容区域 -->
        <div class="screen" ref="screen">
            <!-- 数据大屏顶部 -->
            <div class="top">
                <Top />
            </div>
            <div class="bottom">
                <div class="left">
                    <Tourist class="tourist"></Tourist>
                    <Sex class="sex"></Sex>
                    <Age class="age"></Age>
                </div>
                <div class="center">
                    <Map class="map"></Map>
                    <Line class="line"></Line>
                </div>
                <div class="right">
                    <Rank class="rank"></Rank>
                    <Year class="year"></Year>
                    <Counter class="count"></Counter>
                </div>
            </div>
        </div>
    </div>
</template><script setup lang="ts">
import { ref, onMounted } from "vue";
//引入顶部的子组件
import Top from './components/top/index.vue';
//引入左侧三个子组件
import Tourist from './components/tourist/index.vue';
import Sex from './components/sex/index.vue';
import Age from './components/age/index.vue'//引入中间两个子组件
import Map from './components/map/index.vue';
import Line from './components/line/index.vue';
​
//引入右侧三个子组件
import Rank from './components/rank/index.vue';
import Year from './components/year/index.vue';
import Counter from './components/couter/index.vue'
//获取数据大屏展示内容盒子的DOM元素
let screen = ref();
onMounted(() => {
    screen.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
});
//定义大屏缩放比例
function getScale(w = 1920, h = 1080) {
    const ww = window.innerWidth / w;
    const wh = window.innerHeight / h;
    return ww < wh ? ww : wh;
}
//监听视口变化
window.onresize = () => {
    screen.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
}
​
​
</script><style scoped lang="scss">
.container {
    width: 100vw;
    height: 100vh;
    background: url(./images/bg.png) no-repeat;
    background-size: cover;
​
    .screen {
        position: fixed;
        width: 1920px;
        height: 1080px;
        left: 50%;
        top: 50%;
        transform-origin: left top;
​
        .top {
            width: 100%;
            height: 40px;
        }
​
        .bottom {
            display: flex;
​
            .right {
                flex: 1;
                display: flex;
                flex-direction: column;
                margin-left: 40px;
​
                .rank {
                    flex: 1.5;
                }
​
                .year {
                    flex: 1;
​
                }
​
                .count {
                    flex: 1;
                }
            }
​
            .left {
                flex: 1;
                height: 1040px;
                display: flex;
                flex-direction: column;
​
                .tourist {
                    flex: 1.2;
                }
​
                .sex {
                    flex: 1;
​
                }
​
                .age {
                    flex: 1;
                }
            }
​
            .center {
                flex: 1.5;
                display: flex;
                flex-direction: column;
​
                .map {
                    flex: 4;
                }
​
                .line {
                    flex: 1;
                }
            }
        }
    }
}
</style>

权限控制

  1. 功能分析

    菜单的权限:
    超级管理员账号:admin atguigu123   拥有全部的菜单、按钮的权限
    飞行员账号  硅谷333  111111       不包含权限管理模块、按钮的权限并非全部按钮
    同一个项目:不同人(职位是不一样的,他能访问到的菜单、按钮的权限是不一样的)
    ​
    ​
    一、目前整个项目一共多少个路由!!!
    login(登录页面)、
    404(404一级路由)、
    任意路由、
    首页(/home)、
    数据大屏、
    权限管理(三个子路由)
    商品管理模块(四个子路由)
    ​
    1.1开发菜单权限
    ---第一步:拆分路由
    静态(常量)路由:大家都可以拥有的路由
    login、首页、数据大屏、404
    ​
    异步路由:不同的身份有的有这个路由、有的没有
    权限管理(三个子路由)
    商品管理模块(四个子路由)
    ​
    任意路由:任意路由
    ​
    1.2菜单权限开发思路
    目前咱们的项目:任意用户访问大家能看见的、能操作的菜单与按钮都是一样的(大家注册的路由都是一样的)
    
  2. 安装 插件

    //  可以操作js对象,主要用于深拷贝对象,解决权限递归遍历时将数据覆盖,从而导致账号切换时候权限不对的问题
    pnpm i lodash
    
  3. 路由拆分

    //对外暴露配置路由(常量路由):全部用户都可以访问到的路由
    export const constantRoute = [
        {
            //登录
            path: '/login',
            component: () => import('@/views/login/index.vue'),
            name: 'login',
            meta: {
                title: '登录',//菜单标题
                hidden: true,//代表路由标题在菜单中是否隐藏  true:隐藏 false:不隐藏
                icon: "Promotion",//菜单文字左侧的图标,支持element-plus全部图标
            }
        }
        ,
        {
            //登录成功以后展示数据的路由
            path: '/',
            component: () => import('@/layout/index.vue'),
            name: 'layout',
            meta: {
                title: '',
                hidden: false,
                icon: ''
            },
            redirect: '/home',
            children: [
                {
                    path: '/home',
                    component: () => import('@/views/home/index.vue'),
                    meta: {
                        title: '首页',
                        hidden: false,
                        icon: 'HomeFilled'
                    }
                }
            ]
        },
        {
            //404
            path: '/404',
            component: () => import('@/views/404/index.vue'),
            name: '404',
            meta: {
                title: '404',
                hidden: true,
                icon: 'DocumentDelete'
            }
        },
        {
            path: '/screen',
            component: () => import('@/views/screen/index.vue'),
            name: 'Screen',
            meta: {
                hidden: false,
                title: '数据大屏',
                icon: 'Platform'
            }
        }]
    ​
    //异步路由
    export const asnycRoute = [
        {
            path: '/acl',
            component: () => import('@/layout/index.vue'),
            name: 'Acl',
            meta: {
                title: '权限管理',
                icon: 'Lock'
            },
            redirect: '/acl/user',
            children: [
                {
                    path: '/acl/user',
                    component: () => import('@/views/acl/user/index.vue'),
                    name: 'User',
                    meta: {
                        title: '用户管理',
                        icon: 'User'
                    }
                },
                {
                    path: '/acl/role',
                    component: () => import('@/views/acl/role/index.vue'),
                    name: 'Role',
                    meta: {
                        title: '角色管理',
                        icon: 'UserFilled'
                    }
                },
                {
                    path: '/acl/permission',
                    component: () => import('@/views/acl/permission/index.vue'),
                    name: 'Permission',
                    meta: {
                        title: '菜单管理',
                        icon: 'Monitor'
                    }
                }
            ]
        }
        ,
        {
            path: '/product',
            component: () => import('@/layout/index.vue'),
            name: 'Product',
            meta: {
                title: '商品管理',
                icon: 'Goods',
            },
            redirect: '/product/trademark',
            children: [
                {
                    path: '/product/trademark',
                    component: () => import('@/views/product/trademark/index.vue'),
                    name: "Trademark",
                    meta: {
                        title: '品牌管理',
                        icon: 'ShoppingCartFull',
                    }
                },
                {
                    path: '/product/attr',
                    component: () => import('@/views/product/attr/index.vue'),
                    name: "Attr",
                    meta: {
                        title: '属性管理',
                        icon: 'ChromeFilled',
                    }
                },
                {
                    path: '/product/spu',
                    component: () => import('@/views/product/spu/index.vue'),
                    name: "Spu",
                    meta: {
                        title: 'SPU管理',
                        icon: 'Calendar',
                    }
                },
                {
                    path: '/product/sku',
                    component: () => import('@/views/product/sku/index.vue'),
                    name: "Sku",
                    meta: {
                        title: 'SKU管理',
                        icon: 'Orange',
                    }
                },
            ]
        }
    ]
    ​
    //任意路由
    export const anyRoute = {
        //任意路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any',
        meta: {
            title: '任意路由',
            hidden: true,
            icon: 'DataLine'
        }
    }
    
  4. 修改userStore 中引入的路由,和与异步路由过略合并

    //创建用户相关的小仓库
    import { defineStore } from 'pinia';
    //引入接口
    import { reqLogin, reqUserInfo, reqLogout } from '@/api/user';
    import type { loginFormData, loginResponseData, userInfoReponseData } from "@/api/user/type";
    import type { UserState } from './types/type';
    //引入操作本地存储的工具方法
    import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token';
    //引入路由(常量路由)
    import { constantRoute, asnycRoute, anyRoute } from '@/router/routes';
    ​
    //引入深拷贝方法
    //@ts-ignore
    import cloneDeep from 'lodash/cloneDeep'
    import router from '@/router';
    //用于过滤当前用户需要展示的异步路由
    function filterAsyncRoute(asnycRoute: any, routes: any) {
        return asnycRoute.filter((item: any) => {
            if (routes.includes(item.name)) {
                if (item.children && item.children.length > 0) {
                    //硅谷333账号:product\trademark\attr\sku
                    item.children = filterAsyncRoute(item.children, routes);
                }
                return true;
            }
        })
    }
    ​
    //创建用户小仓库
    let useUserStore = defineStore('User', {
        //小仓库存储数据地方
        state: (): UserState => {
            return {
                token: GET_TOKEN(),//用户唯一标识token
                menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
                username: '',
                avatar: '',
                //存储当前用户是否包含某一个按钮
                buttons:[],
            }
        },
        //异步|逻辑的地方
        actions: {
            //用户登录的方法
            async userLogin(data: loginFormData) {
                //登录请求
                let result: loginResponseData = await reqLogin(data);
                //登录请求:成功200->token 
                //登录请求:失败201->登录失败错误的信息
                if (result.code == 200) {
                    //pinia仓库存储一下token
                    //由于pinia|vuex存储数据其实利用js对象
                    this.token = (result.data as string);
                    //本地存储持久化存储一份
                    SET_TOKEN((result.data as string));
                    //能保证当前async函数返回一个成功的promise
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.data));
                }
            },
            //获取用户信息方法
            async userInfo() {
                //获取用户信息进行存储仓库当中[用户头像、名字]
                let result: userInfoReponseData = await reqUserInfo();
                //如果获取用户信息成功,存储一下用户信息
                if (result.code == 200) {
                    this.username = result.data.name;
                    this.avatar = result.data.avatar;
                    this.buttons = result.data.buttons;
                    //计算当前用户需要展示的异步路由
                    let userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute), result.data.routes);
                    //菜单需要的数据整理完毕
                    this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute];
                    //目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
                    [...userAsyncRoute, anyRoute].forEach((route: any) => {
                        router.addRoute(route);
                    });
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.message));
                }
            },
            //退出登录
            async userLogout() {
                //退出登录请求
                let result: any = await reqLogout();
                if (result.code == 200) {
                    //目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失效)
                    this.token = '';
                    this.username = '';
                    this.avatar = '';
                    REMOVE_TOKEN();
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.message));
                }
    ​
            }
    ​
        },
        getters: {
    ​
        }
    })
    //对外暴露获取小仓库方法
    export default useUserStore;
    
  5. 404页面搭建

    <template>
        <div class="box">
           <img src="../../assets/images/error_images/404.png" alt="">
           <button @click="goHome">首页</button>
        </div>
    </template><script setup lang="ts">
    import {useRouter} from 'vue-router';
    let $router = useRouter();
    const goHome = ()=>{
       $router.push('/home')
    }
    </script><style scoped lang="scss">
    .box{
      width: 100vw;
      height: 100vh;
      background: yellowgreen;
      display: flex;
      justify-content: center;
      img{
        width: 800px;
        height: 400px;
      }
      button{
        width: 50px;
        height: 50px;
      }
    }
    </style>
    
  6. 按钮权限控制

将当前用户的按钮标识储存到userStore 仓库,

定义自定义子令并在main.ts 注册

project\src\directive\has.ts

import pinia from '@/store';
import useUserStore from '@/store/modules/user';
let userStore =useUserStore(pinia)
export const isHasButton = (app: any) => {
    //获取对应的用户仓库
    //全局自定义指令:实现按钮的权限
    app.directive('has', {
        //代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次
        mounted(el:any,options:any) {
            //自定义指令右侧的数值:如果在用户信息buttons数组当中没有
            //从DOM树上干掉
            if(!userStore.buttons.includes(options.value)){
               el.parentNode.removeChild(el);
            }
        },
    })
}
​

注册指令

............................
// 注册模板路由
app.use(router);
// 注册仓库
app.use(pinia);
//引入自定义指令文件 +++++++++++++
import { isHasButton } from '@/directive/has';
// +++++++++++++++++++++
isHasButton(app);
// 将应用挂载到挂载点
app.mount('#app')

使用指令

project\src\views\product\trademark\index.vue

<template>
  <div>
     <el-card class="box-card">
​
            <!-- 卡片顶部添加品牌按钮 ++++++++++++++ v-has="`btn.Trademark.add`" -->
            <el-button type="primary" size="default" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</el-button>
            <!-- 表格组件:用于展示已有得平台数据 -->
            <!-- table:---border:可以设置表格纵向是否有边框
                table-column:---label:某一个列表 ---width:设置这列宽度 ---align:设置这一列对齐方式    
            -->
           <el-table style="margin:10px 0px" border :data="trademarkArr">
             .......................

在按钮显示的地方判断当前用户的所有的

项目上线

pnpm run build