Vue保姆级项目教程:十万字零基础开发信贷管理系统!

目录

项目简介与搭建过程

项目简介

需求背景

信贷管理系统是一种用于银行、金融机构或其他借贷组织用于管理信贷流程的软件系统。它可以帮助机构管理贷款申请、贷款审批、合同管理等相关流程,提高信贷业务的效率和准确性。

需求描述

本需求文档旨在定义信贷管理系统的功能和特性,以满足机构对于信贷业务管理的需求。
信贷管理系统项目基于vue2实现,鉴于vue2的语法已经停止维护,后期会考虑使用vue3重写本项目

搭建过程

新建项目命令

vue create credit-management-system

项目配置语言选择

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

项目启动

npm run serve

登录页面初始化及其vue-router路由编写

配置路由

  1. 先把app.vue的默认代码删除,留下一个router-view路由出口
    App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

  1. 打开router文件夹下的index.js,删除掉默认的路由配置
Vue.use(VueRouter)

const routes = [

]

const router = new VueRouter({
  routes
})

export default router

  1. 在main.js里引入公共样式文件,导入和使用elementui组件
    引入elementui组件
cnpm i element-ui -S

导入使用

import App from './App.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

4. 先在router文件夹下index.js添加一个登录页面的路由和重定向
组件是使用的懒加载,可以减少页面加载时间

const routes = [
    {
    path:'/',
    redirect:'/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue')
  },
]

LoginView.vue写css之后报错不能解析sass-loader
在命令行执行如下命令

npm install sass-loader -D
cnpm install node-sass -D

引入公共样式normalize.css文件之后出现错误
错误信息:

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
解决方案:ctrl+shift+p,在setting.json中添加如下
“javascript.validate.enable”: false

编写登录页面

第一步

<div class="login-box">
    <div class="login-input-box center">
      <h1>信贷管理系统</h1>
    
    </div>
  </div>

第二步

先定义表单内容放到–上面的h1标签下面

<el-form :model="ruleForm" :rules="rules" status-icon ref="ruleForm" class="demo-ruleForm">
    
        <el-form-item prop="username">
          <el-input prefix-icon="el-icon-user-solid" v-model="ruleForm.username"></el-input>
        </el-form-item>
    
        <el-form-item prop="pass">
          <el-input prefix-icon="el-icon-s-order" type="password" v-model="ruleForm.pass" autocomplete="off"></el-input>
        </el-form-item>

        <el-button type="primary" @click="submitForm">提交</el-button>
      </el-form>

第三步

1.data中定义一个ruleForm,接收用户名和密码

2.再定义一个rules,限制输入格式

3.在methods中添加一个submitForm表单提交方法和密码限制的validatePass方法

data中:

ruleForm: {
            username: "",
            pass: ""
          }
rules: {
        pass: [
          {
            validator: this.validatePass,
            trigger: "blur"
          }
        ],
        username: [{ required: true, trigger: "blur", message: "请输入用户名" }]
      }

methods中:

validatePass(rule, value, callback) {
      if (value === "") {
        callback(new Error("请输入密码"));
      } else if (value.length < 6) {
        callback(new Error("密码不能小于6位数"));
      } else {
        callback();
      }
    },
 submitForm(){
 this.$refs.ruleForm.validate(valid => {
        if (valid) {
          // 如果验证通过,
         //调用login方法,传入登录信息,下面会定义
         this.login(this.ruleForm)
        } else {
         
          return false;
        }
      });
  }

4.创建login登录方法

async login(form){
    //成功后跳转到首页
     this.$router.replace("/home");
}

5.样式的引入

在main.js中引入样式

 import '@/assets/normalize.css';

6.在登录界面中引入下面样式

!!!注意语言是scss

<style lang="scss" scoped>
.login-input-box {
  width: 650px;
  height: 320px;
  background: #fff;
  text-align: center;
  padding: 40px 40px 12px 12px;
}

.login-box {
  height: 100%;
  background: url(../assets/bg2.jpg);
  background-size: cover;
}

.el-button {
  width: 600px;
}

.el-input {
  width: 600px;
  margin-bottom: 16px;
}

::v-deep .el-input__inner {
  background: #e5e5e5;
}
</style>

编写首页

添加路由规则

打开router文件夹下的index.js

其中的Layout为布局页面

//为routes添加
{
    path: '/',
    redirect:'/home',
        //下面这两个页面按照层级自行创建
    component:()=>  import('@/layout/layout.vue'),
    children:[
      {
      meta:{
       title:'首页'
        },
        path:'/home',
        component:()=> import('@/views/home.vue')
      }
    ]
  }

layout布局和编写首页及其vue-router路由跳转

layout布局包括侧边栏,导航栏和内容部分

 <div>
    <el-container>
     <el-aside width="240px">
        <el-menu   class="el-menu-vertical-demo" background-color="#545c64"
            text-color="#fff" active-text-color="#ffd04b">
            <!--//没有二级导航的 -->
             <el-menu-item index="1">
                <span slot="title"> 首页</span>
             </el-menu-item>
        
    	 <!--	//有二级导航的 -->
            <el-submenu index="2">
                    <template slot="title">
                    <span>贷款管理</span>
                    </template>
                  <el-menu-item index="2-1">
                     贷款申请
                  </el-menu-item>
            </el-submenu>   
        </el-menu>
    </el-aside>
        <el-container>
            <el-header>
               <!--//左侧面包屑-->
               <div class="left">
                   首页
                </div>
         		<!--//右侧用户名-->
                <div class="right">
                   <!--// //下拉菜单-->
                   <el-dropdown >
                   
                    <span class="el-dropdown-link"> admin </span>
                   
                    <el-dropdown-menu slot="dropdown">
                         <el-dropdown-item >退出登录</el-dropdown-item>
                    </el-dropdown-menu>
                   
                  </el-dropdown>
                </div>
            </el-header>
            <el-main>
                <!-- //放置一个路由出口 -->
                <router-view />
            </el-main>
        </el-container>
    </el-container>
  </div>

侧边栏包含二级菜单,导航栏最右侧有一个退出登录选择,内容包括了数字滚动和echarts图表

数字滚动组件vue-count-to

官网:www.npmjs.com/package/vue…
安装插件的命令如下

npm i -S vue-count-to@1.0.13

引入后插入页面使用,在使用命令安装过程中出现如下报错信息

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
解决方案:
根据提示检查网络情况,更换手机热点(我开发过程中更换热点仍然报错,应该不是网络原因了)
修改npm的网络设置 npm config list

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
图中设置用到的命令如下

npm config set proxy null
npm config set https-proxy null
npm config set registry https://registry.npmjs.org/

最初查阅文档之类的设置了很多次,最后有点懵懵哒,npm init了
稍过一会,再次下载就ok了(也许是设置原因或者最初网络不佳,不是非常确定)
最后一次成功使用的命令是

cnpm i -S vue-count-to@1.0.13 

( npm是node官方的包管理器。 cnpm是个中国版的npm,是淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm)

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

首先设置3个栅格布局,其中属性作用,查看element-ui官网的layout布局

 <div class="home">
    <el-row :gutter="10">
            <el-col :xl="6" :lg="6" :md="12" :sm="24" :xs="24" >
            <div>1</div>
            </el-col>
            <el-col :xl="6" :lg="6" :md="12" :sm="24" :xs="24" >
            <div>2</div>
            </el-col>
            <el-col :xl="6" :lg="6" :md="12" :sm="24" :xs="24" >
            <div>3</div>
        </el-col>
    </el-row>
</div>

把栅格布局的内容使用数组遍历来显示

<el-row :gutter="10">
        <el-col :xl="6" :lg="6" :md="12" :sm="24" :xs="24" v-for="(item, index) in list"          :key="index">
          
          <div class="dashboard-item" :style="{ background: item.color }">
            <p>{{ item.title }}</p>
            
          </div>
        </el-col>
</el-row>

data中定义一个数组

 data(){
        return {
            list: [
                { val: 13594, title: '最高可借金额', color: '#67ca3a', },
                { val: 9833, title: '回报率', color: '#ff6600' },
                { val: 8888, title: '业绩领跑', color: '#f56c6c' },
                { val: 6666, title: '安稳底薪战队', color: '#409eff' }
             ]
        }
    }

echarts图表

介绍echart

官网echarts.apache.org/zh/index.ht…

下载echarts并使用

  1. 下载
cnpm i -S echarts@4.9.0

2. HomeViews页面引入

import echarts from 'echarts';

编写第一个图表

  1. 定义图表位置
<template>
    <div class="home">
     <el-row :gutter="10">
          <el-col :xl="6" :lg="6" :md="12" :sm="24" :xs="24" v-for="(item, index) in             list"  :key="index">
              
            <div class="dashboard-item" :style="{ background: item.color }">
              <p>{{ item.title }}</p>
              <CountTo :startVal="0" :endVal="item.val" :duration="3000" />
            </div>
          </el-col>
        </el-row>
        
		<!-- 所有图表位置,一共3个 -->
    <div class="echarts-box">
        <!-- 第一个图表位置-->
      <div class="chart1"></div>

    </div>


    </div>
  </template>

2. 配置图表

添加一个initCharts方法

 initCharts() {
     //初始化图表实例,为什么要添加到this上面,因为后面要检测页面大小改变,更新图表大小
      this.chart1 = echarts.init(document.querySelector('.chart1'));
     //图表配置
     let option1 = {
        title: {
   
        },
        tooltip: {
          trigger: 'axis'
        },
        legend: {
          data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        toolbox: {
          feature: {
            saveAsImage: {}
          }
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            name: 'Email',
            type: 'line',
            stack: 'Total',
            data: [120, 132, 101, 134, 90, 230, 210]
          },
          {
            name: 'Union Ads',
            type: 'line',
            stack: 'Total',
            data: [220, 182, 191, 234, 290, 330, 310]
          },
          {
            name: 'Video Ads',
            type: 'line',
            stack: 'Total',
            data: [150, 232, 201, 154, 190, 330, 410]
          },
          {
            name: 'Direct',
            type: 'line',
            stack: 'Total',
            data: [320, 332, 301, 334, 390, 330, 320]
          },
          {
            name: 'Search Engine',
            type: 'line',
            stack: 'Total',
            data: [820, 932, 901, 934, 1290, 1330, 1320]
          }
        ]
      }
     
     //应用配置生成图表
      this.chart1.setOption(option1)
 }

