vue2.X 中使用 echarts5.4.0实现项目进度甘特图

news2024/10/1 12:13:18

vue2.X 中使用 echarts5.4.0实现项目进度甘特图

效果图:
在这里插入图片描述
左侧都是名称上面时间,当中的内容是日志内容

  1. 组件: gantt.vue
<template>
    <div id="main" style="width: 100%; height: 100%"></div>
  </template>
  <script>
  import * as echarts from "echarts";
  export default {
    name: "Gantt",
    props: {
      baseDate: {
        type: String,
        default: "",
      },
      ganttData: {
        type: Array,
        default: () => [],
      },
      roomData: {
        type: Array,
        default: () => [],
      },
    },
    data() {
      return {
          minHours: '08:00',
          maxHours: '24:00',
         // colors: ['#5c2223','#346c9c', '#525288', '#87723e', '#d1c2d3','#f07c82', '#835e1d', '#d99156', '#954416', '#ee8055', '#126e82', '#61649f', '#a7a8bd']
      };
    },
    created() {},
    mounted() {
     this.myEcharts();
    },
    watch: {
      ganttData(newVal) {
        this.myEcharts();
      },
    },
    methods: {
      getTimes() {
        let baseDate = `${this.baseDate}`;
        // 获取日志的最早和最晚时间,这样防止两边出现空白,比如00:00-09:00;
        this.$http.post(`/sys/task/times`, {taskDate: baseDate}).then(({ data: res }) => {
          if (res.code !== 0) {
              return this.$message.error(res.msg)
          }
          this.minHours = res.data.minTime
          this.maxHours = res.data.maxTime
        }).catch(() => {})
      },
      myEcharts() {
        this.getTimes();
        // 基于准备好的dom,初始化echarts实例
        const container = document.getElementById("main");
        this.$echarts.init(container).dispose();
        var myChart = this.$echarts.init(container);
        // 用于随机颜色
        var colors= ['#8dddfa','#f98e72', '#f7b84f', '#a872f9', '#d6a9d1','#a7e56d', '#ff73c7', '#d6a9d1', '#b1e7fb', '#d3b3af', '#2859b1', '#1f6cb0']
        //let min = `${this.$moment().format("YYYY-MM-DD")} 00:00:00`;
        //let max1 = `${this.$moment().add(1, "day").format("YYYY-MM-DD")} 00:00:00`;
        // 指定图表的配置项和数据
        var option = {
          color: "#0A8BFF",
          backgroundColor: "#fff",
          title: {},
          tooltip: {
            // enterable: true,
            trigger: "item",
            show: true,
            // axisPointer: { // 坐标轴指示器,坐标轴触发有效
            //   type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
            // },
            // alwaysShowContent: true,
            hideDelay: 100,
            backgroundColor: "rgba(255,255,255,1)", // 背景颜色(此时为默认色)
            borderRadius: 5, // 边框圆角
            confine: true,
            textStyle: {
              color: "#000"
            },
            // 悬浮的时候展示对应的数据
            formatter: function (params) {
              for(var i = 0; i < params.data.value.length; i++) {
                var content = (
                  params.data.value[i].content +
                  // "<br/>" +
                  // (params.data.value[i].status === "1" ? '<span style="color:#4dc394;">已完成</span>' : '<span style="color:#e5835b;">进行中</span>') +
                  "<br/>" +
                  params.data.value[i].stime +
                  " - " +
                  params.data.value[i].etime
                );
                // 作用:鼠标悬浮在内容,出现弹窗显示内容详情,这里限制了宽,以防止宽度过长
                 var tooltipHtml = '<div style="width:fit-content; max-width: 500px; white-space: wrap;">'+content+'</div>';
                 return tooltipHtml;
                //return content;
              }
            },
          },
          legend: {
            // left: '90px',
            top: "1%",
            itemWidth: 16,
            itemHeight: 16,
            show: true,
            // selectedMode: false, // 图例设为不可点击
            textStyle: {
              color: "rgba(0, 0, 0, 0.45)",
              fontSize: 14,
            },
          },
          grid: {
            // 绘图网格
            left: "2%",
            right: "3%",
            top: "10%",
            bottom: "10%",
            containLabel: true,
          },
          xAxis: {
            type: "time",
            position: "top",
          //  interval: 3600 * 1000, // 以一个小时递增
          // 以一小时的时间递增
            minInterval: 3600 * 1000 ,
            maxInterval: 3600 * 1000 ,
            min: `${this.baseDate} ` + this.minHours,
            max: `${this.baseDate} ` + this.maxHours,
            //max:`${this.baseDate} 24:00`,
            //max: `${this.baseDate} 19:00`, // 设置最大时间为18点
            //min:`${this.baseDate} 00:00`, //将data里最小时间的整点时间设为min,否则min会以data里面的min为开始进行整点递增
            //min: `${this.baseDate} 08:00`, // 将data里最小时间的整点时间设为min,否则min会以data里面的min为开始进行整点递增
            axisLabel: {
              // 设置最后一个数据显示
              showMaxLabel: true,
              formatter: function (value, index) {
                var data = new Date(value);
                var hours = data.getHours();
                var minutes = data.getMinutes();
                if ((index !== 0 && hours === 0) || index === 25){
                  return "23:59"
                } else {
                  //return hours + ":00";
                  if (minutes === 0) {
                    return hours + ":00";
                  } else {
                    return hours + ":" + minutes;
                  }
                }
              },
              textStyle: {
                color: "rgba(0,0,0,0.65)", // 更改坐标轴文字颜色
                fontSize: 14, // 更改坐标轴文字大小
              },
            },
            axisLine: {
              lineStyle: {
                color: "#e5e5e5",
              },
              onZero: false,
            },
            splitLine: {
              show: true,
              lineStyle: {
                type: "dashed",
              },
            },
          },
          // dataZoom: [
          //   // 给x轴设置滚动条
          //   {
          //     type: 'slider',
          //     show: true,
          //     yAxisIndex: [0, 1],
          //     left: '96%',
          //     start: 1,
          //     end: 100,
          //     fiterMode: 'filter'
          //   },
          //   {
          //     type: 'inside',
          //     fiterMode: 'filter',
          //     yAxisIndex: [0, 1],
          //     start: 1,
          //     end: 100
  
          //   }
          // ],
          yAxis: {
            inverse: true, // 是否反转
            type: "category",
            axisTick:{
              show: true //不显示坐标轴刻度线
            },
            splitLine: {     //网格线
              "show": true
            },
            axisLine: {
              show: true,
              lineStyle: {
                color: "#e5e5e5",
              },
            },
            data: this.roomData,
            axisLabel: {
              textStyle: {
                color: "rgba(0, 0, 0, 0.65)", // 刻度颜色
                fontSize: 14, // 刻度大小
              },
            },
          },
          series: [
            {
              type: "custom",
              clickable: false,
              renderItem: function (params, api) {
               // 开发者自定义的图形元素渲染逻辑,是通过书写 renderItem 函数实现的
                var categoryIndex = api.value(0).index; // 这里使用 api.value(0) 取出当前 dataItem 中第一个维度的数值。
                var start = api.coord([api.value(0).startTime, categoryIndex]); // 这里使用 api.coord(...) 将数值在当前坐标系中转换成为屏幕上的点的像素值。
                var end = api.coord([api.value(0).endTime, categoryIndex]);
                var height = 26;
                return {
                  type: "rect", // 表示这个图形元素是矩形。还可以是 'rect', 'circle', 'sector', 'polygon' 等等。
                  shape: echarts.graphic.clipRectByRect(
                    {
                      // 矩形的位置和大小。
                      x: start[0],
                      y: start[1] - height / 2,
                      width: end[0] - start[0],
                      height: 27,
                    },
                    {
                      // 当前坐标系的包围盒。
                      x: params.coordSys.x,
                      y: params.coordSys.y,
                      width: params.coordSys.width,
                      height: params.coordSys.height,
                    }
                  ),
                  style: api.style(),
                };
              },
              label: {
                normal: {
                  show: true,
                  position: "insideBottom",
                  formatter: function (params) {
                    //return params.value[0].content;
                    let value =  params.value[0].content;
                    if (!value) return "";
                    if (value.length > 6) {
                      return value.slice(0, 6) + "...";
                    }
                    return value;
                  },
                  textStyle: {
                    align: "center",
                    fontSize: 14,
                    fontWeight: "400",
                    lineHeight: "20",
                  },
                },
              },
              encode: {
                x: 1, // data 中『维度1』对应到 X 轴
                y: 0, // 把"维度0"映射到 Y 轴。
              },
  
              itemStyle: {
                normal: {
                  color: function (params) {
                    const randomIndex = Math.floor(Math.random() * colors.length);
                    return colors[randomIndex];
                  },
                },
              },
              // dataZoom: [
              //   {
              //     show: true,
              //     realtime: true,
              //     start: 0,
              //     end: 50
              //   }
              // ],
              data: this.ganttData,
            },
          ],
        };
        // 使用刚指定的配置项和数据显示图表。
        myChart.setOption(option);
        window.onresize = function () {
          myChart.resize();
        };
        // myChart.getZr().on("mousemove", (param) => {
        //   var pointInPixel = [param.offsetX, param.offsetY];
        //   if (myChart.containPixel("grid", pointInPixel)) {
        //     // 若鼠标滑过区域位置在当前图表范围内 鼠标设置为小手
        //     // myChart.getZr().setBackgroundColor('red')
        //     myChart.getZr().setCursorStyle("pointer");
        //   } else {
        //     myChart.getZr().setCursorStyle("default");
        //   }
        // });
        // 任意位置点击事件----注册双击
        // myChart.getZr().on("click", (params) => {
        //   if (!params.target) {
        //     // 点击在了空白处,做些什么。
        //     const point = [params.offsetX, params.offsetY];
        //     if (myChart.containPixel("grid", point)) {
        //       // 获取被点击的点在y轴上的索引
        //       const idxArr = myChart.convertFromPixel({ seriesIndex: 0 }, point);
        //       const xValue = new Date(+idxArr[0]).getHours();
        //       const yValue = idxArr[1];
        //       const sendData = [xValue, yValue];
        //       this.$emit("getInfoCallback", sendData);
        //     }
        //   }
        // });
        // // 图例点击事件-返回数据给父组件---单击事件
        // myChart.on("click", (params) => {
        //   this.$emit("getInfoCallback", params.data.value);
        // });
      },
      
    },
    computed: {},
  };
  </script>
  
  <style scoped lang="less">
  </style>
  1. js文件
