2022-05-28  2022-05-28    10700 字   22 分钟

第四章

优学题库项目开发2

优就业.JAVA教研室

学习目标

  • 了解启动开源管理前端框架
  • 掌握开发配置题目分类服务及前端维护管理功能
  • 掌握开发配置题目维护后端服务及前端维护管理功能
  • 掌握完成题目导入、导出功能开发
  • 掌握Echarts图表入门
  • 掌握统计分析功能开发

1、启动人人开源管理前端

1.1、确认本地已经安装好Node.js

输入指令,确认:

node -v

node.js安装教程

https://www.runoob.com/nodejs/nodejs-install-setup.html

1.2、安装前端开发工具VSCode

下载前端代码:

renren-fast-vue 管理后台前端 https://gitee.com/hk109/renren-fast-vue.git

1.2.1 打开人人开源管理前端代码所在目录

1.2.2 选中人人开源前端代码的目录,

1.2.3打开目录效果。

1.2.4保存工作区:

1.2.5选中工作区保存位置

1.2.6 最终保存为工作区

1.2.7、禁用eslint插件

1.3、启动前端项目

1.3.1 进入VSCODE终端

终端界面

npm -v

1.3.2 配置cnpm

npm install -g cnpm --registry=https://registry.npm.taobao.org

1.3.3 安装node_modules依赖包

cnpm install

注意安装的饿时候,需要修改package.json,里面的node-sass的依赖版本号:^4.14.1

1.3.4 打包运行前端项目

npm run dev

1.3.5 浏览器访问

注意:renren-fast项目要先启动 
浏览后台:http://localhost:8001

登录后台

账号:admin

密码:admin

1.4、配置前端使用API网关-请求后端API接口调用地址

修改配置文件对接后端api地址:

文件:renren-fast-vue\static\config\index.js

api接口请求地址替换为题目微服务的地址

window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
替换为
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8888/api'; // 网关地址

配置cloud-gateway转发到renren-fast的路由:

spring:
  cloud:    
    gateway:
      routes:        
        - id: renrenfast-route # 人人后台服务路由
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

刷新登录页面,可以正常获取验证码,请求路径为网关地址 + /api/captcha.jpg?uuid=xxxxx

http://localhost:8888/api/captcha.jpg?uuid=84d36089-07ae-4201-85c0-8217b032f23e

1.5、登录跨域问题

点击登录,控制台报错,登录失败:

Access to XMLHttpRequest at 'http://localhost:8888/api/sys/login' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' head is present on the requested resource

登录页面url:http://localhost:8001,点击登录访问的请求url:http://localhost:8888/api/sys/login,两个url的端口号不一样,产生了跨域问题。

什么是跨域?

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

比如,站点 http://domain-a.com 的某 HTML 页面通过 的 src 请求 http://domain-b.com/image.jpg。网络上的许多页面都会加载来自不同域的CSS样式表,图像和脚本等资源。

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。

1.6、解决跨域问题

添加响应头,配置当次请求允许跨域

Access-Control-Allow-Origin:支持哪些来源的请求跨域 Access-Control-Allow-Methods:支持哪些方法跨域 Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含cookie Access-Control-Expose-Headers:跨域请求暴露的字段CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。 Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无 须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果 该首部字段的值超过了最大有效时间,将不会生效。

1.6.1 添加跨域配置

在cloud-gateway模块添加解决跨域的配置文件GatewayCorsConfiguration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;

import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class GatewayCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();

        // 配置跨域
        corsConfiguration.addAllowedHeader("*"); // 允许所有请求头跨域
        corsConfiguration.addAllowedMethod("*"); // 允许所有请求方法跨域
        corsConfiguration.addAllowedOrigin("*"); // 允许所有请求来源跨域
        corsConfiguration.setAllowCredentials(true); //允许携带cookie跨域,否则跨域请求会丢失cookie信息

        source.registerCorsConfiguration("/**", corsConfiguration);

        return new CorsWebFilter(source);
    }
}

也可以采用配置方式,修改application.yml:

spring: 
  cloud:    
    gateway:
      globalcors:   #跨域配置
        cors-configurations:
          '[/**]':
            allowCredentials: true #允许跨域读取cookie
            allowedOrigins: "*"  #允许发出跨域请求主机域名 * 任意主机
            allowedMethods: "*"  #请求方法
            allowedHeaders: "*"  #允许发出跨域的请求头

1.6.2、注释renren-fast跨域配置

注释renren-fast里面的跨域配置

文件路径:src/main/java/io/renren/config/CorsConfig.java

重启各个服务及网关,测试即可成功登录。

1.6.3 前后端联调登录