3. 在mounted生命周期里执行initCharts方法

mounted() {
    this.initCharts()
 }

4. 图表没有显示

需要设置容器大小

 .chart1 {
    width: 100%;
    height: 400px;
  }

编写第二个图表

	<!-- 放在第一个图表下面     --> 
<div class="chart2"></div>

2. 在initCharts方法中编写第二个图表数据

     this.chart2 = echarts.init(document.querySelector('.chart2'));
//下面都是图表的配置,顺序不能错
let posList = [
'left',
'right',
'top',
'bottom',
'inside',
'insideTop',
'insideLeft',
'insideRight',
'insideBottom',
'insideTopLeft',
'insideTopRight',
'insideBottomLeft',
'insideBottomRight'
];
let app = {};
app.configParameters = {
rotate: {
min: -90,
max: 90
},
align: {
options: {
left: 'left',
center: 'center',
right: 'right'
}
},
verticalAlign: {
options: {
top: 'top',
middle: 'middle',
bottom: 'bottom'
}
},
position: {
options: posList.reduce(function (map, pos) {
map[pos] = pos;
return map;
}, {})
},
distance: {
min: 0,
max: 100
}
};
app.config = {
rotate: 90,
align: 'left',
verticalAlign: 'middle',
position: 'insideBottom',
distance: 15,
onChange: function () {
const labelOption = {
rotate: app.config.rotate,
align: app.config.align,
verticalAlign: app.config.verticalAlign,
position: app.config.position,
distance: app.config.distance
};
this.chart2.setOption({
series: [
{
label: labelOption
},
{
label: labelOption
},
{
label: labelOption
},
{
label: labelOption
}
]
});
}
};
const labelOption = {
show: true,
position: app.config.position,
distance: app.config.distance,
align: app.config.align,
verticalAlign: app.config.verticalAlign,
rotate: app.config.rotate,
formatter: '{c}  {name|{a}}',
fontSize: 16,
rich: {
name: {}
}
};
let option2 = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['Forest', 'Steppe', 'Desert', 'Wetland']
},
toolbox: {
show: true,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar', 'stack'] },
restore: { show: true },
saveAsImage: { show: true }
}
},
xAxis: [
{
type: 'category',
axisTick: { show: false },
data: ['2012', '2013', '2014', '2015', '2016']
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Forest',
type: 'bar',
barGap: 0,
label: labelOption,
emphasis: {
focus: 'series'
},
data: [320, 332, 301, 334, 390]
},
{
name: 'Steppe',
type: 'bar',
label: labelOption,
emphasis: {
focus: 'series'
},
data: [220, 182, 191, 234, 290]
},
{
name: 'Desert',
type: 'bar',
label: labelOption,
emphasis: {
focus: 'series'
},
data: [150, 232, 201, 154, 190]
},
{
name: 'Wetland',
type: 'bar',
label: labelOption,
emphasis: {
focus: 'series'
},
data: [98, 77, 101, 99, 40]
}
]
};
this.chart2.setOption(option2);
  1. 设置第二个图表的容器大小
.chart2 {
height: 400px;
width: 50%;
}

编写第三个图表

      <!--放到第二个图表下面 -->
<div class="chart3"></div>
  1. 配置第三个图表信息
      this.chart3 = echarts.init(document.querySelector('.chart3'));
let option3  = {
angleAxis: {},
radiusAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu'],
z: 10
},
polar: {},
series: [
{
type: 'bar',
data: [1, 2, 3, 4],
coordinateSystem: 'polar',
name: 'A',
stack: 'a',
emphasis: {
focus: 'series'
}
},
{
type: 'bar',
data: [2, 4, 6, 8],
coordinateSystem: 'polar',
name: 'B',
stack: 'a',
emphasis: {
focus: 'series'
}
},
{
type: 'bar',
data: [1, 2, 3, 4],
coordinateSystem: 'polar',
name: 'C',
stack: 'a',
emphasis: {
focus: 'series'
}
}
],
legend: {
show: true,
data: ['A', 'B', 'C']
}
}
this.chart3.setOption(option3); 
  1. 设置css样式
 .chart3 {
height: 400px;
width: 50%;
}

贷款申请

页面跳转

在layout布局页面写贷款申请的相关内容,首先用router-link设置跳转页面

 <router-link to="/loan-input/index">贷款申请</router-link>

在index.js里面编写路由

  {
path: '/loan',
redirect: '/loan-input/index',
component: () => import('@/layout/layout.vue'),
children: [
{
meta: {
title: '贷款申请'
},
path: '/loan-input/index',
name: '/loan-input',
component: () => import('@/views/loan/IndexView.vue')
}
]
}

编写之后可以跳转到贷款申请页面,但是在贷款申请页面点击首页无法跳转,这个时候可以给首页添加router-link,后续可能有最佳实践(欲知后事如何,请听下回分解)
下面是修改后的代码

      <el-menu-item index="1">
<span slot="title"> 
<router-link to="/home">首页</router-link>
</span>
</el-menu-item>

信息表内容IndexView.vue

信息表的内容以卡片的形式呈现,使用elementui中的卡片card,注意将卡片布局写入el-form中
信息表的信息分行显示,采用栅格布局

 <el-form :model="form" ref="form" label-width="80px">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>个人基本信息</span>
</div>
</el-card>
</el-form>

!()[4.png]
观察span标签的个人基本信息四个字的dom结构,尝试将文字居左。

.card-box{
>div{
>div{
text-align: left;
}
}
}

贷款申请的样式会颜色,如果想重置,添加下面的样式

a:link {
text-decoration: none;
color: #fff;
}
/* 状态二: 已经访问过的链接 */
a:visited {
text-decoration: none;
color: #fff;
}
/* 状态三: 鼠标划过(停留)的链接(默认红色) */
a:hover {
text-decoration: none;
color: #fff;
}
/* 状态四: 被点击的链接 */
a:active {
text-decoration: none;
color: #fff;
}

第一个card个人基本信息

个人基本信息具体内容使用栅格布局


<el-card class="box-card">
<div slot="header">
<span>个人基本信息</span>
</div>
<el-row> 
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<!-- prop的值是下面form后面的属性名也就是name -->
<el-form-item label="姓名" prop="name">
<!-- 等所有表单写完统一设置属性接收-->
<el-input  type='input' v-model="form.name" ></el-input>
</el-form-item>
</el-col>  
</el-row>      
</el-card> 

下面三个el-col都是放到el-row里面的

<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24">
<el-form-item label="出生日期" prop="birthday">
<!-- 宽度会有点小,后面复制样式后会改变 -->
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="选择日期">
</el-date-picker>
</el-form-item>
</el-col>  
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24">
<el-form-item label="性别" prop="sex">
<el-select  v-model="form.sex" >
<el-option
key="man"
label="男"
value="man">
</el-option>
<el-option
key="woman"
label="女"
value="woman">
</el-option>
</el-select>
</el-form-item>
</el-col>   
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24">
<el-form-item label="身份证" prop="identity_card">
<el-input  type='input' v-model="form.identity_card" ></el-input>
</el-form-item>
</el-col>

添加第二个el-row

在第一个el-row下面添加

 <el-row>
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24">
<el-form-item label="婚姻状态" prop="marriage">
<el-select  v-model="form.marriage" >
<el-option
key="married"
label="已婚"
value="married">
</el-option>
<el-option
key="unmarried"
label="未婚"
value="unmarried">
</el-option>
</el-select>
</el-form-item>
</el-col>   
</el-row>   

添加在第二个el-row里面


<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24">
<el-form-item label="教育程度" prop="education">
<el-select  v-model="form.education" >
<el-option
key="college"
label="大学"
value="college">
</el-option>
<el-option
key="highschool"
label="高中"
value="highschool">
</el-option>
</el-select>
</el-form-item>
</el-col> 
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<el-form-item label="居住地址" prop="address1">
<el-input  type='input' v-model="form.address1" ></el-input>
</el-form-item>
</el-col>  

添加第三个el-row

  <el-row>
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<el-form-item label="户籍地址" prop="address2">
<el-input  type='input' v-model="form.address2" ></el-input>
</el-form-item>
</el-col>  
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<el-form-item label="居住电话" prop="phone">
<el-input  type='input' v-model="form.phone" ></el-input>
</el-form-item>
</el-col>  
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<el-form-item label="手机号" prop="mobile_phone">
<el-input  type='input' v-model="form.mobile_phone" ></el-input>
</el-form-item>
</el-col>  
</el-row> 

第二个card,职业信息

1.添加第二个card

<el-card class="box-card">
<div slot="header">
<span>职业信息</span>
</div>
<el-row> 
<el-col
:xl='8' :lg='6'
:md="12" :sm="24" :xs="24" >
<el-form-item label="现职公司" prop="company">
<el-input  type='input' v-model="form.company" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='8' :lg='6'
:md="12" :sm="24" :xs="24" >
<el-form-item label="所属行业" prop="trade">
<el-select  placeholder='请选择Green'  v-model="form.trade" >
<el-option
key="education"
label="教育"
value="education">
</el-option>
<el-option
key="finance"
label="金融"
value="finance">
</el-option>
</el-select>
</el-form-item>
</el-col> 
<el-col
:xl='8' :lg='6'
:md="12" :sm="24" :xs="24" >
<el-form-item label="职位" prop="position">
<el-input  type='input' v-model="form.position" ></el-input>
</el-form-item>
</el-col>   
<el-col
:xl='8' :lg='6'
:md="12" :sm="24" :xs="24" >
<el-form-item label="公司地址" prop="address3">
<el-input  type='input' v-model="form.address3" ></el-input>
</el-form-item>
</el-col>   
</el-row>   
<el-row>
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<el-form-item label="公司类型" prop="company_type">
<el-input  type='input' v-model="form.company_type" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24" >
<el-form-item label="公司邮箱" prop="company_email">
<el-input  type='input' v-model="form.company_email" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='8' :lg='8'
:md="12" :sm="24" :xs="24">
<el-form-item label="公司电话" prop="company_phone">
<el-input  type='input' v-model="form.company_phone" ></el-input>
</el-form-item>
</el-col>
</el-row>
</el-card>