import Gantt from './src/gantt'

Gantt.install = function (Vue) {
  Vue.component(Gantt.name, Gantt)
}
export default Gantt
  1. main.js中组件引用组件
    在这里插入图片描述
    在这里插入图片描述
  2. log页面
<template>
  <div class="appointment">
    <div class="a-gantt">
      <el-row style="padding: 12px 10px; background-color: #fff">
        <el-col :span="12" align="left" style="font-weight: 700">
          日志
        </el-col>
        <el-col :span="12" align="right">
          <el-date-picker
            v-model="baseDate"
            type="date"
            @change="handleSelect"
            placeholder="选择日期"
            value-format="yyyy-MM-dd"
          >
          </el-date-picker>
        </el-col>
      </el-row>
    </div>
    <div class="f-gantt">
      <Gantt
        :baseDate="baseDate"
        ref="gantt"
        :ganttData="ganttData"
        @getInfoCallback="getGanttInfo"
        :roomData="roomData"
      ></Gantt>
    </div>
    <!-- 新增编辑框 -->
    <!-- <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="30%">
      <el-form :model="form" :label-width="formLabelWidth">
        <el-form-item label="会议室">
          <el-input
            disabled
            v-model="usernameData[form.index]"
            autocomplete="off"
          ></el-input>
        </el-form-item>
        <el-form-item label="内容">
          <el-input v-model="form.content" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="时间" :label-width="formLabelWidth">
          <el-date-picker
            v-model="form.startTime"
            type="datetime"
            placeholder="选择日期"
            value-format="yyyy-MM-dd HH:mm"
          >
          </el-date-picker>
          -
          <el-date-picker
            v-model="form.endTime"
            type="datetime"
            placeholder="选择日期"
            value-format="yyyy-MM-dd HH:mm"
          >
          </el-date-picker>
        </el-form-item>
        <el-form-item label="状态" :label-width="formLabelWidth">
          <el-select v-model="form.status" style="width: 100%">
            <el-option label="进行中" value="0"></el-option>
            <el-option label="已完成" value="1"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="saveData">确 定</el-button>
      </span>
    </el-dialog> -->
  </div>
