Skip to content

Eyes22798/Crops

Repository files navigation

crops

本项目构建工具及其相关库

  • 基本库: Vue 2.60 + vue-router 3.0.3 + vuex 3.0.1
  • 构建工具: Vue 脚手架工具 Vue-cli 3.0+ webpack-dev-server 3.7.2
  • 项目UI框架: 使用 google 的 vuetify 2.0.0
  • CSS 预处理器:sass-loader 7.1.0 + stylus-loader 3.0.2
  • ajax库:axios 0.19.0
  • 工具类:qs 6.8.0nprogress 0.2.0vuetify-toast-snackbar 0.5.0
  • 字体图标库: Material Design IconsFont-awesome 4.0

1. 项目初始

安装项目

npm install

启动项目

npm run serve

打包项目

npm run build

测试项目

npm run test

修复文件

// Eslint 配置 standard 标准
npm run lint
npm run lint --fix		// 自动修复已知不规范代码

项目结构

项目初始目录

.
|-- README.md						# 项目说明文件
|-- babel.config.js					# babel 配置文件
|-- package-lock.json					# package.josn 绑定文件
|-- package.json					# 项目管理文件
|-- postcss.config.js
|-- vue.config.js					# webpack 配置文件
|-- public
|   |-- favicon.ico
|   `-- index.html
`-- src
    |-- App.vue						# 路由文件顶层路由
    |-- api						# 后端交互相关方法和配置
    |   |-- config.js					# 项目配置:生产、开发、测试接口配置 全局常量
    |   |-- index.js					# sercice文件夹api统一出口
    |   `-- service					# 对应组件使用的api方法和数据处理
    |       |-- modules					# 对应组件相关api
    |		|	|-- index.js			# 导出所有模块的api
    |		|	|-- common.js			# 公共模块api
    |		|	|-- expert.js			# 专家模块api
    |		|	|-- ordinary.js			# 普通用户模块api
    |       `-- http.js					# 封装使用ajax方法,拦截器
    |-- assets						# 静态文件目录
    |   |-- icon					# 字体图标目录
    |   |-- images					# 静态图片目录(背景图,logo)
    |   |-- logo.png
    |   |-- logo.svg
    |   `-- styles					# 自定义样式目录(由于使用框架,此目录少改动)
    |       `-- base.css       
    |-- common						# 公有目录
    |   |-- const.js					# 封装的js变量(不包括axios)
    |   `-- untils					# 封装的工具函数
    |		|-- cookieUntil.js			# cookie 操作工具函数
    |		|-- untils.js				# 未定义扩展工具函数
    |-- components					# 公共组件目录
    |   |-- HelloWorld.vue
    |   |-- common					# 封装的公共组件(非项目可以使用)
    |	|	|-- Loading.vue				# loading 动画组件
    |	|	|-- Snackbars.vue			# message 提示组件
    |   `-- content					# 封装的公共组件(本项目使用)
    |		|-- Header.vue				# 公共组件--appbar
    |		|-- Footer.vue				# 公共组件--footer
    |-- main.js						# vue入口文件
    |-- plugins
    |   `-- vuetify.js					# vuetify 基本配置文件
    |-- router						# vue-router相关配置
    |   |-- index.js					# 导出所有路由
    |   `-- routes.js					# 所有路由
    |-- store						# vuex相关配置
    |   |-- global					# 全局vuex
    |   |   |-- actions.js
    |   |   |-- index.js				# 导出全局vuex配置
    |   |   |-- mutations.js
    |   |   `-- state.js
    |   |-- index.js
    |   `-- modules					# 模块vuex
    |       |-- disease.js
    |		|-- index.js				# 导出所有modules vuex配置
    |       `-- pest.js
    `-- views						# 视图(路由)组件
        |-- home					# 主页面
        |   |-- Home.vue
        |   `-- components
        |       |-- Mian.vue
        |       |-- Header.vue
        |       |-- Search.vue      			#搜索框
        |       `-- Slidebar.vue
        `-- login					# 登录页面
            |-- Login.vue
            `-- components

===============================================================================
项目结构树文档更新规则:
	+ 新增文件
	~ 修改文件
	- 删除文件
view 目录中更新不需要写文档
===============================================================================
>> 第一次更新 + api目录 	新增 modules 目录 => 对应模块 API
>> 第二次更新 ~ api目录 	修改 instance.js 文件名称为 http.js
>> 第三次更新 + 根目录	       新增 vue.config.js 文件 => webpack 配置文件
>> 第四次更新 ~ common目录 	修改 untils.js 文件变更为 untils 文件夹
>> 第五次更新 + untils目录 	新增 cookieUntil.js 文件 untils.js 文件
>> 第六次更新 + components目录 新增 Loading.vue Snackbars.vue 两个组件
>> 第七次更新 + components目录 新增 Header.vue Footer.vue 两个组件
>> 第八次更新 + components目录 新增 svg-icon 组件
>> 第九次更新 + common目录 新增 ifEmety.js ifLogin.js两个全局方法
>> 第十次更新 + views目录 新增 两个页面