第三个card,收支情况

<el-card class="box-card">
<div slot="header">
<span>收支情况</span>
</div>
<el-row> 
<el-col
:xl='8' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="收支情况" prop="income">
<el-input  type='input' v-model="form.income" ></el-input>
</el-form-item>
</el-col>
</el-row>   
</el-card>

第四个card,家庭联系人

<el-card class="box-card">
<div slot="header">
<span>家庭联系人</span>
</div>
<el-row> 
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="关系1" prop="contact">
<el-input  type='input' v-model="form.contact" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="姓名" prop="contact_name">
<el-input  type='input' v-model="form.contact_name" ></el-input>
</el-form-item>
</el-col>
</el-row>   
<el-row>
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="手机" prop="contact_phone">
<el-input  type='input' v-model="form.contact_phone" ></el-input>
</el-form-item>
</el-col>
</el-row>
</el-card>

第五个card,工作证明人

<el-card class="box-card">
<div slot="header">
<span>工作证明人</span>
</div>
<el-row> 
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="关系2" prop="contact2">
<el-input  type='input' v-model="form.contact2" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="姓名" prop="contact2_name">
<el-input  type='input' v-model="form.contact2_name" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="手机" prop="contact2_phone">
<el-input  type='input' v-model="form.contact2_phone" ></el-input>
</el-form-item>
</el-col>
</el-row>   
<el-row> 
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="部门" prop="contact2_dep">
<el-input  type='input' v-model="form.contact2_dep" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="职位" prop="contact2_pos">
<el-input  type='input' v-model="form.contact2_pos" ></el-input>
</el-form-item>
</el-col>
</el-row>  
<el-row> 
<el-col
:xl='12' :lg='12'
:md="12" :sm="24" :xs="24" >
<el-form-item label="备注" prop="remark">
<el-input  type='textarea' v-model="form.remark" ></el-input>
</el-form-item>
</el-col>
</el-row>  
</el-card>

提交按钮和重置,放到最后一个card下面

<el-button @click="createUser" type="primary">立即创建</el-button>
<el-button @click="reset">重置</el-button>

引入css样式

<style lang="scss" scoped>
.el-select,.el-input {
width: 100%;
}
.btns {
text-align: center;
}
.box-card {
margin-bottom: 10px;
>div {
>div {
text-align: left;
}
}
}
</style>

定义form的属性,接收数据

form:{
name:'' ,
birthday:'',
sex:'',
identity_card:'',
marriage:'',
education:'',
address1:'',
address2:'',
phone:'',
mobile_phone:'',
company:'',
trade:'',
position:'',
address3:'',
company_type:'',
company_email:'',
company_phone:'',
income:'',
contact:'',
contact_name:'',
contact_phone:'',
contact2:'',
contact2_name:'',
contact2_phone:'',
contact2_dep:'',
contact2_pos:'',
remark:''
}

为form添加rules规则校验

  1. 为el-form上设置rules
    <el-form ref="form" :model="form" :rules="rules"  label-width="80px">
  1. data中定义rules
    required: true必填校验

rules: {
name: [
{ required: true, message: "请输入姓名", trigger: "blur" },
{
min: 2,
max: 5,
message: "长度在 2 到 5 个字符",
trigger: "blur"
}
],
identity_card: [
{ required: true, message: "请输入身份证", trigger: "change" }
],
birthday: [
{
type: "date",
required: true,
message: "请选择日期",
trigger: "change"
}
],
sex: [{ required: true, message: "请选择性别", trigger: "change" }],
marriage: [
{ required: true, message: "请选择婚姻状态", trigger: "change" }
],
education: [
{ required: true, message: "请选择教育程度", trigger: "change" }
],
trade: [
{ required: true, message: "请选择所属行业", trigger: "change" }
],
address1: [
{ required: true, message: "请输入居住地址", trigger: "blur" }
],
address2: [
{ required: true, message: "请输入户籍地址", trigger: "blur" }
],
phone: [{ required: true, message: "请输入居住电话", trigger: "blur" }],
mobile_phone: [
{ required: true, message: "请输入手机号", trigger: "blur" }
],
company: [
{ required: true, message: "请输入现职公司全称", trigger: "blur" }
],
position: [{ required: true, message: "请输入职位", trigger: "blur" }],
address3: [
{ required: true, message: "请输入公司地址", trigger: "blur" }
],
company_type: [
{ required: true, message: "请输入公司类型", trigger: "blur" }
],
company_email: [
{ required: true, message: "请输入公司邮箱", trigger: "blur" }
],
company_phone: [
{ required: true, message: "请输入公司电话", trigger: "blur" }
],
income: [
{ required: true, message: "请输入收支情况", trigger: "blur" }
],
contact: [{ required: true, message: "请输入关系1", trigger: "blur" }],
contact_name: [
{ required: true, message: "请输入姓名", trigger: "blur" }
],
contact_phone: [
{ required: true, message: "请输入手机", trigger: "blur" }
],
contact2: [{ required: true, message: "请输入关系2", trigger: "blur" }],
contact2_name: [
{ required: true, message: "请输入姓名", trigger: "blur" }
],
contact2_phone: [
{ required: true, message: "请输入手机", trigger: "blur" }
],
contact2_dep: [
{ required: true, message: "请输入部门", trigger: "blur" }
],
contact2_pos: [
{ required: true, message: "请输入职位", trigger: "blur" }
]
}

提交方法和重置方法

 createUser() {
this.$refs.form.validate(async (data) => {
if (!data) return this.$message('表单验证不通过');
})
},
reset() {
this.$refs.form.resetFields()
}

为form添加rules规则校验

  1. 为el-form上设置rules
    <el-form ref="form" :model="form" :rules="rules"  label-width="80px">
  1. data中定义rules

rules: {
name: [
{ required: true, message: "请输入姓名", trigger: "blur" },
{
min: 2,
max: 5,
message: "长度在 2 到 5 个字符",
trigger: "blur"
}
],
identity_card: [
{ required: true, message: "请输入身份证", trigger: "change" }
],
birthday: [
{
type: "date",
required: true,
message: "请选择日期",
trigger: "change"
}
],
sex: [{ required: true, message: "请选择性别", trigger: "change" }],
marriage: [
{ required: true, message: "请选择婚姻状态", trigger: "change" }
],
education: [
{ required: true, message: "请选择教育程度", trigger: "change" }
],
trade: [
{ required: true, message: "请选择所属行业", trigger: "change" }
],
address1: [
{ required: true, message: "请输入居住地址", trigger: "blur" }
],
address2: [
{ required: true, message: "请输入户籍地址", trigger: "blur" }
],
phone: [{ required: true, message: "请输入居住电话", trigger: "blur" }],
mobile_phone: [
{ required: true, message: "请输入手机号", trigger: "blur" }
],
company: [
{ required: true, message: "请输入现职公司全称", trigger: "blur" }
],
position: [{ required: true, message: "请输入职位", trigger: "blur" }],
address3: [
{ required: true, message: "请输入公司地址", trigger: "blur" }
],
company_type: [
{ required: true, message: "请输入公司类型", trigger: "blur" }
],
company_email: [
{ required: true, message: "请输入公司邮箱", trigger: "blur" }
],
company_phone: [
{ required: true, message: "请输入公司电话", trigger: "blur" }
],
income: [
{ required: true, message: "请输入收支情况", trigger: "blur" }
],
contact: [{ required: true, message: "请输入关系1", trigger: "blur" }],
contact_name: [
{ required: true, message: "请输入姓名", trigger: "blur" }
],
contact_phone: [
{ required: true, message: "请输入手机", trigger: "blur" }
],
contact2: [{ required: true, message: "请输入关系2", trigger: "blur" }],
contact2_name: [
{ required: true, message: "请输入姓名", trigger: "blur" }
],
contact2_phone: [
{ required: true, message: "请输入手机", trigger: "blur" }
],
contact2_dep: [
{ required: true, message: "请输入部门", trigger: "blur" }
],
contact2_pos: [
{ required: true, message: "请输入职位", trigger: "blur" }
]
}

提交和重置方法

提交按钮绑定了提交方法,重置按钮有重置方法,在methods里定义。

 createUser() {
this.$refs.form.validate(async (data) => {
if (!data) return this.$message('表单验证不通过');
})
},
reset() {
this.$refs.form.resetFields()
}

axios的二次封装

首先进入文件根目录,安装axios的依赖

cnpm i axios

在src目录下新建utils文件夹request.js引入组件
request.js即对axios的二次封装

import axios from 'axios'
import router from '@/router'
import { Notification, Message } from 'element-ui'

Notification关于错误的提醒

创建axios的实例

const axios = axios.create(
{
// 关于公共地址的基本配置,请求路径会在vueconfig里面配置
// 添加api前缀
baseURL: '/api',
// 请求过期时间
timeout: 5000
}
)

这里有一个小小的失误:
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError:\request.js: Identifier ‘axios’ has
already been declared. (5:6)
就是创建实例,名称不能是axios,修改名称后出现了新的错误:
ReferenceError: request is not defined
真是糊涂了,请求拦截器使用的axios实例就是request,所以这里命名就是request。

请求拦截器