</template>

<script>


export default { 
  data() {
    return {
      baseDate: `${new Date().getFullYear()}-${
        new Date().getMonth() + 1
      }-${new Date().getDate()}`,
      roomData: [],
      ganttData: [],
      dialogVisible: false,
      formLabelWidth: "120px",
      formTitle: "",
      form: {
        id: "",
        index: "",
        content: "",
        endTime: "",
        status: "",
        startTime: "",
      },
    };
  },
  created() {
    this.getDataList()

    // this.ganttData = [
    //   {
    //     value: [
    //       {
    //         index: 1,
    //         roomName: "会议室二",
    //         RoomId: "123",
    //         id: "333",
    //         startTime: '2023-09-05 08:28', //`${this.baseDate} 8:28`,
    //         endTime: '2023-09-05 09:28', // `${this.baseDate} 9:28`,
    //         status: "1",
    //         content: "睡觉",
    //       }
    //     ],
    //   },
    //   {
    //     value: [
    //     {
    //         index: 1,
    //         roomName: "会议室二",
    //         RoomId: "123",
    //         id: "333",
    //         startTime: '2023-09-05 12:28', //`${this.baseDate} 8:28`,
    //         endTime: '2023-09-05 15:28', // `${this.baseDate} 9:28`,
    //         status: "1",
    //         content: "工作",
    //       },
    //     ]
    //   },
    //   {
    //     value: [
    //       {
    //         index: 0,
    //         roomName: "会议室一",
    //         RoomId: "2234",
    //         id: "444",
    //         startTime: `2023-09-05 10:28`,
    //         endTime: `2023-09-05 12:28`,
    //         status: "0",
    //         content: "吃饭",
    //       },
    //     ],
    //   },
    //   {
    //     value: [
    //     {
    //         index: 0,
    //         roomName: "会议室一",
    //         RoomId: "123",
    //         id: "333",
    //         startTime: '2023-09-05 13:28', //`${this.baseDate} 8:28`,
    //         endTime: '2023-09-05 15:28', // `${this.baseDate} 9:28`,
    //         status: "1",
    //         content: "工作111",
    //       },
    //     ]
    //   }
    // ];
    //this.roomData = ['会议室一', '会议室二', '会议室三', '会议室四'];
  },
  mounted() {
    this.getDataList();
  },
  methods: {
    handleSelect() {
      this.getDataList()
      this.$refs.gantt.myEcharts();
    },
    getDataList() {
      this.$http.post(`/sys/task/ganteLog`, {taskDate: this.baseDate}).then(({ data: res }) => {
          if (res.code !== 0) {
              return this.$message.error(res.msg)
          }
          this.roomData = res.data.usernameList
          this.ganttData = res.data.data
        }).catch(() => {})
    },
    getGanttInfo(data) {
      this.dialogVisible = true;
      // 根据data的长度判断是新增还是编辑
      // 新增
      if (data.length === 2) {
        this.formTitle = "新增";
        this.form = this.$options.data().form;
        this.$set(this.form, "index", data[1]);
      } else {
        this.formTitle = "修改";
        this.form = data[0];
      }
    },
    saveData() {
      if (this.formTitle === "修改") {
        this.ganttData = this.ganttData.filter((item) => {
          return item.value[0].id !== this.form.id;
        });
      } else {
        this.$set(this.form, "status", "0");
      }
      const obj = Object.assign({}, this.form);
      this.ganttData.push({ value: [obj] });
      this.$refs.gantt.myEcharts();
      this.dialogVisible = false;
    },
  },
};
</script>