2. 封装 axios

2.1 相关库介绍

2.1.1 qs 库的使用

  • ajax请求的get请求是通过URL传参的(以?和&符连接)
  • post大多是通过json传参的。

qs是一个库。里面的stringify方法可以将一个json对象直接转为(以?和&符连接的形式)。在开发中,发送请求的入参大多是一个对象。

在发送时,如果该请求为get请求,就需要对参数进行转化。使用该库,就可以自动转化,而不需要手动去拼接

qs.stringify(config.data)			// 转化参数

2.1.2 nprogress

进度条动画,模拟网络

NProgress.start()					// 开启动画
NProgress.done()					// 结束动画

2.1.3 vuetify-toast-snackbar

snackbar message 提示插件,高度封装,调用方便

// vuetify-toast-snackbar 默认配置及可配置选项
Vue.use(VuetifyToast, {
    x: 'right', // default
    y: 'bottom', // default
    color: 'info', // default
    icon: 'info',
    iconColor: '', // default
    classes: [
        'body-2'
    ],
    timeout: 3000, // default
    dismissable: true, // default
    multiLine: false, // default
    vertical: false, // default
    queueable: false, // default
    showClose: false, // default
    closeText: '', // default
    closeColor: '', // default
    shorts: {
        custom: {
            color: 'purple'
        }
    },
    property: '$toast' // default
})
// 引入
Vue.use(Vuetify, {
  components: {
    VSnackbar,
    VBtn,
    VIcon
  }
})
// 使用
Vue.use(VuetifyToast)

// 案例
this.$toast('异常错误!', {
  x: 'right',
  y: 'top',
  icon: 'info',
  color: 'error',
  dismissable: false,
  showClose: true
})

本项目中将添加到原型上的$toast变量存储为vuex变量,变量名称为$toast

2.2 Axios 封装过程

2.2.1 相关库的导入

// http.js 所需库
import axios from 'axios'
import qs from 'qs'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

2.2.2 相关工具及其他文件导入

import config from '../config'
import { getCookie } from '@/common/untils/cookieUntil'		// 获取cookie 按操作
import globalStore from '@/store/global'					// 全局路由token或cookie或 jssessionid
import router from '@/router'								// 404、 跳转 login 跳转				

2.2.3 封装路由跳转

const routerToObj = (type) => {
  router.replace({
    path: `/${type}`,
    query: {
      redirect: router.currentRoute.fullPath
    }
  })
}
// 根据 type 传值 跳转不同的 view
routerToObj('login')

2.2.4 封装 HTTP 状态码 错误处理

const errorHandle = (status, other) => {
  // 状态码判断
  const items = [
    [404, '数据不存在,请刷新一下'],
    [405, '请求出错,请再一下'],
    [406, '您未登录,请先登录'],
    [409, '服务端数据操作错误'],
    [500, '服务器错误,请稍后再试'],
    [1000, '用户名或者密码错误'],
    [1001, '该号码未注册,请先注册'],
    [1002, '验证码错误'],
    [1003, '登录已过期,请重新登录'],
    [1004, '该号码已停用,请重新输入'],
    [1005, '请使用一个新的电话号码'],
    [1006, '未找到任何相关信息']
  ]
  console.log(other)
  const statusMap = new Map(items)
  return statusMap.get(status)
}

const serverErrorHanle = (status) => {
  const items = [
    [400, '请求错误'],
    [401, '未授权,请登录'],
    [403, '拒绝访问'],
    [404, '请求地址出错'],
    [408, '请求超时'],
    [500, '服务器繁忙,请稍后再试'],
    [501, '服务器未实现'],
    [502, '网络错误'],
    [503, '服务不可用'],
    [504, '网络超时'],
    [505, 'HTTP版本不受支持']
  ]
  const errorMap = new Map(items)
  return errorMap.get(status)
}

2.2.5 Axios 封装 骨架

  • $axios()
    • Promise(resolve, reject)
      • 创建 instance 实例,并导入 config.js 配置
      • instance.interceptors.request 请求拦截器
      • instance.interceptors.response 响应拦截器
      • instance 请求处理
export default function $axios (options) {
	return new Promise((resolve, reject) => {
        const instance = axios.create({...})
        instance.interceptors.request.use(...)
        instance.interceptors.response.use(...)
        instance.(options)
    })
}