request.interceptors.request.use(config => {
return config
})

响应拦截器

request.interceptors.response.use(
response => {
//20000代表成功
if (response?.data?.code === 20000) {
// 由于历史遗留问题, 后端响应的结构不能一次性做到统一,兼容处理一下
if (typeof response?.data?.data === 'string')
Message.success(response?.data?.data);
if ( typeof response?.data?.data?.info === 'string') 
Message.success(response?.data?.data?.info);
return response;
// 603 代表token失效, 处理跳转到登录
} else if (response?.data?.code === 603) { 
Notification.error({
title: '错误',
message: 'token失效,请重新登录'
});
// 替换到登录页面,但是如果是login页面那就不重复跳转
let url=window.location.href.split('/')
if(url[url.length-1]!=='login'){
router.replace('/login')
}
} else {
//如果原始的请求状态不等于200表示响应错误
if(response.status!==200){
Notification.error({
title: '错误',
message: '响应错误'
})
}
}
return response
}
)
export default request

添加代理服务器

打开vue.config.js设置代理服务器,配置完成后项目需要重新启动。

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 打包配置
configureWebpack: {
devServer: {
proxy: {
'/api': {
target: 'http://124.223.69.156:6300/',
changeOrigin: true,
//后端请求并没有api所以要去掉
pathRewrite: { '^/api': '' }
}
}
}
}
})const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 打包配置
configureWebpack: {
// 开发服务器
devServer: {
// 设置代理服务器,解决跨域问题
proxy: {
'/api': {
target: 'http://124.223.69.156:6300/',
changeOrigin: true,
//后端请求并没有api所以要去掉
pathRewrite: { '^/api': '' }
}
}
}
}
})

登录接口api联调

封装请求方法

在src的apis目录下新建文件user.js,引入二次封装的axios文件即request.js

import request from '@/utils/request' 
export const doLogin = (user)=>{
return request.post("/user/login", {
//这里是后端需要account字符,也就是用户名
account: user.username,
password: user.password
})
}

在登录页引入doLogin方法

import { doLogin } from "@/apis/user"

测试登录

用户名:admin 密码:approve123456.

!!!该请求会返回token

登录页面,继续编写login方法,并保存token

 async login(form){
const res = await doLogin(form)
//如果是20000,说明是成功
if (res.data.code === 20000) {
//保存token
if (res?.data?.data?.token) {
localStorage.setItem('token',res?.data?.data?.token)
// 跳转到首页
this.$router.replace("/home");
}
} else {
this.$message.error('登录失败!!!');
}
},
login(form) {
const res =  doLogin(form)
}

这一小段代码打印结果是

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
因为axios是基于promise的,所以打印结果res是promise

向请求拦截器中加内容

把token放到请求头中(因为后续的接口都需要带着token)

打开utils下的request.js

request.interceptors.request.use(config=>{
const token = localStorage.getItem("token");
if (token) {
config.headers.token = token;
}
return config
})

打开登陆页面观察控制台,输入账号密码测试登录查看返回的token信息,报错,请求超时,将request.js文件中的请求超时时间改为50000,错误代码500,清空所有cookies,重新启动项目,依然不能访问,接口地址失效,有点无语.第二天接口正常运行显示token失效,清除应用程序中本地cookies(虽然本来就没有)使用超级管理员账号admin,密码approve123456.登录成功,真不容易

  async login(form) {
const res = await doLogin(form)
console.log(form)
console.log(res)
}

看一下控制台

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

贷款申请接口api编写联调

在apis文件下新建loan.js

在IndexView中引用调用loan.js

import { createLoan } from '@/apis/loan';
createUser() {
this.$refs.form.validate((valid) => {
if (!valid) {
this.$message('表单验证不通过');
return false;
} else {
createLoan(this.form);
}
});
},

记得别忘了给按钮绑定事件(createUser方法里面调用了createLoan事件)

<el-button @click="createUser" type="primary">立即创建</el-button>

点击创建表单报错:ReferenceError: createLoan is not defined
检查代码发现忘记在IndexView.vue引入loan.js了、
引入之后报错:Error in v-on handler: “TypeError: Cannot read properties of undefined (reading ‘code’)”
修改代码如图所示

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

  if (res?.data?.code === 20000) {
this.$message("表单创建成功")
}      

面包屑组件封装

Breadcrumb 面包屑
显示当前页面的路径,快速返回之前的任意页面。
在components里面新建面包屑组件Breadcrumb.vue
使用elementui的面包屑组件
面包屑的路径子路径需要自动获取不能写死,所以使用计算属性computed获取

<template>
<el-breadcrumb separator="/">
<!--第一个面包屑默认首页-->
<el-breadcrumb-item>
<router-link to="/home" style="color: black;">首页</router-link>
</el-breadcrumb-item>
<!--第二个取路由route的页面栈matched的最后一个-->
<el-breadcrumb-item v-if="route">
{{ route }}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
export default {
computed: {
route() {
let matched = this.$route.matched;
return matched[matched.length - 1].meta.title !== "首页" ? matched[matched.length - 1].meta.title : "";
}
}
}

这段代码的作用是根据当前路由的最后一个路由记录的 meta.title 值来动态计算并返回一个新的值给计算属性 route,
用于在页面中展示当前路由的标题。
如果最后一个路由记录的标题是 “首页”,则返回空字符串,以避免显示标题为 “首页” 的情况。
在layout.vue引入注册使用面包屑组件

import BreadCrumb from "@/components/Breadcrumb.vue"
export default {
components: {
BreadCrumb
},
}
   <!--//左侧面包屑-->
<div class="left">
<BreadCrumb/>
</div>

退出登录接口联调

接口地址:http://124.223.69.156:6300/user/logout
请求方式:post
参数名token,从header中传入,必须的
在utils的request.js中请求拦截器的代码下给headers添加了token所以不需要再次添加token了
查看element文档,dropdown如何绑定事件
layout.vue

<!--//右侧用户名-->
<div class="right">
<!--// //下拉菜单-->
<el-dropdown @command="logout">
<span class="el-dropdown-link"> admin </span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="login-exit">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>

在user.js里写退出登录的接口

export const logout = () => {
return request.post('/user/logout');
}

在layout.vue里面调接口

import { logout } from '@/apis/user';
methods: {
async logout(command) {
// console.log(command);
if (command === "login-exit") {
// 退出登录业务逻辑
let res = await logout();
//  清楚token
if (res.data.code === 603)
// localStorage.removeItem("token");
localStorage.clear();
}
}
}

res后台打印结果截图

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
为了保证信息安全,在退出登录的时候清除token。

申请列表

申请列表的菜单路由及表格页面编写

编写页面布局

layout.vue里面

  	 <!--	//有二级导航的 -->
<el-submenu index="3">
<template slot="title">
<span>申请管理</span>
</template>
<el-menu-item index="3-1">
<router-link to="/application-manage/index">申请列表</router-link>
</el-menu-item>
</el-submenu>   

router-link的跳转路径复制index.js的路由配置的路径即可

配置路由

申请管理及其二级菜单申请列表在index.js

{
path: 'application-manage',
meta: {
title: '申请管理'
},
component: () => import('@/layout/layout.vue'),
children: [
{
path:'/application-manage',
redirect: '/application-manage/index',
name: 'application-manage',
meta: {
title: '申请列表'
},
component: () => import('@/views/application-manage/IndexView.vue')
},
]
}

编写页面内容

表格页面最上面是一个搜索栏,内容为一行两列在element上查找一行两列的layout布局,申请列表页面是views/application-manage目录下新建的
IndexView.vue

<template>
<div>
<el-row>
<el-col :span="22">
<el-input type="text" placeholder="请输入名称" v-model="query">
</el-input>
<div class="grid-content bg-purple"></div>
</el-col>
<el-col :span="2">
<el-button type="primary" click="queryName">搜索</el-button>
</el-col>
</el-row>
<!--tableData是表格数据-->   
<el-table :data="tableData"  stripe style="width:100%">
<el-table-column type="index" label="序号" width="70"></el-table-column>
<el-table-column 
v-for="(item,index) in columns"
:key="index"
:label="item.label"
:prop="item.prop"
:width="item.width"
>
</el-table-column>  
</el-table>
</div>
</template>

接下来在页面中添加表格 在element搜索table组件
使用带斑马纹的表格,可以更容易区分出不同行的数据。
stripe属性可以创建带斑马纹的表格。它接受一个Boolean,默认为false,设置为true即为启用。
关于上面表格使用到的一些属性的介绍
v-for=”(item, index) in columns”: 这里使用 v-for 指令来遍历 columns 数组中的每个元素,item 表示当前遍历到的元素,index 表示当前元素的索引。
:key=”index”: 这是 Vue.js 中用于标识每个节点的唯一性的属性。在使用 v-for 进行循环渲染时,需要为每个生成的元素指定一个唯一的 key。
:label=”item.label”: 这个属性用于设置 组件的标签文本,即列的标题。
:prop=”item.prop”: 这个属性用于设置 组件绑定的数据字段,即列与数据源中的字段对应。
:width=”item.width”: 这个属性用于设置 组件的宽度,可以指定固定的宽度值或者使用百分比。

开发中遇到的表格失效问题

在这里遇到的问题是el-table表格失效,查阅资料降低elementUI的依赖
操作步骤如下

  1. 在package.json文件中手动把element-ui版本改为一个较低版本,如2.9.2
  2. 把原来创建的Vue项目中的node_modules文件夹删掉,重新执行cnpm install,重新安装

申请列表接口联调

编写和调用申请列表数据请求接口

首先在apis文件目录下新建loan.js定义获取申请列表的接口

export const getLoanList = (params) => {
return request({
url: '/loan/list',
method: 'get',
params
})
}

在IndexView中调用loan.js

import { getLoanList } from '@/apis/loan.js'

申请列表数据的获取方法(调用申请数据接口)