<style scoped>
/* .a-gantt {
  position: absolute;
  top: 0;
  height: 60px;
  width: 100%;
  box-sizing: border-box;
  text-align: center;
} */

/* .appointment {
  position: relative;
  height: 100%;
  overflow-y: hidden;
  border: 1px solid #ddd;
  color: #0f1419;
  box-sizing: border-box;
} */
.f-gantt {
  position: absolute;
  bottom: 10px;
  top: 80px;
  width: 100%;
  /*height: 600px;*/
  box-sizing: border-box;
}
</style>

注意数据结构
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/983676.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lumion 和 Enscape 应该选择怎样的笔记本电脑?

Lumion 和 Enscape实时渲染对配置要求高&#xff0c;本地配置不够&#xff0c;如何快速解决&#xff1a; 本地普通电脑可一键申请高性能工作站&#xff0c;资产安全保障&#xff0c;供软件中心&#xff0c;各种软件插件一键获取&#xff0c;且即开即用&#xff0c;使用灵活&am…

电力4G变倍云台摄像头低功耗测试对比

4G变倍云台摄像头是一种智能化的视频监控摄像头设备。具有4G无线通信和无线网络摄像头的功能&#xff0c;同时还集成了变焦、变倍、云台等多种功能&#xff0c;适用于各种场景的视频监控。 以下是主要的特点和功能&#xff1a; 支持4G无线网络通信&#xff0c;远距离实时监控&…