2.2.6 HTTP 请求拦截器

2.2.6.1 Axios 响应结构
{
  // `data` 由服务器提供的响应
  data: {},

  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,

  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: 'OK',

  // `headers` 服务器响应的头
  headers: {},

  // `config` 是为请求提供的配置信息
  config: {},
  
  // `request` XMLHttpRequest 对象, client 响应结构体
  request: {
      readyState: 4,
      response: {
          // data 中的数据
      },
      responseText: {
          // data 中的数据
      },
      responseType: "",
      responseURL: "http://localhost:8080/eyes/index",
      responseXML: null,
      status: 200,
      statusText: "OK",
      timeout: 2000
  }
}
2.2.6.2 config 参数配置

请求响应参数

  • config 参数

Axios 中 设置 config

{
  // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认是 get

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  transformRequest: [function (data) {
    // 对 data 进行任意转换处理

    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
  // 对 data 进行任意转换处理

    return data;
  }],

  // `headers` 是即将被发送的自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是即将与请求一起发送的 URL 参数
  // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
  params: {
    ID: 12345
  },

  // `paramsSerializer` 是一个负责 `params` 序列化的函数
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求主体被发送的数据
  // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  // 在没有设置 `transformRequest` 时,必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属:FormData, File, Blob
  // - Node 专属: Stream
  data: {
    firstName: 'Fred'
  },

  // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
  // 如果请求话费了超过 `timeout` 的时间,请求将被中断
  timeout: 1000,

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // 默认的

  // `adapter` 允许自定义处理请求,以使测试更轻松
  // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
  adapter: function (config) {
    /* ... */
  },

  // `auth` 表示应该使用 HTTP 基础验证,并提供凭据
  // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
  responseType: 'json', // 默认的

  // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
  xsrfCookieName: 'XSRF-TOKEN', // default

  // `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认的

  // `onUploadProgress` 允许为上传处理进度事件
  onUploadProgress: function (progressEvent) {
    // 对原生进度事件的处理
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  onDownloadProgress: function (progressEvent) {
    // 对原生进度事件的处理
  },

  // `maxContentLength` 定义允许的响应内容的最大尺寸
  maxContentLength: 2000,

  // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认的
  },

  // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
  // 如果设置为0,将不会 follow 任何重定向
  maxRedirects: 5, // 默认的

  // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
  // `keepAlive` 默认没有启用
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // 'proxy' 定义代理服务器的主机名称和端口
  // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
  // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
  proxy: {
    host: '127.0.0.1',
    port: 9000,
    auth: : {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // `cancelToken` 指定用于取消请求的 cancel token
  // (查看后面的 Cancellation 这节了解更多)
  cancelToken: new CancelToken(function (cancel) {
  })
}

alt

2.2.6.3 请求拦截器
 // http request 拦截器
    instance.interceptors.request.use(
      config => {
        if (config) {
          NProgress.start()
        }
        console.log(config)
        // 获取 cookie
        const token = globalStore.state.sessionID
        // bug-2
        const loginMeta = config.url.toLocaleLowerCase().includes('common')
        console.log(`当前的token: ${token}`)
        if (!token && !loginMeta) {
          $toast('请先登录!', {
            x: 'right',
            y: 'top',
            icon: 'info',
            dismissable: false,
            showClose: true
          })
          // 没有 cookie 重定向到登录页
          setTimeout(() => {
            routerToObj('login')
          }, 2000)
        }
        // 根据请求方法,序列化传来的参数,根据后端需求是否序列化
        if ((config.method.toLocaleLowerCase() === 'post' && !config.headers.AuthorizationPhoto) ||
          config.method.toLocaleLowerCase() === 'put' ||
          config.method.toLocaleLowerCase() === 'delete') {
          config.data = qs.stringify(config.data)
        }
        return config
      },
      error => {
        console.log('request:', error)
        $toast('异常错误!', {
          x: 'right',
          y: 'top',
          icon: 'info',
          color: 'error',
          dismissable: false,
          showClose: true
        })
        //  1.判断请求超时
        if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
          $toast('抱歉,请求超时!', {
            x: 'right',
            y: 'top',
            icon: 'info',
            color: 'error',
            dismissable: false,
            showClose: true
          })
          console.log('请求超时')
          // 重新请求一次
          const originalRequest = error.config
          return instance.request(originalRequest)
        }
        //  2.需要重定向到错误页面
        const errorInfo = error.response
        $toast(`错误: ${errorInfo}`, {
          x: 'right',
          y: 'top',
          icon: 'info',
          color: 'error',
          dismissable: false,
          showClose: true
        })
        console.log(errorInfo)
        if (errorInfo) {
          // error =errorInfo.data//页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
          const errorStatus = errorInfo.status // 404 403 500 ... 等
          router.push({
            path: `/error/${errorStatus}`
          })
        }
        return Promise.reject(error) // 在调用的那边可以拿到(catch)你想返回的错误信息
      }
    )
2.2.6.4 响应拦截器
 // response 拦截器
    instance.interceptors.response.use(
      response => {
        // 拿到 header 上的 set-cookie
        let sessionCookie = getCookie(sessionId)
        if (sessionCookie) {
          local.set(sessionId, sessionCookie)
          globalStore.state.sessionID = local.get(sessionId)
        }
        // 1. 保存 response 中的数据
        //    注意: IE9时response.data是undefined,因此需要使用response.request.responseText(Stringify后的字符串)
        let data
        if (response.data === undefined) {
          data = response.request.responseText
        } else {
          data = response.data
        }
        // 2. 处理错误请求 ==> 根据返回的code值来做不同的处理
        const responseError = new Error()
        responseError.data = data
        responseError.response = response
        // 特殊状态码特殊处理
        if (data.errcode === 404) {
          setTimeout(() => {
            routerToObj('404')
          })
        } else if (data.code === 1003) {
          // 清除 localstorage cookie 跳转登录
          delCookie(sessionId)
          localStorage.clear()
          routerToObj('login')
        } else if (data.code === 500) {
          setTimeout(() => {
            routerToObj('500')
          })
        }
        responseError.message = errorHandle(data.code, response.data.message)
        // 3. 错误 => 显示错误消息 || 正常 => 结束 Nprogress 动画
        if (responseError.message) {
          $toast(`错误消息: ${responseError.message}`, {
            x: 'right',
            y: 'top',
            icon: 'info',
            color: 'error',
            dismissable: false,
            showClose: true,
            timeout: 2000
          })
        } else {
          NProgress.done()
        }
        return data
      },
      err => {
        NProgress.done()
        if (err && err.response) {
          err.message = serverErrorHanle(err.response.status)
        }
        if (err.toString().includes('timeout')) {
          err.message = '抱歉,服务器超时,请稍后再试!'
        }
        // 显示错误消息
        $toast(`错误: ${err.message}`, {
          x: 'right',
          y: 'top',
          icon: 'info',
          color: 'error',
          dismissable: false,
          showClose: true
        })
        // 404 500 状态码跳转
        if (err.response.status) {
          routerToObj(`${err.response.status}`)
        }
        console.error(`错误消息: ${err}`)
        return Promise.reject(err) // 返回接口返回的错误信息
      }
    )
2.2.6.5 封装成插件
// 导出所有接口
import apiList from './service/modules'

const install = Vue => {
  if (install.installed) {
    return
  }
  install.installed = true
  Object.defineProperties(Vue.prototype, {
    // 注意哦,此处挂载在 Vue 原型的 $api 对象上
    $api: {
      get () {
        return apiList
      }
    }
  })
}

export default install

// this.$api.common.login()
this.$api.common.loginout().then(res => {console.log(res)})

2.3 API 封装

2.3.1 modules 导出模块

  • common 公共模块接口
  • expert 专家模块接口
  • ordinary 普通用户模块接口
=========================================================================
本代码所在文件 index.js
=========================================================================
import common from './common'
import expert from './expert'
import ordinary from './ordinary'

export default {
  common,
  expert,
  ordinary
}

2.3.2 Common模块

名称 接口 地址 方法
用户注销 loginout(phone) /common/loginout?phone=${phone} Get
用户登录 login(params) /common/login Post
验证码登录 loginSendCode (params) /common/login/sendcode Post
获取session数据 getSessionData () /common/get/user Get
普通用户注册 ordinaryRegister (params) /common/ordinary/register Post
专家用户注册 expertRegister (params) /common/expert/register Post
省市县三级联动 linkAge (level = 1, parentId = 0) /common/linkage?level=${level}&parentId=${parentId} Get
验证注册使用的电话 registerSendCode (params) /common/register/sendcode Post
个人信息修改 updateInfo (params) /common/user/modify Post
修改密码 updatePassword (params) /common/user/password Post
验证电话号码 phoneSendCode (params) /common/changephone/sendcode Post
验证新的电话号码 newPhoneSendCode (params) /common/modifyphone/sendcode Post
验证验证码 verificationPhone (params) /common/phone/verification Post
绑定电话修改 updatePhone (params) /common/user/phone Post
头像修改 updateAvatar (formData, config) /common/user/photo Post
作物信息检索 getPlantByName (params) /common/plant/foundbyname Post
显示作物的详细信息 getPlantInfo (params) /common/plant/info Post
根据作物科属联动检索 getPlantByCategory (params) /common/plant/foundbycate Post
根据作物图像检索 getPlantInfo (plantID) /common/plant/info?plantID=${plantID} Get
根据天敌名称检索 getEnemyByName (params) /common/enemy/foundbyname Post
显示天敌详细信息 getEnemyInfo (enemyid) /common/enemy/info?enemy=${enemyid} Post
根据天敌科属联动检索 getEnemyByCategory (params) /common/enemy/foundbycate Post
显示界门纲目科属的所属关系 getCategory (params) /common/category/found Post
根据天敌图像检索 getEnemyByPhoto (params) /common/enemy/foundbyphoto Post
根据病害名称检索 getDiseaseByName (params) /common/disease/foundbyname Post
根据虫害名称检索 getPestByName (params) /common/pest/foundbyname Post
根据虫害科属联动检索 getPestByCategory (params) /common/pest/foundbycate Post
植保信息检索 getProtectInfo (keyword = "稻瘟病", PageNum = 1, PageSize = 3) /common/protect/foundbykey?keyword=${keyword}&PageNum=${PageNum}&PageSize=${PageSize} Get
咨询信息检索 getQuestionInfo (keyword = "水稻", PageNum = 1, PageSize = 3) /common/message/foundbykey?keyword=${keyword}&PageNum=${PageNum}&PageSize=${PageSize} Get

2.3.3 Expert 模块

名称 接口 地址 方法
咨询信息检索 getAnswerInfo (pagenum = 1, pagesize = 3) /expert/anwserbyexpertid/find?page=${pagenum}&pagesize=${pagesize} Get
回答咨询 addAnswerInfo (params) /expert/answerbyexpertid/add Post
修改自己相关回答 updateAnswer (params) /expert/answer/update Post
删除自己相关回答 deleteAnswer (params) /expert/answer/remove Post
诊断规则查询 getRule (ruleid = 1, pagenum = 1, pagesize = 3) /expert/rule/find?ruleid=${ruleid}&pagenum=${pagenum}&pagesize=${pagesize} Get
增添诊断规则 addRule (formData, config) /expert/rule/save Get
删除自己相关的规则 deleteRule (ruleid) /expert/rule/remove?ruleid=${ruleid} Get
修改自己录入的规则 updateRule (params) /expert/rule/update Post
默认查询诊断模型 getModel (pagenum = 1, pagesize = 3) /expert/model/find?pagenum=${pagenum}&pagesize=${pagesize} Get
增添诊断模型 addModel (params) /expert/model/add Post
删除自己录入的模型 deleteModel (modelId) /expert/model/remove?modelID=${modelId} Get
修改自己录入的模型 updateModel (params) /expert/model/update Post
胁迫情况分析--概要信息 getDuressMap () /expert/operating/map Get
胁迫情况分析--详细信息 getDuressMapInfo (pagenum = 1, pagesize = 3) /expert/information/find?pagenum=${pagenum}&pagesize=${pagesize} Get
添加胁迫情报 addDuress (params) /expert/information/save Post
删除自己录入的胁迫情报 deleteDuress (infoid) /expert/information/remove?infoid=${infoid} Get
修改自己录入的胁迫情报 updateDuress (params) /expert/information/update Post

2.3.4 Ordinary 模块

名称 接口 地址 方法
查询自己的咨询 getOrdinarySelect (page = 1, pageSize = 2) /ordinary/quest/select?page=${page}&pageSize=${pageSize} Get
查询全部咨询 getOrdinarySelectAll (page = 1, pageSize = 2) /ordinary/quest/selectAll?page=${page}&pageSize=${pageSize} Get
添加咨询 addQuestion (formData, config) /ordinary/quest/insert Post
修改自己的咨询--文字 updateQuestionText (params) /ordinary/quest/update Post
修改添加图片内容 updateQuestionAddImg (formData, config) /ordinary/quest/insertImage Post
删除图片 updateQuestionDelImg (questID, imageID) /ordinary/quest/deleteImage?questID=${questID}&imageID=${imageID} Get
删除咨询 deleteQuestion (questID) /ordinary/quest/delete?questID=${questID} Get
胁迫情报上传 uploadDuress (formData, config) /ordinary/info/insert Post

3. 工具函数

3.1 Cookie 操作

  • 设置 cookie setCookie('cookie1', 'eyes', 50000)
  • 获取 cookie getCookie(cookie1)
  • 删除 cookie delCookie(cookie1)
// 设置cookie
export function setCookie (cName, value, expire) {
  var date = new Date()
  date.setSeconds(date.getSeconds() + expire)
  document.cookie = cName + '=' + escape(value) + '; expires=' + date.toGMTString()
  console.log(document.cookie)
};
// 获取cookie
export function getCookie (cName) {
  if (document.cookie.length > 0) {
    let cStart = document.cookie.indexOf(cName + '=')
    if (cStart !== -1) {
      cStart = cStart + cName.length + 1
      let cEnd = document.cookie.indexOf(';', cStart)
      if (cEnd === -1) cEnd = document.cookie.length
      return unescape(document.cookie.substring(cStart, cEnd))
    }
  }
  return ''
};
/* 删除cookie */
export function delCookie (cName) {
  setCookie(cName, '', -1)
};

3.2 判断表单是否缺省

form数据格式为数组的情况下

import Vue from 'vue'
// 拿到 Vue 原型上的 $toast 对象
const prototype = Vue.prototype
export function ifEmety (data) {
  for (let key in data) {
    if (!data[key]) {
      prototype.$toast('请完整填写表单', {
        x: 'right',
        y: 'top',
        icon: 'info',
        dismissable: false,
        showClose: true
      })
      return false
    }
  }
  return true
}

3.3 判断是否登录

判断localStorage是否有用户信息,没有则提示登录并跳转

import Vue from 'vue'
import router from '@/router'

// 拿到 Vue 原型上的 $toast 对象
const prototype = Vue.prototype

export function ifLogin () {
  // 判断是否登录
  if (!localStorage.getItem('userInfo')) {
    prototype.$toast('请先登录!', {
      x: 'right',
      y: 'top',
      icon: 'info',
      dismissable: false,
      showClose: true
    })
    // 跳转到登录页
    setTimeout(() => {
      router.replace({
        path: `/login`,
        query: {
          redirect: router.currentRoute.fullPath
        }
      })
    }, 1000)
    return false
  }
  return true
}

4. 路由封装

4.1 Vue.router介绍

路由拦截 beforeEach 拦截器

router.beforeEach((to, from, next)) => {
	...
}
/** 
 * to表示即将进入的页面路由,
 * to.fullPath 当前点击的页面 登录完成跳转页面
 * from表示当前导航正要离开的路由
 * next: Function:执行效果依赖 next 方法的调用参数。
 * next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
 * next(false): 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
 * next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
 * next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。
 */

4.2 封装路由

以不同模块拆分路由,便于管理和后期维护

  • index.js 导出所有的路由
  • common.js 公共模块路由
  • expert.js 专家模块路由
  • ordinary.js 普通用户路由

4.2.1 公共模块路由

路由各参数设置

  • path: 跳转路径 或者
  • name: 路由名称
  • meta: 路由自定义属性
    • title: 页面标题
    • requireAuth: 是否需要登录 false true
  • component: Home
========================================================================================
   common.js 文件
========================================================================================
const COMMON_ROUTER = [{
  path: '/',
  name: 'Home',
  meta: {
    title: '主页',
    requireAuth: false
  },
  // 跳默认路由用redirect
  component: () => import(/* webpackChunkName: "Home" */ '@/views/home/Home.vue')
},
{
  path: '/login',
  name: 'Login',
  meta: {
    title: '登录',
    requireAuth: false
  },
  component: () => import(/* webpackChunkName: "Login" */ '@/views/login/Login.vue')
},
{
  path: '/register',
  name: 'Register',
  meta: {
    title: '注册',
    requireAuth: false
  },
  component: () => import(/* webpackChunkName: "Register" */ '@/views/register/Register.vue')
},
{
  path: '/404',
  name: '404',
  meta: {
    title: '404',
    requireAuth: false
  },
  component: () => import(/* webpackChunkName: "404" */ '@/views/error/404/404.vue')
},
{
  path: '/500',
  name: '500',
  meta: {
    title: '500',
    requireAuth: false
  },
  component: () => import(/* webpackChunkName: "500" */ '@/views/error/500/500.vue')
}
]

export default COMMON_ROUTER

4.2.2 普通用户模块

const ORDINARY_ROUTER = [
	...
]

export default ORDINARY_ROUTER

4.2.3 专家用户模块

const EXPERT_ROUTER = [
	...
]

export default EXPERT_ROUTER

4.2.4 合并路由

Object.assign 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象

/*
  Object.assign() 方法讲解
  合并具有相同属性的对象
  复制对象 (浅拷贝)
  合并对象
*/

=========================================================
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source)
console.log(target)	// { a: 1, b: 4, c: 5 }
console.log(returnedTarget); // { a: 1, b: 4, c: 5 }
=========================================================
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
=========================================================
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }注意目标对象自身也会改变
============================================================