登录成功

查看后端服务日志

说明前端登录请求发送到了后端服务,并验证了用户名和密码是否正确。

2、开发配置题目分类服务前端维护管理功能

2.1、添加题目管理目录(一级菜单)

选择管理界面–系统管理–菜单管理,点击 新增按钮

刷新页面,就可以看到题目中心菜单

2.2、添加题目类型维护菜单(二级菜单)

选择管理界面–系统管理–菜单管理,点击 新增按钮

选择类型:菜单

刷新,可以看到菜单

可以看到数据库uxeu_admin的表sys_menu中新增了两条记录,分别对应两个菜单

点击类型维护菜单,打开了链接:http://localhost:8001/#/question-type,页面显示空白页面.

2.3、自动生成类型维护前端页面

用renren-generator自动生成前端代码

注意配置数据库连接文件application.yml,指向数据库 uxue_qms

修改模板生成属性配置文件generator.properties

配置模块名和包名:

重启服务,打开生成页面选择表,点击生成

2.4、拷贝前端代码到 前端工程renren-fast-vue

拷贝\main\resources\src\views\modules\question目录到前端目录 renren-fast-vue\src\views\modules

2.5、测试类型维护

点击类型维护菜单,可以看到请求报404

http://localhost:8001/#/question-type

因为页面的请求都访问到renren-fast服务了,所以要修改为访问题目微服务。

修改colud-gateway网关配置,配置转发题目微服务:

(注意:qms-route的路由配置一定要配置在renrenfast-route之前,拥有加载顺序。)

spring:
  cloud:
    nacos:
      discovery:
        server-addr: http://localhost:8848
    gateway:
      routes:
        - id: qms-route # 题目微服务路由
          uri: lb://u-question
          predicates:
            - Path=/api/question/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
        - id: renrenfast-route # 人人后台服务路由
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

重启网关服务,再次刷新访问地址:http://localhost:8001/#/question-type

即可正常访问题目类型列表页:

  • 数据库插入3条测试数据
  • 测试查询列表,可以看到有三条记录查询出来了

点击 修改:

点击 确定 即可完成修改。

点击删除即可删除记录

弹出删除确认提示:

确定即可删除对应记录

2.6、开启类型维护新增、批量删除权限

打开前端工程renren-fast-vue找到配置文件src\utils\index.js

/**
 * 是否有权限
 * @param {*} key
 */
export function isAuth (key) {
  // return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
  return true
}

暂时不判断权限,全部返回true

再次访问类型维护列表页,即可看到新增按钮

去除新增创建时间、更新时间必须输入限制:

修改文件src/views/modules/question/type-add-or-update.vue

 dataRule: {
          type: [
            { required: true, message: '类型名称不能为空', trigger: 'blur' }
          ],
          comments: [
            { required: true, message: '备注不能为空', trigger: 'blur' }
          ],
          logoUrl: [
            { required: true, message: '类型logo路径不能为空', trigger: 'blur' }
          ],
          delFlag: [
            { required: true, message: '删除标记(0-正常,1-删除)不能为空', trigger: 'blur' }
          ],
          createTime: [
            { required: false, message: '创建时间不能为空', trigger: 'blur' }
          ],
          updateTime: [
            { required: false, message: '更新时间不能为空', trigger: 'blur' }
          ]
        }

把createTime、updateTime required: true改成 required: false

点击新增按钮,即可弹出新增界面

批量删除测试

2.7、配置逻辑删除

前面进行的删除都是物理删除,页可以进行逻辑删除,就是不直接把数据记录从数据库删除,而是采用设置删除标记来标识数据记录被删除。

2.7.1、所有表字段添加del_flag字段

字段属性

del_flag tinyint(1) DEFAULT 0 COMMENT '删除标记(0-正常,1-删除)',

2.7.2、修改MyBatisPlus配置逻辑删除

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1  # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
      logic-delete-field: delFlag #全局逻辑删除字段值 3.3.0开始支持,详情看下面。

2.7.3、配置springboot打印mybatis执行sql

修改配置文件application.yml

logging:
  level:
    com.offcn.question: debug

再次测试访问列表和执行删除,可以看到控制台打印sql指令

查询指令:

SELECT id,type,comments,logo_url,del_flag,create_time,update_time FROM qms_type WHERE del_flag=0

删除指令:

UPDATE qms_type SET del_flag=1 WHERE id IN ( 1 ) AND del_flag=0 

查看数据库记录:

2.8、模糊查询类型功能