1.3 BEV开源数据集介绍

本文来自自动驾驶之心知识星球的国内首个BEV感知全栈系列学习教程 文章目录 BEV开源数据集介绍&#xff1a;KITTIBEV开源数据集介绍&#xff1a;nuScenesBEV开源数据集介绍&#xff1a;Waymo BEV开源数据集介绍&#xff1a;KITTI 传感器位置 KITTI数据怎么采集&#xff1f; 通…

qt nodeeditor编译安装

目录 1. 下载源码 2. Qt creator编译源码 2.1 编译debug模式 &#xff08;MinGW&#xff09; 2.2 编译release模式 &#xff08;MinGW&#xff09; 1. 下载源码 https://github.com/paceholder/nodeeditor/archive/refs/tags/3.0.10.zip 2. Qt creator编译源码 解压文件…

面试中的身体语言:非语言信息的重要性

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

如何区分异动电动机和同步电动机

在日常的工作生活中&#xff0c;会遇到很多种不同类型的电动机&#xff0c;比如直流电机、步行电机和伺服电机等等。其中交流电机分为异动电动机和同步电动机两种&#xff0c;那么同步电动机和异步电动机到底有什么区别呢&#xff1f; 转速 同步电动机定子绕组三相电流所产生…

DragGAN应运而生,未来在4G视频上都可能利用拖拽式编辑

原创 | 文 BFT机器人 2023年8月14日-15日&#xff0c;第七届GAIR全球人工智能与机器人大会在新加坡乌节大酒店成功举办。 在「AIGC 和生成式内容」分论坛上&#xff0c;南洋理工大学科学与工程学院助理教授潘新钢以《Interacitve Point-Dragging Manipulation of Visual Cont…

kubernetes集群安装详细步骤

kubernetes集群安装详细步骤&#xff08;V1.20.6&#xff09; 本篇主要介绍kubernetes的1.20.6版本集群安装&#xff0c;废话不多说&#xff0c;直接看步骤&#xff1a; 1、安装环境介绍 主机节点&#xff1a; 主机操作系统&#xff1a;Centos7.9 配置&#xff1a; 内存建议…

引入Bootstrap的CSS样式后,<h>标签、<p>标签等HTML自带的标签被覆写没有?答:覆写了。

引入Bootstrap的CSS样式后,标签、 标签等HTML自带的标签被覆写没有&#xff1f;答&#xff1a;覆写了。 为什么这么说&#xff1f;证据呢&#xff1f; 写一个实例&#xff0c;然后调试模式看一下不就得了。 先看没有引入引入Bootstrap的CSS样式情况。 代码如下&#xff1a; …