合并路由后导出

// 合并所有的路由
const routerObj = Object.assign(
  common,
  expert,
  ordinary
)

const router = new Router({
  routes: routerObj		// 注意是 routes 而不是 routers
})

路由拦截器中使用了 Nprogress 插件,每次跳转路由时都会加载进度条动画

路由拦截器(权限管理)

根据 vuex 中 sessionID 是否存在,然用户进行跳转登录

后面会添加 登录过期 判断

// 设置路由拦截
router.beforeEach((to, from, next) => {
  NProgress.start()
  /* 路由发生变化修改页面title */
  if (to.meta.title) {
    document.title = to.meta.title
  }
  // 验证是否需要登录
  if (to.matched.some(res => res.meta.requireAuth)) {
    // 查询本地是否登录
    if (this.$store.state.sessionID) {
      next()
    } else {
      // 避免登录死循环
      if (to.fullPath === '/login') {
        next()
      } else {
        next({
          path: '/login',
          query: {
            redirect: to.fullPath
          }
        })
      }
    }
  } else {
    next()
  }
  NProgress.done()
})

5. vuex

5.1 前端模块化

在讲解本项目vuex前了解一下原始JS开发中会遇到的问题。特别是大型项目中,我们经常遇到我们需要使用一些全局变量或者全局方法,但想从一个js文件中到另外一个js文件中取值是一个很头疼的事情。

  • 命名冲突:如果你在a.js定义了一个a()方法或者函数,你想把其引入到b.js使用。但是你不小心也在b.js中写了一个a(),那么问题来了来了这个a()是谁的方法的?

    • // a.js
      <script>
          let a = fun => {
              console.log('额是a')
          }
      </script>
    • //b.js
      <script src="a.js"></script>
      <script>
          let a = fun => {
              console.log('额是b')
          }
          a()
      </script>
    • 输出结果:'额是b'

  • 全局变量污染:同样的道理,如果你在a.js中定义了一个a变量(全局变量),你把a.js引入到了b.js中!然后你又在b.js中定义了一个全局变量a,这个a变量将会是哪里的呢?

    • // a.js
      <script>
          let a = '额是a'
      </script>
    • <script src="a.js"></script>
      <script>
          let a = '额是b'
      	console.log(a)
      </script>
    • 输出结果:'额是b

  • 文件相互依赖:我们使用jquery插件时,需要先把jquery引入,把jquery.js放到最前面,再把插件放到jQuery的后面

    •   <script src="lib/jquery/jquery.js"></script>
        <script src="lib/jquery/jquery.appear.js"></script>