页面加载数据就存在的方法写在mounted里面,在mounted中调用接口获取数据

  methods: {
async getLoanList(params) {
let res = await getLoanList(params);
console.log(res);
},
},
//页面加载就请求表格数据(异步获取数据)
mounted() {
this.getLoanList();
}

图为res打印的数据结果

在methods中设置方法

async getLoanList(){
let res = await getLoanList(this.pageOptions)
//20000代表成功,rows是后端返回的总条数
if(res.data.code===20000){
this.tableData=res.data.data.data.data
this.rows=res.data.data.rows
}
}

开发中遇到的err

原因是没有接受params参数

methods: {
async getLoanList(params) {
let res = await getLoanList(params);
console.log(res);
},
},

接收数据内容并调整数据格式

   async getLoanList() {
let pageOptions = {
pageNo: 1,
pageSize: 10,
};
let res = await getLoanList(pageOptions);
// 表格获取数据并调整数据格式
this.tableData = res.data.data.data.data.map((item)=>{
item.birthday = this.getBirthday(item.birthday);
item.sex = this.getSex(item.sex);
item.education = this.getEducation(item.education);
return item;
});
},

接口需要参数pageNo:页码,pageSize:条数
遍历数据并通过调用方法调整数据格式

 getBirthday(date) {
let dateNow = new Date(date);
function convert(data) {
return data < 10 ? "0" + data : data;
}
let year = dateNow.getFullYear();
let month = convert(dateNow.getMonth()+1);
let day = dateNow.getDate();
let hours = convert(dateNow.getHours());
let minutes = convert(dateNow.getMinutes());
let secounds = convert(dateNow.getSeconds());
let data = year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + secounds;
// console.log(data);
return data;
},
// 转换性别格式
getSex(str) { 
return str === "man" ? "男" : "女";
},
// 转换教育程度格式
getEducation(educationLevel) {
switch (educationLevel) {
case 'highschool':
return '高中毕业';
case 'college':
return '专科毕业';
case 'university':
return '大学毕业';
default:
return '其他';
}
}

为了提升观感,添加样式使数据居中显示,给每一列添加样式 align=”center”
如图所示

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
这里补充一点,要状态码为20000的时候才可以返回数据
所以

if (res?.data?.code = 20000) {
this.tableData = res.data.data.data.data.map((item)=>{
item.birthday = this.getBirthday(item.birthday);
item.sex = this.getSex(item.sex);
item.education = this.getEducation(item.education);
return item;
});
}

此处报错信息SyntaxError:IndexView.vue: Support for the experimental syntax ‘optionalChainingAssign’ isn’t currently enabled (95:11)
原因是不应该使用赋值等号,应该严格的使用===

申请状态的编写

使用vue的slot插槽完成

  <!-- 默认插槽  column是所有列信息,row是当前行信息-->
<template v-slot="{ column,row}">
<!-- 如果列的prop的值为status-->
<div  v-if="column.property==='status'">
<!-- 使用两个过滤器来格式化方法,因为后端返回的申请状态是0到7 -->
<el-tag :type="row[column.property] | statusColor">{{ row[column.property] | status }}   
</el-tag>
</div>
<div v-else>
{{ row[column.property] }}
</div>
</template>

定义过滤器

filters:{
statusColor(status){
switch (status) {
case 0:
return 'success'
break
case 1:
return ''
break
case 2:
return 'success'
break
case 3:
return 'danger'
break
case 4:
return 'warning'
break
case 5:
return 'success'
break
case 6:
return 'danger'
break
case 7:
return 'success'
break
default:
return 'danger'
}
},
status(status){
switch (status) {
case 0:
return '进件'
break
case 1:
return '提交初审'
break
case 2:
return '初审通过'
break
case 3:
return '初审拒绝'
break
case 4:
return '提交终审'
break
case 5:
return '终审通过'
break
case 6:
return '终审拒绝'
break
case 7:
return '生成合同'
break
default:
return '出错了'
}
}
}

删除提交和编辑

三个按钮的功能及其绑定的点击事件

          <div  v-else-if="column.property === 'opts'">
<el-button @click="editApplication(row)"   type="primary">编辑</el-button>
<el-button @click="deliteApplication(row.id)"  type="danger">删除</el-button>
<el-button @click="submitApplication(row.id)"   type="success">提交审核</el-button>
</div>
<div v-else>
{{ row[column.property] }}
</div>

根据 column.property 的值来决定显示不同的内容:如果 column.property 等于 ‘opts’,则显示三个按钮(编辑、删除、提交审核),否则显示 row[column.property] 的值

methods方法里面写三个按钮的点击事件,先查看

 //编辑
editApplication(row) {
console.log(row);
},
// 删除
deliteApplication(id) {
console.log('删除',id)
},
// 提交审核
submitApplication(id) {
console.log('审核',id)
},

编辑需要所在行row的数据,而删除和提交只需要知道所在行的id值就可以。

稍后完善按钮的点击事件。

添加接口请求方法

data:id 是用于访问对象属性的方式,而 data:{id} 是用于从对象中提取属性值并赋给变量的对象解构语法。

//编辑提交接口
export const updateLoan = (data) => {
return request({
url: '/loan/update',
method: 'put',
data
})
}
//删除申请接口
export const deleteLoan = (id) => {
return request({
url: '/loan/delete/'+id,
method: 'delete',
})
}
// 提交审核接口
export const submitApprove = (id) => {
return request({
url: '/loan/submitToApprove',
method: 'post',
data:{
id
}
})
}

引入到IndexView.vue里面

import {getLoanList,updateLoan,deleteLoan,submitApprove} from '@/apis/loan.js'

删除单击事件

 async deliteApplication(id) {
let res = await deleteLoan(id);
if (res.data.code === 20000) {
//删除成功就重新调用一下申请列表的数据
this.getLoanList();
}
console.log("删除", id);
},

完善提交审核按钮点击事件

后端接口会判断这个申请的状态,如果是状态已经提交审核则返回错误

  async submitApplication(id) {
let res = await submitApprove(id); 
if (res.data.code === 20000) {
this.getLoanList();
}
console.log('审核', id);
},

调用提交审核的接口,如果请求接口成功就重新调用请求页面,刷新数据。

编辑弹框加接口联调

使用elementui的dialog对话框组件实现弹窗。
updateForm 是保存编辑的申请信息,rules是校验规则,dialogVisible是控制对话框是否显示,dialogVisible默认是false,如果要调试可以先改成true.

      updateForm: {
id: 0,
name: "",
sex: "",
mobile_phone:"",
},
rules: {
name: [{ required: true, message: "必须写用户名" }],
sex:[{required:true,message:"必须写性别"}],
},
dialogVisible: true ,

编辑对话框和el-table同级。

  <el-dialog title="申请管理-编辑" :visible="dialogVisible"  @close="dialogVisible = false"  width="30%">
<div class="form-box">
<el-form ref="updateForm" :model="updateForm" :rules="rules" label-width="80px">
<el-row>
<el-col
:xl=20 :lg=20
:md=12 :sm=24 :xs=24>
<el-form-item label="姓名" prop="name">
<el-input  type='input' v-model="updateForm.name" ></el-input>
</el-form-item>
</el-col>
<el-col
:xl=20 :lg=20
:md=12 :sm=24 :xs=24>
<el-form-item label="性别" prop="sex">
<el-select  v-model="updateForm.sex" >
<el-option
key="man"
label="男"
value="man">
</el-option>
<el-option
key="woman"
label="女"
value="woman">
</el-option>
</el-select>
</el-form-item>
</el-col> 
<el-col
:xl=20 :lg=20
:md=12 :sm=24 :xs=24>
<el-form-item label="手机号" prop="mobile_phone">
<el-input  type='input' v-model="updateForm.mobile_phone" ></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="btns clear-fix">
<div>
<el-button type="primary" @click="submitUpdate">提交</el-button>
<el-button @click="cleanFrom" >重置</el-button>
</div>
</div>
</div>
</el-dialog>

将提交按钮的单击事件写在methods里面

    async submitUpdate(){
let res = await updateLoan(this.updateForm);
if (res.data.code === 20000) {
this.dialogVisible = false;//若成功,更新对话框
this.getLoanList();//更新数据
}
}

重置按钮单击事件

    cleanFrom() {
this.$refs.updateForm.resetFields();
},

设置按钮是否可用

status

  • 0-默认值进件状态
  • 1-提交初审状态
  • 2-初审通过状态
  • 3-初审拒绝状态
  • 4-提交终审状态
  • 5-终审通过状态
  • 6-终审拒绝状态
  • 7-生成合同状态
    将编辑和提交审核按钮设置 0,2,3,6状态时可以使用。通过动态设置disabled属性。
<el-button :disabled="[0,2,3,6].indexOf(row['status'])===-1" @click="showEdit(row)" type="primary">编辑</el-button>
<el-button  @click="delLoan(row.id)"  type="danger">删除</el-button>
<el-button :disabled="[0,2,3,6].indexOf(row['status'])===-1" @click="submit(row.id)"   type="success">提交审核</el-button>

申请列表分页组件编写及数据联动

编写分页组件

通过elementui的分页组件Pagination实现数据分页;组件使用了size-change和current-change事件来处理页码大小和当前页变动时候触发的事件。page-sizes接受一个整型数组,数组元素为展示的选择每页显示个数的选项,[10, 20, 30, 40]表示四个选项,每页显示 10 个,20个,30个或者40个。

<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:page-sizes="[10, 20, 30, 40]"
:page-size="pageOptions.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="rows">
</el-pagination>

分页数据联动

data里面定义一下rows:0,在getLoanList里面看一下有多少数据,并将数据总数赋给rows

      // console.log(res)
this.rows = res.data.data.rows;

下面两个图是查看rows数据量的操作和结果

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