修改实现类TypeServiceImpl的方法queryPage

 @Override
    public PageUtils queryPage(Map<String, Object> params) {
        //1、获取查询关键字
     String key= (String) params.get("key");
     //2、创建查询条件对象
        QueryWrapper<TypeEntity> queryWrapper = new QueryWrapper<>();
        //3、设置查询条件
       if(!StringUtils.isEmpty(key)){
           queryWrapper.eq("id",key).or().like("type",key);

       }
        IPage<TypeEntity> page = this.page(
                new Query<TypeEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

设置id或者类型名称模糊查询的条件

前端访问测试:

2.9、配置分页

修改u-question配置分页插件配置类:

@Configuration
public class MyBatisConfig {
    //引入分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInterceptor.setLimit(1000);
        return paginationInterceptor;
    }
}

添加分页插件后的显示:

3、开发配置题目前端维护管理功能

3.1、添加题目维护菜单(二级菜单)

刷新后台管理页面,就可以看到题目维护菜单

3.2、拷贝代码生成器生成的题目维护的前端源码

拷贝题目维护源码到前端工程renren-fast-vue

3.3、测试题目维护

访问题目维护菜单,可以正常浏览到题目数据。

3.4、新增题目–处理题目类型下拉菜单

打开新增题目,发现里面涉及到题目类型,需要下拉选择已经存在的题目类型。

3.4.1.获取根据分类获取题库列表数据接口

3.4.1.1、编写获取题库分类接口及实现类

接口:TypeService 增加获取全部分类方法

public interface TypeService extends IService<TypeEntity> {

     //获取全部分类
    List<TypeEntity> findAll();
}

实现类:TypeServiceImpl

@Service("typeService")
public class TypeServiceImpl extends ServiceImpl<TypeDao, TypeEntity> implements TypeService {

      @Override
    public List<TypeEntity> findAll() {
        return this.list();
    }
}

3.4.1.2、编写获取题库分类控制器

修改TypeController,增加获取全部分类接口

@RestController
@RequestMapping("question/type")
public class TypeController {
    @Autowired
    private TypeService typeService;

    //获取全部分类
    @GetMapping("findall")
    public R findAll(){

        List<TypeEntity> all = typeService.findAll();

        return R.ok().put("data",all);
    }

}

3.4.2、前端代码编写

修改question-add-or-update.vue

修改数据结构,定义题目类型下拉菜单数据

export default {
    data () {
      return {       
        options: [],

修改方法,当用户点击打开新增、或者修改页面的时候,调用后台接口获取全部题目类型数据

 methods: {
      init (id) {
        this.dataForm.id = id || 0
        this.visible = true
        this.$nextTick(() => {
          this.$refs['dataForm'].resetFields()
           //向服务器端发出一个请求,获取全部题目分类数据
          this.$http({
            url: this.$http.adornUrl(`/question/type/findall`),
            method: 'get'           
          }).then(({data}) => {
           this.options = data.data
          })
 <el-form-item label="题目类型" prop="type">
      <el-select v-model="dataForm.type" placeholder="请选择">
       <el-option
         v-for="item in options"
         :key="item.id"
         :label="item.type"
         :value="item.id">
       </el-option>
      </el-select>
    </el-form-item>

点击新增、或者修改,可以看到题目类型下拉菜单成功显示

3.5、新增题目–处理是否显示、是否删除

修改页面question-add-or-update.vue

<el-form-item label="是否显示" prop="enable">
      <el-radio v-model="dataForm.enable" label="0">不显示</el-radio>
      <el-radio v-model="dataForm.enable" label="1">显示</el-radio>
    </el-form-item>
<el-form-item label="删除标记" prop="delFlag">
      <el-radio v-model="dataForm.delFlag" label="0">不删除</el-radio>
      <el-radio v-model="dataForm.delFlag" label="1" >删除</el-radio>
</el-form-item>

修改数据结构,定义是否显示初始化值

修改从后台接口获取数据赋值方法,把后台响应数据转换为字符串

关键点:data.question.enable + ''

​ data.question.delFlag + ''

点击新增或者修改,可以看到是否显示、是否删除成功已单选框显示

3.6、模糊查询题目

修改实现类QuestionServiceImpl的方法queryPage

@Override
    public PageUtils queryPage(Map<String, Object> params) {
        //1、获取查询关键字
        String key= (String) params.get("key");
        //2、创建查询条件对象
        QueryWrapper<QuestionEntity> queryWrapper = new QueryWrapper<>();
        //3、设置查询条件
        if(!StringUtils.isEmpty(key)){
            queryWrapper.eq("id",key).or().like("title",key);

        }
        IPage<QuestionEntity> page = this.page(
                new Query<QuestionEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

测试输入id或者标题包含的关键字查询

3.7、快速设置是否显示

想要将是否显示改为快速开关

修改页面question.vue添加更新状态方法

    methods: {
      // 更新题目是否显示
      updateQuestionStatus (data) {
          console.log(data)
          let {id, enable} = data
        this.$http({
           url: this.$http.adornUrl('/question/question/update'),
           method: 'post',
           data: this.$http.adornData({id, enable}, false)
      }).then(({ data }) => {
        this.$message({
            type:'success',
            message: '状态更新成功'
         })
     });
   },

修改页面question.vue

      <el-table-column
        prop="enable"
        header-align="center"
        align="center"
        label="是否显示">
        <template slot-scope="scope">
         <el-switch
           v-model="scope.row.enable"
           :active-value=1
           :inactive-value=0
           active-color="#13ce66"
           inactive-color="#ff4949"
           @change="updateQuestionStatus(scope.row)">
        </el-switch>
        </template>
      </el-table-column>

测试页面效果

3.8、校验排序不能是小数或者小于0的数字

修改前端页面question-add-or-update.vue

 var validateOrder = (rule, value, callback) => {
        if (value === '') {
          callback(new Error('请输入排序'));
        } else {
          if (!(value%1 === 0)||(value<0)) {
           callback(new Error('排序应该是整数且大于0'));
          }
          callback();
        }
      };
return {
	…………略
 dataRule: {
    …………
    displayOrder: [
            { validator: validateOrder, trigger: 'blur' }
          ],
    …………
 },
}

测试

只有输入整数才能验证通过。

3.9、修改题目答案为多行文本框

<el-form-item label="题目解答" prop="answer">
      <el-input  type="textarea" :rows="2" v-model="dataForm.answer" placeholder="题目解答"></el-input>
</el-form-item>

效果:

4、完成题目导入、导出功能

4.1、JAVA操作excel类库POI

12.1.1 什么是POI?

Apache POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。

· HSSF - 提供读写Microsoft Excel格式档案的功能。(.xls)

· XSSF - 提供读写Microsoft Excel OOXML格式档案的功能。(.xlsx)

· HWPF - 提供读写Microsoft Word格式档案的功能。

· HSLF - 提供读写Microsoft PowerPoint格式档案的功能。

· HDGF - 提供读写Microsoft Visio格式档案的功能。

12.1.2 POI官网

http://poi.apache.org/

官网可以找到文档和每个版本的下载地址

4.2、POI入门demo

4.2.1.创建一个普通的maven项目

项目名:excel-poi

4.2.2.pom中引入xml相关依赖

<dependencies>
    <!--xls-->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.9</version>
    </dependency>

    <!--xlsx-->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.9</version>
    </dependency>

    
    <!--test-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

4.2.3.java写入excel

2003版写文件

package com.excel.poi;

public class ExcelTest {


    @Test
    public void testWrite03() throws IOException {

        // 创建新的Excel 工作簿
        Workbook workbook = new HSSFWorkbook();

        // 在Excel工作簿中建一工作表,其名为缺省值 Sheet0
        //Sheet sheet = workbook.createSheet();

        // 如要新建一名为"会员登录统计"的工作表,其语句为:
        Sheet sheet = workbook.createSheet("会员登录统计");

        // 创建行(row 1)
        Row row1 = sheet.createRow(0);

        // 创建单元格(col 1-1)
        Cell cell11 = row1.createCell(0);
        cell11.setCellValue("今日人数");

        // 创建单元格(col 1-2)
        Cell cell12 = row1.createCell(1);
        cell12.setCellValue(666);

        // 创建行(row 2)
        Row row2 = sheet.createRow(1);

        // 创建单元格(col 2-1)
        Cell cell21 = row2.createCell(0);
        cell21.setCellValue("统计时间");

        //创建单元格(第三列)
        Cell cell22 = row2.createCell(1);
        String dateTime = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
        cell22.setCellValue(dateTime);

        // 新建一输出文件流(注意:要先创建文件夹)
        FileOutputStream out = new FileOutputStream("d:/excel-poi/test-write03.xls");
        // 把相应的Excel 工作簿存盘
        workbook.write(out);
        // 操作结束,关闭文件
        out.close();

        System.out.println("文件生成成功");
    }
}

2007版写文件

@Test
public void testWrite07() throws IOException {


    // 创建新的Excel 工作簿
    Workbook workbook = new XSSFWorkbook();


    // 在Excel工作簿中建一工作表,其名为缺省值 Sheet0
    //Sheet sheet = workbook.createSheet();

    // 如要新建一名为"会员登录统计"的工作表,其语句为:
    Sheet sheet = workbook.createSheet("会员登录统计");

    // 创建行(row 1)
    Row row1 = sheet.createRow(0);

    // 创建单元格(col 1-1)
    Cell cell11 = row1.createCell(0);
    cell11.setCellValue("今日人数");

    // 创建单元格(col 1-2)
    Cell cell12 = row1.createCell(1);
    cell12.setCellValue(666);

    // 创建行(row 2)
    Row row2 = sheet.createRow(1);

    // 创建单元格(col 2-1)
    Cell cell21 = row2.createCell(0);
    cell21.setCellValue("统计时间");

    //创建单元格(第三列)
    Cell cell22 = row2.createCell(1);
    String dateTime = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
    cell22.setCellValue(dateTime);

    // 新建一输出文件流(注意:要先创建文件夹)
    FileOutputStream out = new FileOutputStream("d:/excel-poi/test-write07.xlsx");
    // 把相应的Excel 工作簿存盘
    workbook.write(out);
    // 操作结束,关闭文件
    out.close();

    System.out.println("文件生成成功");
}

注意:如果针对不同版本的Excel使用了不适合的类库,则会报告异常 org.apache.poi.POIXMLException: org.apache.poi.openxml4j.exceptions.InvalidFormatException: Package should contain a content type part [M1.13]

4.2.4.Java读取excel

2003版读文件

package com.excel.poi;

public class ExcelReadTest {
    @Test
    public void testRead03() throws Exception{

        InputStream is = new FileInputStream("d:/excel-poi/test-write03.xls");
        Workbook workbook = new HSSFWorkbook(is);
        Sheet sheet = workbook.getSheetAt(0);
        // 读取第一行第一列
        Row row = sheet.getRow(0);
        Cell cell = row.getCell(0);
        // 输出单元内容
        System.out.println(cell.getStringCellValue());
        // 操作结束,关闭文件
        is.close();
    }

}

2007版读文件

@Test
public void testRead07() throws Exception{

    InputStream is = new FileInputStream("d:/excel-poi/test-write07.xlsx");

    Workbook workbook = new XSSFWorkbook(is);
    Sheet sheet = workbook.getSheetAt(0);

    // 读取第一行第一列
    Row row = sheet.getRow(0);
    Cell cell = row.getCell(0);

    // 输出单元内容
    System.out.println(cell.getStringCellValue());

    // 操作结束,关闭文件
    is.close();
}

4.3、JAVA后端代码编写

4.3.1修改项目u-question的接口QuestionService增加导入、导出接口定义

public interface QuestionService extends IService<QuestionEntity> {

    PageUtils queryPage(Map<String, Object> params);
    //导入
    public Map importExcel(MultipartFile file);
   //导出
    public Workbook exportExcel();
}

4.3.2 修改实现类QuestionServiceImpl实现导入、导出方法

  @Override
    public Map importExcel(MultipartFile file) {
        Map result=new HashMap();

        //获取上传文件名称
        String filename = file.getOriginalFilename();
         String extName = filename.substring(filename.lastIndexOf(".")+1);
        //2、判断文件扩展名是否等于xlsx,xls
        if(!extName.equals("xls") &&!extName.equals("xlsx") ){
            result.put("result",false);
            result.put("msg","文件扩展名不正确,导入失败");
            result.put("num",0);
        }

       try {
            InputStream inputStream = file.getInputStream();
            //初始化创建工作簿对象
            Workbook workbook = WorkbookFactory.create(inputStream);
            //获取第一张工作表
            Sheet sheet = workbook.getSheetAt(0);
            //获取工作表里面有数据行数
            int rows = sheet.getPhysicalNumberOfRows();
            //循环表格行
            for (int i = 1; i <rows ; i++) {
                QuestionEntity questionEntity = new QuestionEntity();
                //获取当前行
                Row row = sheet.getRow(i);
                //获取第一个单元格的内容 标题
              String title=  row.getCell(0).getStringCellValue();
              //设置到题目对象标题属性
                questionEntity.setTitle(title);
                System.out.println("title:"+title);
                //获取第二个单元格的内容 答案
                String answer = row.getCell(1).getStringCellValue();
                questionEntity.setAnswer(answer);
                System.out.println("answer:"+answer);
                //获取第三个单元格的内容 题目难度等级
                double level = row.getCell(2).getNumericCellValue();
                questionEntity.setLevel((int)level);

                //获取第四个单元格的内容 paix
                questionEntity.setDisplayOrder((int)row.getCell(3).getNumericCellValue());

                //获取副标题
                questionEntity.setSubTitle(row.getCell(4).getStringCellValue());
                //题目类型
                questionEntity.setType((long)row.getCell(5).getNumericCellValue());
                //是否显示
                questionEntity.setEnable((int)row.getCell(6).getNumericCellValue());

                //调用数据库保存方法,保存题目数据到书籍
                this.save(questionEntity);
            }

            result.put("result",true);
            result.put("msg","导入成功");
            System.out.println("导入成功总条数:"+list.size());
        } catch (Exception e) {
            e.printStackTrace();
            result.put("result",false);
            result.put("msg","文件导入失败,"+e.getMessage());
            result.put("num",0);
        }


        return result;
    }


    @Override
    public Workbook exportExcel() {
        // 创建新的Excel 工作簿
        Workbook workbook = new HSSFWorkbook();

        // 在Excel工作簿中建一工作表,其名为缺省值 Sheet0
        //Sheet sheet = workbook.createSheet();

        // 创建工作表
        Sheet sheet = workbook.createSheet("题库");

        Row row = sheet.createRow(0);
        row.createCell(0).setCellValue("题目标题");
        row.createCell(1).setCellValue("题目解答");
        row.createCell(2).setCellValue("题目难度等级");
        row.createCell(3).setCellValue("排序");
        row.createCell(4).setCellValue("副标题");
        row.createCell(5).setCellValue("题目类型");
        row.createCell(6).setCellValue("是否显示");
        List<QuestionEntity> list = this.list();
        for (int i = 0; i < list.size(); i++) {
            Row row2 = sheet.createRow(i + 1);
            //创建表格
            row2.createCell(0).setCellValue(list.get(i).getTitle());
            row2.createCell(1).setCellValue(list.get(i).getAnswer());
            row2.createCell(2).setCellValue(list.get(i).getLevel());
            row2.createCell(3).setCellValue(list.get(i).getDisplayOrder());
            row2.createCell(4).setCellValue(list.get(i).getSubTitle());
            row2.createCell(5).setCellValue(list.get(i).getType());
            row2.createCell(6).setCellValue(list.get(i).getEnable());

        }

        return workbook;
    }

4.3.3 修改控制器QuestionController实现导入、导出方法

//题目上传导入
    @PostMapping("/upload")
    public R upload(MultipartFile file){
        Map result = questionService.importExcel(file);
        return R.ok().put("result",result.get("result")).put("msg",result.get("msg")).put("num",result.get("num"));
    }

//导出excel
    @GetMapping("exportExcel")
    public void export(String tableName, HttpServletResponse response){
        System.out.println("导出excele");
        Workbook workbook = questionService.exportExcel();

        if (workbook != null) {
            String fileName = "uxue_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setContentType("application/octet-stream;charset=UTF-8");
            OutputStream outputStream;
            try {
                outputStream = response.getOutputStream();
                workbook.write(outputStream);
                outputStream.flush();
                outputStream.close();
            } catch (Exception e) {
               e.printStackTrace();
            }
        } else {
            try {
                response.getWriter().print("error");
            } catch (Exception e) {
               e.printStackTrace();
            }
        }


    }

4.4、前端代码编写

4.4.1 创建导入弹窗口视图页面

在renren-fast-vue前端工程/src/views/modules/question/ 目录下新增视图文件 question-import.vue

<template>
<el-dialog
    :title="'导入题库'"
    :close-on-click-modal="false"
    :visible.sync="visible" >
  <el-row>
    <div class="el-form-item__content">
        <el-upload ref="upload"
            accept=".xls,.xlsx"
            action="#"
            :auto-upload="false"
            :multiple="false"
            :file-list="fileList"           
            :http-request="uploadHttpRequest"
            >
            <el-button size="small" type="primary">选择文件</el-button>
            <div slot="tip" class="el-upload__tip">一次只能上传一个xls/xlsx文件且不超过10M</div>
        </el-upload>        
    <el-button type="primary" size="small" @click="submitUpload"> </el-button>
    <a type="primary" href="/test.xls" size="small" >下载导入模板</a>
    </div>   
</el-row>
</el-dialog>
</template>

<script>
  export default {
    data () {
      return {
        visible: false,    
        fileList: [],        
        }
    },
    methods: {     
      init () {
        this.visible = true
      },   
    // param是默认参数,可以取得file文件信息,具体信息如下图
    uploadHttpRequest(param) {
        const formData = new FormData() //FormData对象,添加参数只能通过append('key', value)的形式添加
        formData.append('file', param.file) //添加文件对象
        formData.append('uploadType', this.rulesType)        
        this.$http({
            url: this.$http.adornUrl(`/question/question/upload`),
            method: 'post',
            data: formData,
            contentType: 'form'
          }).then( res => {
                const { data: { code, mark, num, msg } } = res
                if(code === 0) {
                    this.fileList = []
                    this.visible = false
                  //  param.onSuccess()  // 上传成功的文件显示绿色的对勾
                  //  this.uploadMark = mark
                  //this.$message.info('上传文件成功,导入条数:'+num)
                  this.$message({
                  message: '上传文件成功,导入条数:'+num,
                  type: 'success',
                  duration: 1000,
                  onClose: () => {
                    this.visible = false
                    this.$emit('refreshDataList')
                  }
                })
                }else {
                    this.$message.error(msg)
                }
              
            })            
            .catch( err => {
                console.log('失败', err)
                param.onError() //上传失败的文件会从文件列表中删除
            })
    },    
    // 点击上传:手动上传到服务器,此时会触发组件的http-request
    submitUpload() {
        this.$refs.upload.submit()
    }    
    
    }
  }
</script>

4.4.2 修改题目维护页面question.vue

引入导入页面:

import ImportExcel from './question-import'

注册导入窗口:

<!-- 导入窗口-->
<ImportExcel v-if="importVisible" ref="importExcel" @refreshDataList="getDataList"></ImportExcel>

声明允许显示导入弹窗变量

data () {
      return {
      //是否允许显示导入窗口
        importVisible: false
      }
    },

把导入页面注册到当前页面组件:

components: {
      AddOrUpdate,
      ImportExcel
    },

添加导入导出点击触发方法:

//导入
      importExcelHandle () {
        this.importVisible = true
        this.$nextTick(() => {
          this.$refs.importExcel.init()
        })        
      },
      //导出
      exportExcelHandle () {
        var dom=document.getElementById('ifile')
        dom.src= this.$http.adornUrl('/question/question/exportExcel')
      },

在页面模板区域,添加 导入、导出按钮:

<el-button type="primary" @click="importExcelHandle()">导入</el-button>
<el-button type="success" @click="exportExcelHandle()">导出</el-button>
<iframe id="ifile" style="display:none"></iframe>

导入、导出视图显示效果:

点击 导入 按钮,弹出导入弹窗:

下载导入模板,按照模板,填写数据,选中文件,点击上传,即可导入

导入模板样式:

导入成功,提示导入条数,关闭导入弹窗。

5、Echarts图表入门

5.1.Echarts简介

ECahrs是百度的一个项目,用于图表展示,提供了常规的折线图、柱状图、散点图、饼图、K线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。 官方网站:https://echarts.apache.org/zh/index.html

5.2.基本使用

入门参考:官网->文档->教程->5分钟上手ECharts

5.2.1、柱状图创建

(1)创建html页面:柱图.html

(2)引入ECharts

<!-- 引入 ECharts 文件 -->
<script src="echarts.min.js"></script>

(3)定义图表区域

<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px;height:400px;"></div> 

(4)渲染图表

<script>
    var myChart = echarts.init(document.getElementById('main'));
    var option = {
        title: {
            text: 'Echarts 柱状图入门demo'
        },
        tootip: {},
        legend: {
            data: ['销量']
        },
        xAxis: {
            data: ["衬衫","羊毛衫","羽绒服","裤子","高跟鞋"]
        },
        yAxis: {},
        series: [{
            name: '销量',
            type: 'bar',
            data: [1,6,7,4,15]
        }]
    };

    myChart.setOption(option);
</script>

测试效果:

5.2.2、折线图创建

<script>
    var myChart = echarts.init(document.getElementById('main'));
    var option = {
        title: {
            text: 'Echarts 折线图入门demo'
        },
        tootip: {},
        legend: {
            data: ['销量']
        },
        xAxis: {
            type: 'category',
            data: ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]
        },
        yAxis: {
            type: 'value'
        },
        series: [{
            name: '销量',
            type: 'line',
            data: [820, 932, 901, 934, 1290, 1330, 1320]
        }]
    };

    myChart.setOption(option);
</script>

6、统计分析

6.1.题库分类数量统计分析

6.1.1 编写java后端代码

打开java模块u-question ,编辑接口QuestionService新增统计分类题目数量方法

public interface QuestionService extends IService<QuestionEntity> {

     public List<Map<String, Object>> countTypeQuestion();
}

编辑接口实现类QuestionServiceImpl,编写统计分类题目数量实现方法

@Service("questionService")
public class QuestionServiceImpl extends ServiceImpl<QuestionDao, QuestionEntity> implements QuestionService {

    @Autowired
    private QuestionDao questionDao;

    @Override
    public List<Map<String, Object>> countTypeQuestion() {
        QueryWrapper<QuestionEntity> queryWrapper = new QueryWrapper<QuestionEntity>().select("TYPE,COUNT(TYPE) AS num").groupBy("type");
        List<Map<String, Object>> mapList = questionDao.selectMaps(queryWrapper);

        return mapList;
    }
}

编辑QuestionController,新增统计分类题目数量方法

@RestController
@RequestMapping("question/question")
public class QuestionController {
    @Autowired
    private QuestionService questionService;

    @Autowired
    private TypeService typeService;

    //获取按照题库分类的统计数据
    @RequestMapping("countTypeQuestion")
    public R countTypeQuestion(){
        List<Map<String, Object>> mapList = questionService.countTypeQuestion();
        for (Map<String, Object> map : mapList) {

            //根据分类id,读取对应的分类数据
             Long typeId = (Long) map.get("TYPE");
             TypeEntity typeEntity = typeService.getById(typeId);
            //重新封装分类名称到map
            map.put("name",typeEntity.getType());
        }
        return R.ok().put("mapList",mapList);
    }

}

6.1.2 编写前端代码

前端项目renren-fast-vue/src/views/modules/question目录 新增统计注册用户数量页面 echarts.vue

<template> 
  <div class="mod-demo-echarts">   
   
    <el-row :gutter="20">
      
      <el-col :span="24">
        <el-card>
          <div id="J_chartBarBox" class="chart-box"></div>
        </el-card>
      </el-col>      
    </el-row>
  </div>
</template>

<script>
  import echarts from 'echarts'
  export default {
    data () {
      return {       
        chartBar: null,
         beginTime: '',
         endTime: '',
         mapList: [],
         xData: [],
         yData: [],
         btnDisabled: false
      }
    },
    mounted () {     
         this.create()
    },
    activated () {
      // 由于给echart添加了resize事件, 在组件激活时需要重新resize绘画一次, 否则出现空白bug     
      if (this.chartBar) {
        this.chartBar.resize()
      }     
    },
    methods: {      
      create () {
         this. getDataList ()
         this.initChartBar()  
      },
       // 获取数据列表
      getDataList () {
        this.dataListLoading = true
        this.$http({
          url: this.$http.adornUrl('/question/question/countTypeQuestion'),
          method: 'get'          
        }).then(({data}) => {
          if (data && data.code === 0) {            
            this.mapList = data.mapList   
            this.xData = []
            this.yData = []         
            for(let j in this.mapList){
            this.xData.push(this.mapList[j].name)
            this.yData.push(this.mapList[j].num)
            }
             console.log(this.xData)
            console.log(this.yData)
            this.chartBar.resize()
          // this.xData= this.mapList.map(obj => n.NAME);       
          // this.yData= this.mapList.map(obj => n.VALUE);  
          this.chartBar.setOption({
             xAxis: [
            {
              type: 'category',
              data: this.xData
            }
          ],
          yAxis: [
            {
              type: 'value'
            }
          ],
          series: [
            {
              name: '题目数量',
              type: 'bar',
              data: this.yData
            }
          ]
          })
          } else {
            this.mapList = []           
          }        
        })
      },
      // 柱状图
      initChartBar () {

        var option = {
          tooltip: {
            trigger: 'axis',
            axisPointer: {
              type: 'shadow'
            }
          },
          legend: {
            data: ['题目数量']
          },
          grid: {
            left: '3%',
            right: '4%',
            bottom: '3%',
            containLabel: true
          },
          xAxis: [
            {
              type: 'category',
              data: this.xData
            }
          ],
          yAxis: [
            {
              type: 'value'
            }
          ],
          series: [
            {
              name: '每日注册用户数',
              type: 'bar',
              data: this.yData
            }
          ]
        }
        this.chartBar = echarts.init(document.getElementById('J_chartBarBox'))
       // this.chartBar.setOption(option)
        window.addEventListener('resize', () => {
          this.chartBar.resize()
        })
      }     
    }
  }
</script>

<style lang="scss">
  .mod-demo-echarts {
    > .el-alert {
      margin-bottom: 10px;
    }
    > .el-row {
      margin-top: -10px;
      margin-bottom: -10px;
      .el-col {
        padding-top: 10px;
        padding-bottom: 10px;
      }
    }
    .chart-box {
      min-height: 400px;
    }
  }
</style>

6.1.3 配置用户注册统计菜单

新增菜单后,刷新,即可查看菜单

点击题目数量统计可以查看按照题目分类统计的数量柱状图


avatar
青山
悟已往之不谏 知来者之可追
一言
今日诗词
站点信息
本站访客数 :
本站总访问量 :