解决这几个问题,前端提出了模块化的思想。什么是模块化?模块化就是将一个复杂的系统分解成多个独立的模块的代码组织方式

在前端没有正确引入模块化机制前,有很多社区版的模块化方案

  • IIFE(自执行函数)
  • AMD(require.js)
  • CMD(sea.js)
  • ES6(module)
    • 现阶段模块化解决方案
  • Common.js
    • 每个文件就是一个模块,都有自己单独一个作用域。
    • 同步
    • 服务端

5.2 vuex作用

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

翻译一下:现如今的web应用大多就是操作数据,对数据进行保存、操作和管理就是web应用的核心思想。

那这些数据我们怎么保存呢?全局变量?localstorage?显然这两种方式能够保存数据。那我们也要操作和管理数据怎么办呢?比如后端返回的JSON数据需要我们做一下什么什么预处理,我们想保存数据的同时就做了这些数据处理。如果使用全局变量或者localStorage我们需要写配套的数据处理逻辑,那么问题来了,localStorage里面的内容改了你组件数据会改吗? (可以做一下onStorage监听,恭喜你要做出一个类vuex的东西了)。

这样的数据有了个100、1000个会怎么办!这里面的数据如果还有的相互依赖我们有会怎么办?那就需要花费大量的时间和精力进行分析封装和管理。