total就是总数据量,即tatal对应的是rows,前两个事件对应的方法从官网直接copy,修改如下,实现了点击页码就进入当前页,并刷新数据。

    handleSizeChange(val) {
this.pageOptions.pageSize = val;
this.getLoanList();
console.log(`每页 ${val} 条`);
},
handleCurrentChange(val) {
this.pageOptions.pageNo = val;
this.getLoanList();
console.log(`当前页: ${val}`);
},

补充一点,就是当总数据小于等于当前页可展示数据时,数据可以在一页展示,隐藏分页;
这里按照这个思路实现,而是直接采取了官网的做法

<!-- 分页组件定义 -->
:hide-on-single-page="value"
<!-- data里面定义 -->
value:false,//数据只有一页隐藏分页

模糊查询

根据接口文档可知查询条件的参数是name,由于上面的getLoanList 中的params中没有定义name,所以现在补充定义,分页也会用到params所以封装params

 queryName() {
this.pageOptions.name = this.query;
let res = this.getLoanList();
this.pageOptions.name = null;
console.log(res);
},

输入框的内容使用了双向数据绑定query,将输入框的内容query赋给name,重新获取数据,将输入框置空就ok。

贷款审批

贷款审批路由

配置贷款审批及其子集初审和终审的路由

{
path: '/loan-approve',
redirect: '/loan-approve/first',
meta: {
title: '贷款审批'
},
component: () => import('@/layout/layout.vue'),
children: [
{
path: '/loan-approve/first',
name: 'loan-approve-first',
meta: {
title: '初审'
},
component: () => import('@/views/loan-approve/FirstView.vue')
},
{
path: '/loan-approve/end',
name: 'loan-approve-end',
meta: {
title: '终审'
},
component: () => import('@/views/loan-approve/EndView.vue')
},
]
}

记得创建对应目录下的文件,否则会因为找不到文件而报错

贷款审批页面布局

layout.vue

                <el-submenu index="4">
<template slot="title">
<span>贷款审批</span>
</template>
<el-menu-item index="4-1">
<router-link to="/loan-approve/first">初审</router-link>
</el-menu-item>
<el-menu-item index="4-2">
<router-link to="/loan-approve/end">终审</router-link>
</el-menu-item>
</el-submenu>  

添加左侧菜单当前页显示样式

目前当前页在左侧菜单中没有体现,我们需要给左侧菜单添加样式,使其当前菜单字体颜色变成黄色
layout.vue

.router-link-active{
color:rgb(255,208,75) !important;
}

添加样式之后的效果如图所示

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

贷款审批-初审

内容区域最上方是一行两列布局和申请列表的一样。

<template>
<div>
<el-row>
<el-col :span="22"><el-input placeholder="请输入名称" type="text" v-model="query"></el-input></el-col>
<el-col :span="2"><el-button type="primary" @click="setQueryName">搜索</el-button></el-col>
</el-row>
<el-table :data="tableData" id="gbox"  :useIndex="true"
:stripe="true" :border="true" :fit="true" :useMultiSelect="false"
>
<el-table-column
type="index"
label="序号"
width="55"
align="center"
>
</el-table-column>
<el-table-column
v-for="(col, index) in columns"
:key="index"
:label="col.label"
:prop="col.prop"
:width="col.width"
align="center"
>
</el-table-column>
</el-table>
</div>
</template>

在data的返回数据中定义query,tableData,columns

            query: "",
tableData: [],
columns: [
{
label: "姓名",
prop: "name",
width: "80",
},
{
label: "出生日期",
prop: "birthday",
width: "160",
},
{
label: "身份证",
prop: "identity_card",
width: "260",
},
{
label: "操作",
// width: "280",
prop: "opts",
},
],
pageOptions: {
pageNo: 1,
pageSize:10,
},
rows:0,

与申请列表同理PageOptions封装PageNo和pageSize,定义rows,下面在事件方法里面调用。

async getFirstList() {
let res = await queryList(this.pageOptions);
if (res.data.code === 20000) {
this.tableData = res.data.data.data.data;
this.rows = res.data.data.rows;
}
}
    mounted() {
this.getFirstList();
}

在getFirstList事件方法里面调用初审的查询接口,参数为pageOptions,如果成功,就把数据赋给tableData,数据总数赋给rows。在mouted里调用getFirstList方法。
之后调整出生日期的格式

 convertBirthday(date) {
function convertTime(n) {
return n < 10 ? '0' + n : n
}
let d2 = new Date(date)
return d2.getFullYear() + '-'
+ convertTime(d2.getMonth() + 1) + '-'
+ convertTime(d2.getDate()) + ' '
+ convertTime(d2.getHours()) + ':'
+ convertTime(d2.getSeconds()) + ':'
+ convertTime(d2.getMinutes())
},

但是出生日期格式依旧不对,最后发现是获取列表方法的原因
初始版本

 async getFirstList() {
let res = await queryList(this.pageOptions);
if (res.data.code === 20000) {
this.tableData = res.data.data.data.data;
this.rows = res.data.data.rows;
}
//初审列表数据类型
console.log(res.data.data.data.data);
}

修改后

async getFirstList() {
let res = await queryList(this.pageOptions);
if (res.data.code === 20000) {
this.tableData = res.data.data.data.data.map((item) => {
item.birthday = this.convertBirthday(item.birthday);
return item;
});
this.rows = res.data.data.rows;
}
},

原始版本草率了

样式

关于本小结涉及到的样式,一开始表格的标题及其内容不居中,在遍历表头的那部分加上一个居中对齐的样式,前面的代码有写到,很简单这里不在重述。
写在初审页面的style

.btns {
text-align: center;
}
.el-row {
margin-bottom: 10px;
}
//当具有水平滚动条的表格组件的主体包装器元素出现水平溢出时,将水平溢出的部分隐藏,以避免页面出现水平滚动条。
::v-deep .el-table--scrollable-x .el-table__body-wrapper {
overflow-x: hidden;
}

最后一个样式解释一下:
::v-deep伪元素,用于穿透子组件的作用域样式。
.el-table–scrollable-x是一个类选择器,代表具有水平滚动条的表格组件。
.el-table__body-wrapper是一个类选择器,代表表格主体的包装器元素,在需要时提供滚动功能。

初审-插槽展示操作对应的按钮

          <!--插槽,如果当前列是操作,添加三个按钮-->
<template v-slot="{ column, row }">
<div   v-if="column.property === 'opts'">
<el-button  @click="showInfo(row)" type="primary">查看</el-button>
<!-- 传入pass和reject代表调用通过或者拒绝接口-->
<el-button  @click="submit(row.id, 'pass')"  type="success">通过</el-button>
<el-button  @click="submit(row.id, 'reject')"   type="danger">拒绝</el-button>
</div>
<div v-else>
{{ row[column.property] }}
</div>
</template>

下面还有对应的方法

        //查看按钮绑定的单击事件
showInfo(row) { },
//通过初审
submit(id, flag) { },

初审-查看详情的编写

定义对话框,点击查看是显示申请信息

<el-dialog title="贷款审批-查看详情" :visible="dialogVisible"  @close="dialogVisible=false"  width="30%">
<div class="form-box" v-if="dialogVisible">
<el-form ref="form" :model="form"  label-width="80px">
<el-row>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="id" prop="id">
<el-input  type='input' v-model="form.id" disabled ></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="姓名" prop="name">
<el-input  type='input' v-model="form.name" disabled></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="出生日期" prop="birthday">
<el-input  type='input' v-model="form.birthday" disabled></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="性别" prop="sex">
<el-input  type='input' v-model="form.sex" disabled></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="教育程度" prop="education">
<el-input  type='input' v-model="form.education" disabled></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="地址" prop="address1">
<el-input  type='input' v-model="form.address1" disabled></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="手机号" prop="phone">
<el-input  type='input' v-model="form.phone" disabled></el-input>
</el-form-item>
</el-col>
<el-col
:xl=24 :lg=24
:md=12 :sm=24 :xs=24>
<el-form-item label="身份证" prop="identity_card">
<el-input  type='input' v-model="form.identity_card" disabled></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="btns clear-fix">
<div>
<el-button type="primary" @click="dialogVisible=!dialogVisible">取消</el-button>
</div>
</div>
</div>
</el-dialog>

在close事件触发时,将一个标识控制对话框显示与隐藏的数据属性设置为false,以达到关闭对话框的效果。

showInfo(row){
this.form=row
this.dialogVisible=true
}

初审-审批(通过/拒绝)接口联调

export const approveFirst = (id, flag) => {
return request({
url: '/approve/first/' + flag,
method: 'post',
data: {
appId: id,
loanId: id
}
})
}

然后通过单击事件submit调用接口,如果成功就刷新页面数据(调用getFirstList重新获取页面数据)

       async submit(id, flag) { 
let res = await approveFirst(id, flag);
if (res.data.data.code === 20000) {
this.getFirstList()
}
},

初审-分页查询

在对话框组件下面添加分页器

<el-pagination 
@current-change="handleCurrentChange"
@size-change="updateSize"
:page-sizes="[10,20,30,40]"
:page-size="pageOptions.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="rows"
>
</el-pagination >

分页器用到的方法

handleCurrentChange(pageNo){
this.pageOptions.pageNo=pageNo
this.getFirstList()
},
updateSize(pageSize){
this.pageOptions.pageSize=pageSize
this.getFirstList()
}

初审-搜索查询接口联调

精确查询

将数据双向绑定的query数据传给pageOptions的name,然后调用展示初审列表数据的方法(参数为pageOptions),然后将pageOptions.name变成null。

async setQueryName(){
this.pageOptions.name=this.query
await this.getFirstList(this.pageOptions)
this.pageOptions.name=null
}

开发实践发现后端没有做模糊查询,所以这里改精确查询

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
如上图初审数据一共有9条

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
如上图查询姓张的初审信息,无数据,判断出后端这里没有写模糊查询。

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
输入张三进行精确查询可以查询到具体的用户数据。

一个疑问