二分查找实例1(在排序数组中查找元素的第一个和最后一个位置)

题目 给你一个按照非递减顺序排列的整数数组 nums&#xff0c;和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target&#xff0c;返回 [-1, -1]。 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。 示例 1&…

深入学习与探索:高级数据结构与复杂算法

文章目录 学习高级数据结构B树&#xff1a;数据库引擎的骨干线段树&#xff1a;高效的区间查询Trie树&#xff1a;高效的字符串检索 探索复杂算法领域图算法&#xff1a;解决复杂网络问题字符串匹配算法&#xff1a;处理文本搜索近似算法&#xff1a;在NP难题上取得近似解 结论…

聊聊Kafka的生产者消费者确认机制

一、生产者确认机制 消息从生产者客户端发送至broker服务端topic&#xff0c;需要ack确认。acks与min.insync.replicas是两个配置参数.其中acks是producer的配置参数&#xff0c;min.insync.replicas是Broker端的配置参数&#xff0c;这两个参数对于生产者不丢失数据起到了很大…

PMP证书续费是否真的有必要呢?(内附续证流程)

PMP项目管理专业人士资格认证是由项目管理协会&#xff08;Project Management Institute&#xff0c;简称PMI&#xff09;发起的。PMP作为世界级的项目管理认证证书&#xff0c;拥有着先进的项目管理知识体系&#xff0c;它严格评估项目管理人员知识技能是否具有高品质的资格认…

Android图片一直在另一张图的下边

因为之前开发的时候&#xff0c;头像设置了高度属性android:elevation"2px",导致同一父布局中另一张图一直就是显示在下方&#xff0c;如下图&#xff1a; 方法一&#xff1a;大家可以注意下也加上这个属性&#xff0c;这个属性值大于上边这个图的值就能在这张图的上…

KubeSphere Namespace 数据删除事故分析与解决全记录

作者&#xff1a;宇轩辞白&#xff0c;运维研发工程师&#xff0c;目前专注于云原生、Kubernetes、容器、Linux、运维自动化等领域。 前言 2023 年 7 月 23 日在项目上线前夕&#xff0c;K8s 生产环境出现故障&#xff0c;经过紧急修复之后&#xff0c;K8s 环境恢复正常&#…

nodejs-处理http请求

文章目录 前言node 处理 get 请求node 处理 post 请求总结 前言 使用nodejs搭建后端代理服务&#xff0c;处理http请求&#xff0c;理解nodejs是如何处理get、post请求的 node 处理 get 请求 使用 http 模块创建代理服务器使用 querystring 模块解析请求参数req.end 方法发送…

UOS系统下fastdeploy推理

Cmake安装 apt install build-essential zlib1g-dev libssl-dev wget https://github.com/Kitware/CMake/releases/download/v3.23.2/cmake-3.23.2.tar.gz tar -zxvf cmake-3.23.2.tar.gz cd cmake-3.23.2 ./bootstrap make make install cmake --version在Github或者gitee 查…

IDEA中启动类是灰色,重启idea启动类自动消失解决方法

问题描述&#xff1a; idea中启动多个服务会在services中展示服务的信息和控制台&#xff0c;但是经常有一些启动类会变成灰色的&#xff0c;而且重启idea后经常会自动消失&#xff0c;下次启动时需要手动再去启动&#xff0c;很麻烦。如下图所示&#xff1a; 解决方法&…

智能配电管理系统

智能配电管理系统是按用户的需求&#xff0c;遵循配电系统的标准规范而二次开发的一套具有专业性强、自动化程度高、易使用、高性能、高可靠等特点的适用于低压配电系统的电能管理系统。 智能配电管理系统包括监控管理层、网络通信层、现场采集层、用电保护层和受控设备层&…

基于javaweb的网上图书销售系统(servlet+jsp)

系统简介 本项目采用eclipse工具开发&#xff0c;jspservletjquery技术编写&#xff0c;数据库采用的是mysql&#xff0c;navicat开发工具。 角色&#xff1a; 管理员普通用户 模块简介 管理员&#xff1a; 登录用户管理图书分类管理图书管理图书订单管理图书评论管理数据统…