而Vuex就是把数据和数据处理逻辑全部集中到一个单独的模块管理对象上,让这个对象进行管理。

此外由于Vue是单向数据流,因为用户一些行为(Actions)导致了数据发生了变化(State),数据驱动页面视图(View)改变,视图更新后又有会有一些行为(Actions)用户可以触发,以此循环。

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

image.png

在这种单向数据流的模式下,如果多个组件视图中都依赖同一个状态(state),这个状态(state)改变需要更改多个视图或怎么样?同样的,多个视图的一些行为会更改同一个状态又会怎么样?

在这种多组件共享状态的情况下,其实Vue中仍然有一些方法可以解决。

如果一个状态(state)改变需要更改多个视图,可以利用组件传值的方法:就是如果我这里数据发生了改变,我把它传给同时需要这个数据状态的组件,告诉他需要的这个state发生了改变,让其更改视图。

组件间的传值主要有

  • 兄弟组件 ---兄弟组件(bus.js)
  • 父组件---子组件(props down)
  • 子组件---父组件(emit on)

如果多个视图Actions更改同一个状态,可以利用本地化存储localStorage以及监听或变更和同步多个拷贝状态。

以上都比较麻烦且不容易维护,所以Vue把这种共享状态分离出来,用一个全局对象状态树管理,这就是状态管理模式Vuex。