此处萌生一个疑问,为什么调用getFirstList,参数是pageOptions(只有三个数据定义),在初审列表中还能返回身份证的数据。
因为getFirstList方法中遍历了所有数据

终审

内容与初审基本相同直接拷贝,修改一下接口名,使其与初审分别。

合同管理

合同列表

layout.vue页面布局

                <!-- 合同管理 -->
<el-submenu index="5">
<template slot="title">
<span>合同管理</span>
</template>
<el-menu-item index="5-1">
<router-link to="/loan-approve/first">合同列表</router-link>
</el-menu-item>
</el-submenu>  

index.js设置路由,layout.vue中的router-link设置成合同列表的path

{
path: '/contract-manage',
redirect: '/contract-manage/index',
meta: {
title: '合同管理'
},
component: () => import('@/layout/layout.vue'),
children: [
{
path: '/contract-manage/index',
name: 'contract-manage',
meta: {
title: '合同列表'
},
component: () => import('@/views/contract-manage/IndexView.vue')
},
]
},

写查询申请列表的接口

export const contractList = (params) => {
return request({
url: '/contract/list',
method: 'get',
params
})
}

合同列表的内容页和初审页面很像,所以可以在初审页面的基础上修改。删除对话提示el-diloag,删除方法中的对话提示显示,
更改列表表头数据

columns: [
{
label: "姓名",
prop: "name",
width: "100",
},
{
label: "身份证",
prop: "identity_card",
width: "260",
},
{
label: "合同状态",
prop: "status",
width: "160",
},
{
label: "操作",
prop: "opts",
},
],

通过替换插槽修改按钮

<template v-slot="{ column,row}">
<div   v-if="column.property==='opts'">
<!--5代表终审通过,大于5就代表已经生成合同了,需要禁用-->
<el-button :disabled="row['status']>5" @click="createContract(row.id)"  type="primary">生成合同</el-button>
<!--7代表生成合同,不等于7需要禁用-->
<el-button :disabled="row['status']!==7" @click="downContract(row.id)"   type="success">下载合同</el-button>
</div>
<div   v-else-if="column.property==='status'">
<el-tag :type="row[column.property]===7?'success':''">{{ row[column.property]===7?'生成合同':'未生成合同' }}</el-tag>
</div>
<div v-else>
{{ row[column.property] }}
</div>
</template>

之后再在方法methods里面写两个按钮绑定的单击事件
实际上,浏览器的下载操作通常是异步的,因为下载文件可能需要一定的时间。如果以同步的方式来进行下载,代码会一直等待下载完成,期间用户可能会感觉页面无响应,而且无法进行其他操作,给用户体验带来了不便。

 // 下载合同
async downloadContract(id) {
let res = await downloadContract(id);
console.log(res);
if (res.data.code === 20000) {
let fileName = '贷款合同-' + id + ".docx";
let url = res.data.data.url.replace("/api", "");
console.log(url);
let resblob = await request({
url,
responseType: "blob",
});
// 内存中的临时指向
let link = window.URL.createObjectURL(resblob.data);
//创建a标签
let tagA = document.createElement('a');
tagA.href = link;
tagA.document = fileName;
//隐藏a标签
tagA.style.display = 'none'
//向body中添加a标签
document.body.appendChild(tagA);
//点击a标签
tagA.click();
//释放资源
document.body.removeChild(tagA);
//释放临时url
window.URL.revokeObjectURL(url);
}
},
//生成合同
async createContract(id) {
let res = await generateContract(id);
console.log(res);
if (res.data.code === 20000) {
this.getContractList();
}
},

内存指向临时的地址存在window下,将内存的临时指向赋给a标签的链接。
点击下载合同,看一下打印的数据是否是服务器上的下载地址。

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
使用空格代替url中的api,使其成为服务器上的文件下载地址。引入request (axios的二次封装)来对url路径转化为blob对象

Blob(binary large object),二进制类文件大对象,是一个可以存储二进制文件的“容器”

生成合同和下载合同接口联调及内容编写

// 合同管理- 生成合同接口
export const generateContract = (id) => {
return requset({
url: '/contract/createFile',
method: 'post',
data: {
id
}
})
}
// 合同管理- 下载合同接口
export const downloadContract = (id) => {
return requset({
url: '/contract/download',
method: 'get',
params: {
id
}
})
}

权限管理

页面布局

layout.vue

                <el-submenu index="6">
<template slot="title">
<span>权限管理</span>
</template>
<el-menu-item index="6-1">
<router-link to="/promission/create">创建管理员</router-link>
</el-menu-item>
<el-menu-item index="6-2">
<router-link to="/promission/index">列表显示</router-link>
</el-menu-item>
</el-submenu>  

路由配置

index.js

 {
path: '/promission',
redirect: '/promission/index',
meta: {
title: '权限管理'
},
component: () => import('@/layout/layout.vue'),
children: [
{
path: '/promission/create',
name: 'promission-create',
meta: {
title: '创建管理员'
},
component: () => import('@/views/promission/CreateView.vue')
},
{
path: '/promission/index',
name: 'promission-index',
meta: {
title: '列表展示'
},
component: () => import('@/views/promission/IndexView.vue')
},
]
},

创建管理员

创建管理员页面

<template>
<div class="form-box">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :xl=12 :lg=12 :md=12 :sm=24 :xs=24>
<el-form-item label="用户名" prop="username">
<el-input type='input' v-model="form.username"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :xl=12 :lg=12 :md=12 :sm=24 :xs=24>
<el-form-item label="密码" prop="password">
<el-input type='password' v-model="form.password"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :xl=12 :lg=12 :md=12 :sm=24 :xs=24>
<el-form-item label="确认密码" prop="confirmPwd">
<el-input type='password' v-model="form.confirmPwd"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :xl=8 :lg=8 :md=12 :sm=24 :xs=24>
<el-form-item label="权限分配" prop="permission">
<el-radio v-model="form.permission" label='2'>销售人员</el-radio>
<el-radio v-model="form.permission" label='1'>初审人员</el-radio>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="left">
<el-button @click="doCreate" type="primary">创建管理员</el-button>
<el-button @click="doReset">重置</el-button>
</div>
</div>
</template>

创建管理员页面声明变量

data

 form:{
username:'',
password:'',
confirmPwd:'',
permission:'2'
},
rules: {
username: [{ required: true ,message:'必须输入用户名' }],
password: [{ required: true ,message:'必须输入密码'}],
//validatePass2方法检测两次密码是否一致
confirmPwd:[{required: true , validator: this.validatePass2, trigger: 'blur' }],
permission:[{ required: true  }]
}

创建管理员事件编写

methods: {
validatePass2(rule, value, callback) {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.form.password) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
},
// 创建管理员单击事件
async doCreate() {
this.$refs['form'].validate(async (data) => {
if (!data) return this.$message('表单验证不通过')
let res = await createUser(this.form)
if (res.data.code === 20000) {
this.doReset()
}
})
},
// 重置
doReset() {
this.$refs['form'].resetFields()
}
}

创建管理员接口

apis/user.js

import { createUser } from '@/apis/user'
//创建用户接口
export const createUser = ({ username, password, permission }) => {
return request.post('/permission/createUser', {
account: username,
password,
role_id: permission
})
}

进入创建管理员页面和点击左侧菜单之后的样式稍有改变,列表有缩小的现象,所以加上如下样式

.form-box {
margin-top: 50px;
}

用户列表

用户列表页面编写

用户列表展示使用斑马纹表格,设置第一个表格列时

<template>
<div>
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column type="index" label="序号" width="80" align="center">
</el-table-column>
<el-table-column v-for="(item, index) in columns" :key="index" :prop="item.prop" :label="item.label"
:width="item.width" align="center">
</el-table-column>
</el-table>
<!-- 分页器 -->
<el-pagination @current-change="handleCurrentChange" @size-change="updateSize" :page-sizes="[10, 20, 30, 40]"
:page-size="pageOptions.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="rows">
</el-pagination>
</div>
</template>

获取用户列表的接口

获取用户列表的接口user.js,别忘记导入IndexView.vue

//获取用户接口
export const userList = () => {
return request({
url: '/user/list?type=new',
method: 'get'
})
}

data

  data() {
return {
rows: 0,//用于接收总条数
pageOptions: {
pageNo: 1,
pageSize: 10
},
tableData: [],
columns: [
{
label: "用户名",
prop: "account",
},
{
label: "密码",
prop: "password",
},
{
label: '创建时间',
prop: 'reg_time',
},
{
label: '创建者',
prop: 'creator'
},
{
label: '权限分配',
prop: 'role_name',
}
]
}
}

methods

 methods: {
async getUserList() {
let res = await userList()
if (res.data.code === 20000) {
this.tableData = res.data.data
}
},
handleCurrentChange(pageNo) {
this.pageOptions.pageNo = pageNo
this.getUserList()
},
updateSize(pageSize) {
this.pageOptions.pageSize = pageSize
this.getUserList()
},
//格式化时间
getTime(date) {
function convertTime(n) {
return n < 10 ? '0' + n : n
}
let d2 = new Date(date)
return d2.getFullYear() + '-'
+ convertTime(d2.getMonth() + 1) + '-'
+ convertTime(d2.getDate()) + ' '
+ convertTime(d2.getHours()) + ':'
+ convertTime(d2.getSeconds()) + ':'
+ convertTime(d2.getMinutes())
},
// 格式化角色
getRole(s) {
switch (s) {
case 'input':
return '销售专员'
case 'approve':
return '初审专员'
case 'administrator':
return '超级管理员'
}
},
// 获取列表展示
async getUserList() {
let res = await userList();
if (res.data.code === 20000) {
let { pageNo, pageSize } = this.pageOptions;
this.rows = res.data.data.length;
let tableDataNew = res.data.data;
tableDataNew = tableDataNew.slice(
(pageNo - 1) * pageSize,
pageNo * pageSize
);
tableDataNew = tableDataNew.map((item) => {
item.reg_time = this.getTime(item.reg_time);
item.role_name = this.getRole(item.role_name);
return item;
});
this.tableData = tableDataNew;
}
},
},
mounted() {
this.getUserList()
}

首先,从this.pageOptions对象中获取当前页码pageNo和每页显示数量pageSize。
接着,将获取到的用户列表数据赋值给tableDataNew变量。
根据当前页码和每页显示数量,使用slice()方法对tableDataNew进行切割,以获取当前页需要显示的数据。
对切割后的数据进行遍历处理,使用map()方法将其中的每个元素进行转换。在这里,item.reg_time通过this.getTime(item.reg_time)方法进行处理,将注册时间转换为需要的格式;item.role_name通过this.getRole(item.role_name)方法进行处理,将角色名转换为需要的格式。

Vuex状态管理用户名

store/index.js用户名信息

state: {
userName: localStorage.getItem('userName') || '',//有就显示,没有就空
},
  mutations: {
NAMEUPDATE(state, userName) {
state.userName = userName
localStorage.setItem('userName', userName)
}
},

之后在LoginView.vue登录成功的方法里面调用,实现跳转到首页前把成功登录的用户名保存

 this.$store.commit("NAMEUPDATE", this.ruleForm.username)

在layout.vue里面修改用户名状态

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
使用computed(不需要触发,直接获取)

computed:{
userName(){
return this.$store.state.userName
}
},

用户鉴权

在store/index.js的state里面声明一个菜单数组,开始从本地存储中读取,使用JSON.parse是因为存的时候转成JSON,如果没有给你个空数组

 menuList: JSON.parse(localStorage.getItem('menuList')) || []

mutations里面的第二个方法,用于改变菜单数组的值,顺便持久化存储

 GETMENULIST(state, menuList) {
state.menuList = menuList
//转成JSON对象
localStorage.setItem('menuList', JSON.stringify(menuList))
}

用户鉴权写在actions里面

 async getMenuList({ commit }) {
//使用这个请求方法,获取用户角色,需要请求头中有token
let res = await userInfo()
//如果返回不等于20000代表token失效了
if (res.data.code !== 20000) return
let role = res.data.data.roles[0].name
//请求菜单列表
let res2 = await axios.get('/menus.json')
//用list保存菜单列表
let list = res2.data
let menuList = []
let routes = []
//用户判断
if (role === "administrator") {
//如果是管理员则获得所有权限
routes = list
} else if (role === "input") {
//如果是销售人员则获得销售权限
routes = list.filter(item =>
item.meta && item.meta.roles && item.meta.roles.indexOf(role) != -1 || item.meta.title === '首页'
)
} else if (role === "approve") {
//如果是审核人员则获得审批管理的初审权限
routes = list.filter(item =>
item.meta && item.meta.roles && item.meta.roles.indexOf(role) != -1 || item.meta.title === '首页'
)
//过滤掉审批管理的终审
routes = routes.map(item => {
if (item.children) {
item.children.forEach((it, index) => {
if (it.meta.title === '终审') {
delete item.children[index]
}
})
}
return item
})
}
//接收格式化的菜单
menuList = routes.map(item => {
//如果菜单有childeren
if (item.children) {
item.children.map(it => {
let path = it.path1
it.component = () => import(`@/views${path}View.vue`)
})
}
//如果菜单没有childeren
let url = item.component
item.component = () => import(`@/layout/${url}.vue`)
//把路由添加到路由器中
router.addRoute(item)
//格式化菜单,把path和meta拿出来
let { path, meta } = item
if (item.children) {
//如果有children把他也格式化
item.children = item.children.map(it => {
let { meta, path } = it
return { path, title: meta.title }
})
return { path, title: meta.title, children: item.children }
}
//没有children就直接格式化
return { path, title: meta.title }
})
//调用mutations的方法
commit("GETMENULIST", menuList)
}

在index.js里面引入router和axios,userInfo,axios用于获取menus的数据

import router from '@/router'
import axios from 'axios'
import {userInfo} from '@/apis/user.js'

user.js获取用户详情,通过token来判断用户角色,所以必须要在登录之后调用

// 获取用户详情接口
export const usereInfo = () => {
return request({
url: '/user/info',
method: 'get'
})
}

接收格式化的菜单如果菜单中有子菜单,

if (item.children) {
item.children.map(it => {
let path = it.path1
it.component = () => import(`@/views${path}View.vue`)
})
}

那么通过遍历item.children数组,给每个元素动态添加一个component属性,这个属性指向一个异步加载的组件。在实际渲染使用这个组件的时候,会根据path1属性值来动态加载对应的视图组件。

路由删除

!!!路由删除,除了下面这三个路由,其他的全删了

 {
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path:'/',
redirect:'/login'
},
{
path: '/',
redirect:'/home',
component:()=> import('@/layout/layout.vue'),
children:[
{
meta:{
title:'首页'
},
path:'home',
component:()=>  import('@/views/home.vue')
}
]
},

为了防止页面刷新时路由消失

在router文件中需要执行动态路由方法

引入store

import store from '@/store'

在router文件中添加代码

if(localStorage.getItem('token')){
store.dispatch('getMenuList')
}

由于这个方法需要token才行所以必须要在login后才能执行

login.vue页面中

 async login(form){
const res = await doLogin(form)
if (res.data.code === 20000) {
if (res?.data?.data?.token) {
localStorage.setItem('token',res?.data?.data?.token)
//在这里成功后执行这个方法,await确保成功到菜单和路由
await this.$store.dispatch('getMenuList')
this.$router.push("/home");
}
} else {
this.$message.error('登录失败!!!');
}
},

3.右上角用户名,由于后端没有返回用户名的接口

store中

 state: {
userName:localStorage.getItem('userName')||''
},
mutations: {
NAMEUPDATE(state,userName){
state.userName=userName
localStorage.setItem('userName',userName)
}
}

login.vue中login方法,看下面注释

 async login(form){
const res = await doLogin(form)  
if (res.data.code === 20000) {
if (res?.data?.data?.token) {
localStorage.setItem('token',res?.data?.data?.token)
await  this.$store.dispatch('getMenuList')
// 跳转到首页前把成功登录的用户名保存
this.$store.commit("NAMEUPDATE",this.ruleForm.username)
this.$router.push("/home");
}
} else {
this.$message.error('登录失败!!!');
}
}

layout页面中

注意互相匹配的两个跳转页面

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

退出登录方法

!!!清除存储和跳转后刷新页面

async  doCommand(e){
if(e==="logout"){
let res =   await logout()
if(res.data.code===20000){
//这里需要await阻塞一下,避免下面刷新先执行
await this.$router.replace("/login")
//退出成功时清掉所有的存储信息
localStorage.clear()
//跳转后刷新下页面,清除掉路由信息
window.location.reload()
}
}
}

报错信息如图

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
这种情况通常发生在导入的模块中,导入的模块可能不是一个函数,而是一个对象或其他类型的值。
检查发现user里面的接口名字多了一个s,无语,该死的键盘。

动态菜单组件封装

在layout.vue中引入注册组件 ,并添加在el-menu底部。

 components:{
asideMenu
}

在components文件夹下新建Asidemenu.vue

<template>
<div>
<!-- 如果菜单没有children--> <!-- index需要接收跳转路由的地址-->
<el-menu-item v-if="!menu.children" :index="menu.path">
<template slot="title">
<span>{{ menu.title }}</span>
</template>
</el-menu-item>
<!-- 如果菜单有children-->
<el-submenu v-else :index="menu.path">
<template slot="title">
<span>{{ menu.title }}</span>
</template>
<!-- 递归调用本组件-->
<asideMenu v-for="(item, index) in menu.children" :key="index" :menu="item" />
</el-submenu>
</div>
</template>

关于递归调用组件,遍历 menuList中的item,将item 传递给menu
报错信息截图

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
引入的文件与组件名大小写不完全一致导致的,认真修改后错误仍然存在,关闭项目重新启动,bug消失。

发布到github

发布项目到 GitHub 上需要按照以下步骤进行:

在 GitHub 上创建一个新的空白仓库。

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

在本地计算机上打开终端或者命令行界面,导航到你的项目目录。(或者直接打开文件目录下)

通过 git init 命令将该目录初始化为 Git 仓库。

Vue保姆级项目教程:十万字零基础开发信贷管理系统!
如果电脑已经安装git,在目录下右键点击git bash here;没有安装自行安装

使用 git add . 命令将项目的所有文件添加到本地 Git 仓库中。如果你只想添加某些文件,可以使用 git add <文件名> 来添加指定文件。

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

使用 git commit -m "Initial commit" 命令提交你的改动到本地 Git 仓库。你可以修改 “Initial commit” 为适合你项目的注释信息。

在 GitHub 上创建的仓库页面中,复制仓库的 URL。

通过 git remote add origin <仓库 URL> 命令将本地仓库与远程仓库关联起来。使用刚刚复制的仓库 URL 替换 <仓库 URL>

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

使用 git push -u origin master 命令将本地代码推送到远程仓库。这样做会将当前分支(通常是 master 分支)的提交推送到 GitHub 上的远程仓库。

或者是计划将项目提交到main分支下,最初进入页面先看到的是main分支。
先切换分支git branch -M main,之后git push -u origin main

Vue保姆级项目教程:十万字零基础开发信贷管理系统!

9.在 GitHub 上刷新仓库页面,你将看到你的项目代码已经成功发布到了 GitHub 上。

现在你的项目就已经成功发布到了 GitHub 上,其他人可以通过克隆仓库或者访问仓库 URL 来获取代码。

Vue保姆级项目教程:十万字零基础开发信贷管理系统!


theme: juejin

原文链接:https://juejin.cn/post/7343921798733938715 作者:隐藏用户_y

(0)
上一篇 2024年3月9日 下午4:20
下一篇 2024年3月9日 下午4:32

相关推荐

发表回复

登录后才能评论