本项目的状态管理分为了两个部分,为什么要分为两个部分,因为使用一个状态树想要把状态全部集中起来太过繁琐复杂且臃肿。目录结构如下。

.
|-- global
|   |-- actions.js
|   |-- getters.js
|   |-- index.js
|   |-- mutation-types.js
|   |-- mutations.js
|   `-- state.js
|-- index.js
`-- modules // 每个模块都有其对应的 state getters action mutations
    |-- disease.js
    |-- index.js
     `-- pest.js

本项目使用vuex主要是为了解决组件传值问题和管理多个组件的共享状态,

  • 全局状态管理 global
  • 局部状态管理
  • 病害
  • 植物
  • 天敌

来看一下store文件夹顶端index.js做了什么事情。

Store是Vuex的一个核心方法(仓库),这个仓库装着你大部分的state。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
import untils from './common/untils/untils'
import api from './api'
import './assets/icon/index'

Vue.use(api)

Vue.prototype.$untils = untils

Vue.config.productionTip = false

new Vue({
  router,
  store,			// 注入到Vue实例里
  vuetify,
  render: h => h(App)
}).$mount('#app')

new Vuex.Store({}) 表示创建一个Vuex实例,通常情况下,他需要注入到Vue实例里。

Vuex Store是响应式的,当Vue组件从store中读取状态(state选项)时,若store中的状态发生更新时,他会及时的响应给其他的组件(类似双向数据绑定) 而且不能直接改变store的状态,改变状态的唯一方法就是,显式地提交更改(mutations选项)

import Vue from 'vue'
import Vuex from 'vuex'
import state from './global/state'
import getters from './global/getters'
import actions from './global/actions'
import mutations from './global/mutations'

import disease from './modules/disease'
import pest from './modules/pest'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  getters,
  mutations,
  actions,
  modules: {
    disease,
    pest
  }
})
  • state:Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。

    • import { stroage } from '@/common/untils/strogeUntil.js'
      
      // 引用封装好的 stroage 构造器
      const local = stroage.local
      // 从 localStorege 中获取用户的详细信息
      const userInfo = '' || local.get('userInfo')
      // 创建唯一标识判断,判断登录状态
      const sessionID = stroage.local.get('JSESSIONID')
      // 从注册到登录信息
      const namePassword = null
      // 分页 name 属性
      const pageName = null
      
      export default {
        namePassword,
        userInfo,
        sessionID,
        pageName
      }
  • getter: 有时候我们需要从 store 中的 state 中派生出一些状态

    • // getters 只会依赖 state 中的成员去更新
      const getters = {
        userData: (state) => state.userInfo,
        sessionID: (state) => state.sessionID,
        namePassword: (state) => state.namePassword,
        pageName: (state) => state.pageName,
        category: (state) => state.category,
        diseaseName: (state) => state.diseaseName,
        pestDiseaseName: (state) => state.pestDiseaseName,
        imageName: (state) => state.imageName,
        searchText: (state) => state.searchText
      }
      
      export default getters
  • mutation: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数

    • import * as types from './mutation-types'
      import { stroage } from '@/common/untils/strogeUntil.js'
      
      const mutations = {
        // 定义一些突变的方法 如果不通过 commit('SET_ADDRESS', address) 会报错
        [types.SET_USERINFO] (state, data) {
          try {
            state.userInfo = data
            stroage.local.set('userInfo', state.userInfo)
          } catch (err) {
            console.log(`保存用户信息失败,${err}`)
          }
        },
        [types.REMOVE_USERINFO] (state) {
          state.userInfo = ''
          stroage.local.set('userInfo', state.userInfo)
        }
      }
      
      export default mutations
  • Action: Action 类似于 mutation,不同在于:

    • Action 提交的是 mutation,而不是直接变更状态。

    • Action 可以包含任意异步操作。

    • 多个 state 的操作 , 使用 mutations 会来触发会比较好维护 , 那么需要执行多个 mutations 就需要用 action

    • import * as types from './mutation-types'
      
      export default {
        // 异步 导出某些行为对象,业务逻辑
        setUserInfoData ({ commit }, param) {
          commit(types.SET_USERINFO, param)
        },
        removeUserInfoData ({ commit }) {
          commit(types.REMOVE_USERINFO)
        }
      }