Bladeren bron

multiple-way init and code completed

hare8999@163.com 1 jaar geleden
bovenliggende
commit
309a94c72c

+ 2 - 1
package.json

@@ -43,7 +43,7 @@
     "core-js": "^3.20.2",
     "docxtemplater": "^3.23.2",
     "echarts": "4.9.0",
-    "element-ui": "2.15.0",
+    "element-ui": "2.15.13",
     "file-saver": "^2.0.5",
     "fuse.js": "6.4.3",
     "highlight.js": "9.18.5",
@@ -68,6 +68,7 @@
     "vue-cropper": "0.5.5",
     "vue-esign": "^1.1.0",
     "vue-router": "3.4.9",
+    "vue-scrollto": "^2.20.0",
     "vue-video-player": "^5.0.2",
     "vuedraggable": "2.24.3",
     "vuex": "3.6.0",

+ 9 - 1
src/App.vue

@@ -1,14 +1,22 @@
 <template>
-  <div id="app">
+  <div id="app" :style="theme">
     <router-view/>
   </div>
 </template>
 
 <script>
 import { uaRedirect } from '@/utils/uaredirect'
+import CssVar from '@/assets/styles/variables.scss'
 
 export default {
   name: 'App',
+  data() {
+    return {
+      theme: {
+        '--themeColor': CssVar['theme'],
+      }
+    }
+  },
   mounted() {
     setTimeout(uaRedirect, 50)
   }

+ 41 - 0
src/api/webApi/multiple-way.js

@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+export function getMultipleWayHistories(params) {
+  return request(({
+    url: '/front/multiway/list',
+    method: 'get',
+    params
+  }))
+}
+export function getMultipleWayArea(params) {
+  return request(({
+    url: '/front/multiway/area',
+    method: 'get',
+    params
+  }))
+}
+
+export function getMultipleWayReport(params) {
+  return request(({
+    url: '/front/multiway/report',
+    method: 'get',
+    params
+  }))
+}
+
+export function submitMultipleWayForm(data) {
+  return request(({
+    url: '/front/multiway/submit',
+    method: 'post',
+    data
+  }))
+}
+
+export function exportMultipleWayReport(params) {
+  return request(({
+    url: '/front/multiway/export',
+    method: 'get',
+    responseType: 'blob',
+    params
+  }))
+}

BIN
src/assets/images/career/img_way.png


+ 15 - 0
src/assets/styles/common.scss

@@ -1,3 +1,5 @@
+@import "variables";
+
 .m0 {
   margin: 0;
 }
@@ -10,6 +12,10 @@
   margin-top: 60px;
 }
 
+.mt50 {
+  margin-top: 50px;
+}
+
 .mt40 {
   margin-top: 40px;
 }
@@ -580,16 +586,25 @@
 .text-white {
   color: #FFFFFF;
 }
+
 .bg-primary {
   background-color: #47C6A2;
   color: #FFFFFF;
 }
 
+.bg-success-lighter {
+  background-color: #f0f9eb;
+}
+
 .bg-red {
   background-color: #FD7C7C;
   color: #FFFFFF;
 }
 
+.bg-red-lighter {
+  background-color: #fef0f0;
+}
+
 .bg-page {
   background-color: #FEFEFE;
 }

+ 15 - 0
src/assets/styles/index.scss

@@ -215,3 +215,18 @@ aside {
 tr.highlight-row {
   color: red;
 }
+
+/* index-block width control by screen size. */
+@media screen and (min-width: 1440px) {
+  .index-block {
+    width: 1350px;
+    overflow: hidden;
+  }
+}
+
+@media screen and (max-width: 1439px) {
+  .index-block {
+    width: calc(100vw - 80px);
+    overflow: hidden;
+  }
+}

+ 3 - 0
src/assets/styles/variables.scss

@@ -26,9 +26,12 @@ $subMenuHover:#001528;
 
 $sideBarWidth: 250px;
 
+$--color-primary: #47C6A2;
+
 // the :export directive is the magic sauce for webpack
 // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
 :export {
+  theme: $--color-primary;
   menuText: $menuText;
   menuActiveText: $menuActiveText;
   subMenuActiveText: $subMenuActiveText;

+ 49 - 0
src/components/DictData/index.js

@@ -0,0 +1,49 @@
+import Vue from 'vue'
+import store from '@/store'
+import DataDict from '@/utils/dict'
+import { getDicts as getDicts } from '@/api/system/dict/data'
+
+function searchDictByKey(dict, key) {
+  if (key == null && key == "") {
+    return null
+  }
+  try {
+    for (let i = 0; i < dict.length; i++) {
+      if (dict[i].key == key) {
+        return dict[i].value
+      }
+    }
+  } catch (e) {
+    return null
+  }
+}
+
+function install() {
+  Vue.use(DataDict, {
+    metas: {
+      '*': {
+        labelField: 'dictLabel',
+        valueField: 'dictValue',
+        request(dictMeta) {
+          const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
+          if (storeDict) {
+            return new Promise(resolve => { resolve(storeDict) })
+          } else {
+            return new Promise((resolve, reject) => {
+              getDicts(dictMeta.type).then(res => {
+                store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
+                resolve(res.data)
+              }).catch(error => {
+                reject(error)
+              })
+            })
+          }
+        },
+      },
+    },
+  })
+}
+
+export default {
+  install,
+}

+ 59 - 0
src/components/DictTag/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="dict-tag">
+    <template v-for="(item, index) in options">
+      <template v-if="values.includes(item.value)">
+        <span
+          v-if="clearStyle || item.raw.listClass == 'default' || item.raw.listClass == ''"
+          :key="item.value"
+          :index="index"
+          :class="item.raw.cssClass"
+          class="dict-item"
+        >{{ item.label }}</span>
+        <el-tag
+          v-else
+          :key="item.value"
+          :disable-transitions="true"
+          :index="index"
+          :type="item.raw.listClass == 'primary' ? '' : item.raw.listClass"
+          :class="item.raw.cssClass"
+        >
+          {{ item.label }}
+        </el-tag>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DictTag',
+  props: {
+    options: {
+      type: Array,
+      default: null
+    },
+    value: [Number, String, Array],
+    clearStyle: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    values() {
+      if (this.value !== null && typeof this.value !== 'undefined') {
+        return Array.isArray(this.value) ? this.value : [String(this.value)]
+      } else {
+        return []
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.dict-tag {
+  .el-tag + .el-tag,
+  .dict-item + .dict-item {
+    margin-left: 10px;
+  }
+}
+</style>

+ 10 - 0
src/directive/hasHistory.js

@@ -0,0 +1,10 @@
+export default {
+  bind: function(el, binding, vnode) {
+    const router = vnode.context.$router
+    if (router.mode === 'history') {
+      if (window.history.length <= 1) {
+        el.style.display = 'none'
+      }
+    }
+  }
+}

+ 24 - 0
src/main.js

@@ -43,9 +43,11 @@ import Top from '@/components/Top'
 import MxVideo from '@/components/MxVideo'
 import FileUpload from '@/components/FileUpload'
 import ImageUpload from '@/components/ImageUpload'
+import hasHistory from "@/directive/hasHistory"
 
 // 用于静态弹窗 函数式调用
 import MxDialog from '@/components/MxDialog/index'
+import DictData from "@/components/DictData"
 
 Vue.prototype.$imgBase = 'https://mingxuejingbang.oss-cn-beijing.aliyuncs.com/mingxueMainImgs/'
 Vue.prototype.$Dialog = MxDialog
@@ -60,6 +62,7 @@ Vue.prototype.selectDictLabel = selectDictLabel
 Vue.prototype.selectDictLabels = selectDictLabels
 Vue.prototype.download = download
 Vue.prototype.handleTree = handleTree
+DictData.install()
 
 Vue.prototype.msgSuccess = function (msg) {
   this.$message({ showClose: true, message: msg, type: "success" });
@@ -79,6 +82,8 @@ Vue.use(ext)
 // filters
 import filters from "@/filters/index"
 Object.keys(filters).forEach(key => Vue.filter(key, filters[key]))
+// directives
+Vue.directive('has-history', hasHistory)
 // 电子签名
 import vueEsign from 'vue-esign'
 Vue.use(vueEsign)
@@ -102,6 +107,7 @@ Vue.component('Top', Top)//头部
 Vue.component('MxVideo', MxVideo) //试图替换系统的video组件
 Vue.component('FileUpload', FileUpload) // OSS文件上传
 Vue.component('ImageUpload', ImageUpload) // OSS文件上传
+Vue.component('DictTag', DictTag)
 
 Vue.use(permission)
 
@@ -113,6 +119,24 @@ const hls = require('videojs-contrib-hls')
 Vue.use(hls)
 Vue.use(VideoPlayer)
 
+// scroll to component
+import VueScrollTo from 'vue-scrollto'
+import DictTag from "@/components/DictTag/index.vue";
+const scrollToOpts = {
+  container: 'body', // 滚动的容器
+  duration: 500, // 滚动时间
+  easing: 'ease', // 缓动类型
+  offset: -90, // 滚动时应应用的偏移量。此选项接受回调函数
+  force: true, // 是否应执行滚动
+  cancelable: true, // 用户是否可以取消滚动
+  onStart: false, // 滚动开始时的钩子函数
+  onDone: false, // 滚动结束时候的钩子函数
+  onCancel: false, // 用户取消滚动的钩子函数
+  x: false, // 是否要在x轴上也滚动
+  y: true // 是否要在y轴上滚动
+}
+Vue.use(VueScrollTo, scrollToOpts)
+
 /**
  * If you don't want to use mock-server
  * you want to use MockJs for mock api

+ 18 - 0
src/router/index.js

@@ -691,6 +691,24 @@ export const constantRoutes = [{
           title: '职业-详情',
           parentPath: '/new-gaokao/three/Vocation'
         }
+      },
+      {
+        path: '/sygh/multiple-way/form',
+        component: (resolve) => require(['@/views/career/MultipleWay/form.vue'], resolve),
+        name: 'MultipleWayForm',
+        meta: {
+          title: '多元升学路径规划测评',
+          parentPath: '/new-gaokao/multiway/index'
+        }
+      },
+      {
+        path: '/sygh/multiple-way/report',
+        component: (resolve) => require(['@/views/career/MultipleWay/report.vue'], resolve),
+        name: 'MultipleWayReport',
+        meta: {
+          title: '多元升学路径规划报告',
+          parentPath: '/new-gaokao/multiway/index'
+        }
       }
     ]
   }, {

+ 1 - 0
src/store/getters.js

@@ -13,6 +13,7 @@ const getters = {
   introduction: state => state.user.introduction,
   school: state => state.user.busiSchool?.first(),
   schoolName: (state, getters) => getters.school?.schoolName,
+  firstGradeName: (state, getters) => getters.school?.grade?.first().gradeName,
   firstClassName: state => state.user.busiSchool?.first()?.grade?.first()?.clazz?.first()?.className,
   roleList: state => state.user.roleList || [],
   roles: (state, getters) => getters.roleList.map(r => r.roleKey),

+ 2 - 0
src/store/index.js

@@ -2,6 +2,7 @@ import Vue from 'vue'
 import Vuex from 'vuex'
 import app from './modules/app'
 import user from './modules/user'
+import dict from './modules/dict'
 import tagsView from './modules/tagsView'
 import permission from './modules/mx-permission'
 import settings from './modules/settings'
@@ -13,6 +14,7 @@ const store = new Vuex.Store({
   modules: {
     app,
     user,
+    dict,
     tagsView,
     permission,
     settings

+ 51 - 0
src/store/modules/dict.js

@@ -0,0 +1,51 @@
+const state = {
+  dict: []
+}
+const mutations = {
+  SET_DICT: (state, { key, value }) => {
+    if (key !== null && key !== '') {
+      state.dict.push({
+        key: key,
+        value: value
+      })
+    }
+  },
+  REMOVE_DICT: (state, key) => {
+    try {
+      for (let i = 0; i < state.dict.length; i++) {
+        if (state.dict[i].key == key) {
+          state.dict.splice(i, i)
+          return true
+        }
+      }
+    } catch (e) {
+      // do nothing
+    }
+  },
+  CLEAN_DICT: (state) => {
+    state.dict = []
+  }
+}
+
+const actions = {
+  // 设置字典
+  setDict({ commit }, data) {
+    commit('SET_DICT', data)
+  },
+  // 删除字典
+  removeDict({ commit }, key) {
+    commit('REMOVE_DICT', key)
+  },
+  // 清空字典
+  cleanDict({ commit }) {
+    commit('CLEAN_DICT')
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 22 - 0
src/utils/blob.js

@@ -0,0 +1,22 @@
+export function downloadBlobFile(response, filename) {
+  // download blob file
+  const url = window.URL.createObjectURL(new Blob([response.data]))
+
+  // get file suffix from response header
+  // build file name from data.name data.score data.batchName and suffix
+  let disposition = response.headers['content-disposition']
+  disposition = disposition.replace('"', '').replace("'", '')
+  const suffix = disposition.substring(disposition.lastIndexOf('.'), disposition.length - 1)
+  const fileName = `${filename}${suffix}`
+
+  // Create a download link and trigger a click event to download the file
+  const link = document.createElement('a')
+  link.href = url
+  link.setAttribute('download', fileName)
+  document.body.appendChild(link)
+  link.click()
+
+  // Remove the link element and release the object URL
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}

+ 82 - 0
src/utils/dict/Dict.js

@@ -0,0 +1,82 @@
+import Vue from 'vue'
+import { mergeRecursive } from "@/utils/ruoyi";
+import DictMeta from './DictMeta'
+import DictData from './DictData'
+
+const DEFAULT_DICT_OPTIONS = {
+  types: [],
+}
+
+/**
+ * @classdesc 字典
+ * @property {Object} label 标签对象,内部属性名为字典类型名称
+ * @property {Object} dict 字段数组,内部属性名为字典类型名称
+ * @property {Array.<DictMeta>} _dictMetas 字典元数据数组
+ */
+export default class Dict {
+  constructor() {
+    this.owner = null
+    this.label = {}
+    this.type = {}
+  }
+
+  init(options) {
+    if (options instanceof Array) {
+      options = { types: options }
+    }
+    const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options)
+    if (opts.types === undefined) {
+      throw new Error('need dict types')
+    }
+    const ps = []
+    this._dictMetas = opts.types.map(t => DictMeta.parse(t))
+    this._dictMetas.forEach(dictMeta => {
+      const type = dictMeta.type
+      Vue.set(this.label, type, {})
+      Vue.set(this.type, type, [])
+      if (dictMeta.lazy) {
+        return
+      }
+      ps.push(loadDict(this, dictMeta))
+    })
+    return Promise.all(ps)
+  }
+
+  /**
+   * 重新加载字典
+   * @param {String} type 字典类型
+   */
+  reloadDict(type) {
+    const dictMeta = this._dictMetas.find(e => e.type === type)
+    if (dictMeta === undefined) {
+      return Promise.reject(`the dict meta of ${type} was not found`)
+    }
+    return loadDict(this, dictMeta)
+  }
+}
+
+/**
+ * 加载字典
+ * @param {Dict} dict 字典
+ * @param {DictMeta} dictMeta 字典元数据
+ * @returns {Promise}
+ */
+function loadDict(dict, dictMeta) {
+  return dictMeta.request(dictMeta)
+    .then(response => {
+      const type = dictMeta.type
+      let dicts = dictMeta.responseConverter(response, dictMeta)
+      if (!(dicts instanceof Array)) {
+        console.error('the return of responseConverter must be Array.<DictData>')
+        dicts = []
+      } else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) {
+        console.error('the type of elements in dicts must be DictData')
+        dicts = []
+      }
+      dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts)
+      dicts.forEach(d => {
+        Vue.set(dict.label[type], d.value, d.label)
+      })
+      return dicts
+    })
+}

+ 17 - 0
src/utils/dict/DictConverter.js

@@ -0,0 +1,17 @@
+import DictOptions from './DictOptions'
+import DictData from './DictData'
+
+export default function(dict, dictMeta) {
+  const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS)
+  const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS)
+  return new DictData(dict[label], dict[value], dict)
+}
+
+/**
+ * 确定字典字段
+ * @param {DictData} dict
+ * @param  {...String} fields
+ */
+function determineDictField(dict, ...fields) {
+  return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f))
+}

+ 13 - 0
src/utils/dict/DictData.js

@@ -0,0 +1,13 @@
+/**
+ * @classdesc 字典数据
+ * @property {String} label 标签
+ * @property {*} value 标签
+ * @property {Object} raw 原始数据
+ */
+export default class DictData {
+  constructor(label, value, raw) {
+    this.label = label
+    this.value = value
+    this.raw = raw
+  }
+}

+ 38 - 0
src/utils/dict/DictMeta.js

@@ -0,0 +1,38 @@
+import { mergeRecursive } from "@/utils/ruoyi";
+import DictOptions from './DictOptions'
+
+/**
+ * @classdesc 字典元数据
+ * @property {String} type 类型
+ * @property {Function} request 请求
+ * @property {String} label 标签字段
+ * @property {String} value 值字段
+ */
+export default class DictMeta {
+  constructor(options) {
+    this.type = options.type
+    this.request = options.request
+    this.responseConverter = options.responseConverter
+    this.labelField = options.labelField
+    this.valueField = options.valueField
+    this.lazy = options.lazy === true
+  }
+}
+
+
+/**
+ * 解析字典元数据
+ * @param {Object} options
+ * @returns {DictMeta}
+ */
+DictMeta.parse= function(options) {
+  let opts = null
+  if (typeof options === 'string') {
+    opts = DictOptions.metas[options] || {}
+    opts.type = options
+  } else if (typeof options === 'object') {
+    opts = options
+  }
+  opts = mergeRecursive(DictOptions.metas['*'], opts)
+  return new DictMeta(opts)
+}

+ 51 - 0
src/utils/dict/DictOptions.js

@@ -0,0 +1,51 @@
+import { mergeRecursive } from "@/utils/ruoyi";
+import dictConverter from './DictConverter'
+
+export const options = {
+  metas: {
+    '*': {
+      /**
+       * 字典请求,方法签名为function(dictMeta: DictMeta): Promise
+       */
+      request: (dictMeta) => {
+        console.log(`load dict ${dictMeta.type}`)
+        return Promise.resolve([])
+      },
+      /**
+       * 字典响应数据转换器,方法签名为function(response: Object, dictMeta: DictMeta): DictData
+       */
+      responseConverter,
+      labelField: 'label',
+      valueField: 'value',
+    },
+  },
+  /**
+   * 默认标签字段
+   */
+  DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'],
+  /**
+   * 默认值字段
+   */
+  DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'],
+}
+
+/**
+ * 映射字典
+ * @param {Object} response 字典数据
+ * @param {DictMeta} dictMeta 字典元数据
+ * @returns {DictData}
+ */
+function responseConverter(response, dictMeta) {
+  const dicts = response.content instanceof Array ? response.content : response
+  if (dicts === undefined) {
+    console.warn(`no dict data of "${dictMeta.type}" found in the response`)
+    return []
+  }
+  return dicts.map(d => dictConverter(d, dictMeta))
+}
+
+export function mergeOptions(src) {
+  mergeRecursive(options, src)
+}
+
+export default options

+ 33 - 0
src/utils/dict/index.js

@@ -0,0 +1,33 @@
+import Dict from './Dict'
+import { mergeOptions } from './DictOptions'
+
+export default function(Vue, options) {
+  mergeOptions(options)
+  Vue.mixin({
+    data() {
+      if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) {
+        return {}
+      }
+      const dict = new Dict()
+      dict.owner = this
+      return {
+        dict
+      }
+    },
+    created() {
+      if (!(this.dict instanceof Dict)) {
+        return
+      }
+      options.onCreated && options.onCreated(this.dict)
+      this.dict.init(this.$options.dicts).then(() => {
+        options.onReady && options.onReady(this.dict)
+        this.$nextTick(() => {
+          this.$emit('dictReady', this.dict)
+          if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) {
+            this.$options.methods.onDictReady.call(this, this.dict)
+          }
+        })
+      })
+    },
+  })
+}

+ 191 - 175
src/utils/ruoyi.js

@@ -1,175 +1,191 @@
-/**
- * 通用js方法封装处理
- * Copyright (c) 2019 ruoyi
- */
-
-const baseURL = process.env.VUE_APP_BASE_API
-
-// 日期格式化
-export function parseTime(time, pattern) {
-	if (arguments.length === 0 || !time) {
-		return null
-	}
-	const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
-	let date
-	if (typeof time === 'object') {
-		date = time
-	} else {
-		if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
-			time = parseInt(time)
-		} else if (typeof time === 'string') {
-			time = time.replace(new RegExp(/-/gm), '/');
-		}
-		if ((typeof time === 'number') && (time.toString().length === 10)) {
-			time = time * 1000
-		}
-		date = new Date(time)
-	}
-	const formatObj = {
-		y: date.getFullYear(),
-		m: date.getMonth() + 1,
-		d: date.getDate(),
-		h: date.getHours(),
-		i: date.getMinutes(),
-		s: date.getSeconds(),
-		a: date.getDay()
-	}
-	const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
-		let value = formatObj[key]
-		// Note: getDay() returns 0 on Sunday
-		if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
-		if (result.length > 0 && value < 10) {
-			value = '0' + value
-		}
-		return value || 0
-	})
-	return time_str
-}
-
-// 表单重置
-export function resetForm(refName) {
-	if (this.$refs[refName]) {
-		this.$refs[refName].resetFields();
-	}
-}
-
-// 添加日期范围
-export function addDateRange(params, dateRange, propName) {
-	var search = params;
-	search.params = {};
-	if (null != dateRange && '' != dateRange) {
-		if (typeof (propName) === "undefined") {
-			search.params["beginTime"] = dateRange[0];
-			search.params["endTime"] = dateRange[1];
-		} else {
-			search.params["begin" + propName] = dateRange[0];
-			search.params["end" + propName] = dateRange[1];
-		}
-	}
-	return search;
-}
-
-// 回显数据字典
-export function selectDictLabel(datas, value) {
-	var actions = [];
-	Object.keys(datas).some((key) => {
-		if (datas[key].dictValue == ('' + value)) {
-			actions.push(datas[key].dictLabel);
-			return true;
-		}
-	})
-	return actions.join('');
-}
-
-// 回显数据字典(字符串数组)
-export function selectDictLabels(datas, value, separator) {
-	var actions = [];
-	var currentSeparator = undefined === separator ? "," : separator;
-	var temp = value.split(currentSeparator);
-	Object.keys(value.split(currentSeparator)).some((val) => {
-		Object.keys(datas).some((key) => {
-			if (datas[key].dictValue == ('' + temp[val])) {
-				actions.push(datas[key].dictLabel + currentSeparator);
-			}
-		})
-	})
-	return actions.join('').substring(0, actions.join('').length - 1);
-}
-
-// 通用下载方法
-export function download(fileName) {
-	window.location.href = baseURL + "/common/download?fileName=" + encodeURI(fileName) + "&delete=" + true;
-}
-
-// 字符串格式化(%s )
-export function sprintf(str) {
-	var args = arguments, flag = true, i = 1;
-	str = str.replace(/%s/g, function () {
-		var arg = args[i++];
-		if (typeof arg === 'undefined') {
-			flag = false;
-			return '';
-		}
-		return arg;
-	});
-	return flag ? str : '';
-}
-
-// 转换字符串,undefined,null等转化为""
-export function praseStrEmpty(str) {
-	if (!str || str == "undefined" || str == "null") {
-		return "";
-	}
-	return str;
-}
-
-/**
- * 构造树型结构数据
- * @param {*} data 数据源
- * @param {*} id id字段 默认 'id'
- * @param {*} parentId 父节点字段 默认 'parentId'
- * @param {*} children 孩子节点字段 默认 'children'
- */
-export function handleTree(data, id, parentId, children) {
-	let config = {
-		id: id || 'id',
-		parentId: parentId || 'parentId',
-		childrenList: children || 'children'
-	};
-
-	var childrenListMap = {};
-	var nodeIds = {};
-	var tree = [];
-
-	for (let d of data) {
-		let parentId = d[config.parentId];
-		if (childrenListMap[parentId] == null) {
-			childrenListMap[parentId] = [];
-		}
-		nodeIds[d[config.id]] = d;
-		childrenListMap[parentId].push(d);
-	}
-
-	for (let d of data) {
-		let parentId = d[config.parentId];
-		if (nodeIds[parentId] == null) {
-			tree.push(d);
-		}
-	}
-
-	for (let t of tree) {
-		adaptToChildrenList(t);
-	}
-
-	function adaptToChildrenList(o) {
-		if (childrenListMap[o[config.id]] !== null) {
-			o[config.childrenList] = childrenListMap[o[config.id]];
-		}
-		if (o[config.childrenList]) {
-			for (let c of o[config.childrenList]) {
-				adaptToChildrenList(c);
-			}
-		}
-	}
-	return tree;
-}
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+const baseURL = process.env.VUE_APP_BASE_API
+
+// 日期格式化
+export function parseTime(time, pattern) {
+	if (arguments.length === 0 || !time) {
+		return null
+	}
+	const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+	let date
+	if (typeof time === 'object') {
+		date = time
+	} else {
+		if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+			time = parseInt(time)
+		} else if (typeof time === 'string') {
+			time = time.replace(new RegExp(/-/gm), '/');
+		}
+		if ((typeof time === 'number') && (time.toString().length === 10)) {
+			time = time * 1000
+		}
+		date = new Date(time)
+	}
+	const formatObj = {
+		y: date.getFullYear(),
+		m: date.getMonth() + 1,
+		d: date.getDate(),
+		h: date.getHours(),
+		i: date.getMinutes(),
+		s: date.getSeconds(),
+		a: date.getDay()
+	}
+	const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+		let value = formatObj[key]
+		// Note: getDay() returns 0 on Sunday
+		if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+		if (result.length > 0 && value < 10) {
+			value = '0' + value
+		}
+		return value || 0
+	})
+	return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+	if (this.$refs[refName]) {
+		this.$refs[refName].resetFields();
+	}
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+	var search = params;
+	search.params = {};
+	if (null != dateRange && '' != dateRange) {
+		if (typeof (propName) === "undefined") {
+			search.params["beginTime"] = dateRange[0];
+			search.params["endTime"] = dateRange[1];
+		} else {
+			search.params["begin" + propName] = dateRange[0];
+			search.params["end" + propName] = dateRange[1];
+		}
+	}
+	return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+	var actions = [];
+	Object.keys(datas).some((key) => {
+		if (datas[key].dictValue == ('' + value)) {
+			actions.push(datas[key].dictLabel);
+			return true;
+		}
+	})
+	return actions.join('');
+}
+
+// 回显数据字典(字符串数组)
+export function selectDictLabels(datas, value, separator) {
+	var actions = [];
+	var currentSeparator = undefined === separator ? "," : separator;
+	var temp = value.split(currentSeparator);
+	Object.keys(value.split(currentSeparator)).some((val) => {
+		Object.keys(datas).some((key) => {
+			if (datas[key].dictValue == ('' + temp[val])) {
+				actions.push(datas[key].dictLabel + currentSeparator);
+			}
+		})
+	})
+	return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 通用下载方法
+export function download(fileName) {
+	window.location.href = baseURL + "/common/download?fileName=" + encodeURI(fileName) + "&delete=" + true;
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+	var args = arguments, flag = true, i = 1;
+	str = str.replace(/%s/g, function () {
+		var arg = args[i++];
+		if (typeof arg === 'undefined') {
+			flag = false;
+			return '';
+		}
+		return arg;
+	});
+	return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function praseStrEmpty(str) {
+	if (!str || str == "undefined" || str == "null") {
+		return "";
+	}
+	return str;
+}
+
+// 数据合并
+export function mergeRecursive(source, target) {
+  for (var p in target) {
+    try {
+      if (target[p].constructor == Object) {
+        source[p] = mergeRecursive(source[p], target[p])
+      } else {
+        source[p] = target[p]
+      }
+    } catch (e) {
+      source[p] = target[p]
+    }
+  }
+  return source
+}
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+	let config = {
+		id: id || 'id',
+		parentId: parentId || 'parentId',
+		childrenList: children || 'children'
+	};
+
+	var childrenListMap = {};
+	var nodeIds = {};
+	var tree = [];
+
+	for (let d of data) {
+		let parentId = d[config.parentId];
+		if (childrenListMap[parentId] == null) {
+			childrenListMap[parentId] = [];
+		}
+		nodeIds[d[config.id]] = d;
+		childrenListMap[parentId].push(d);
+	}
+
+	for (let d of data) {
+		let parentId = d[config.parentId];
+		if (nodeIds[parentId] == null) {
+			tree.push(d);
+		}
+	}
+
+	for (let t of tree) {
+		adaptToChildrenList(t);
+	}
+
+	function adaptToChildrenList(o) {
+		if (childrenListMap[o[config.id]] !== null) {
+			o[config.childrenList] = childrenListMap[o[config.id]];
+		}
+		if (o[config.childrenList]) {
+			for (let c of o[config.childrenList]) {
+				adaptToChildrenList(c);
+			}
+		}
+	}
+	return tree;
+}

+ 0 - 14
src/views/accurateTeaching/main.vue

@@ -100,18 +100,4 @@ export default {
     border: 1px solid #00CCB4;
   }
 }
-
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>

+ 444 - 0
src/views/career/MultipleWay/form.vue

@@ -0,0 +1,444 @@
+<template>
+  <div class="app-container bg-page fx-column fx-cen-cen">
+    <el-card shadow="never" class="bg-white vs-index-card index-block">
+      <template #header>
+        <el-steps :active="step" finish-status="success" simple>
+          <el-step title="1、请确定您的个人信息" class="pointer" @click.native="step=0" />
+          <el-step title="2、请确定您的家庭情况" class="pointer" @click.native="handleJump(1)" />
+          <el-step title="3、请确定您的升学意向" class="pointer" @click.native="handleJump(2)" />
+        </el-steps>
+      </template>
+      <el-form v-show="step==0" ref="form0" :model="form0" :rules="rule0" label-width="120px">
+        <div class="pl60 mb20">
+          <span class="f-333 f20 bold">请确定您的个人信息</span>
+          <span class="f-999 f18">(基础信息是保证测评结果准确的关键请您认真填写,平台将对您的信息进行严格保密)</span>
+        </div>
+        <el-form-item prop="gender" label="性别">
+          <el-radio-group ref="gender" v-model="form0.gender">
+            <el-radio v-for="item in dict.type.multiple_way_gender" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="age" label="年龄">
+          <el-input-number ref="age" v-model="form0.age" :min="10" :max="50" controls-position="right" />
+        </el-form-item>
+        <el-form-item prop="nation" label="民族">
+          <el-radio-group ref="nation" v-model="form0.nation">
+            <el-radio v-for="item in dict.type.multiple_way_nation" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="course0" label="科类">
+          <el-radio-group ref="course0" v-model="form0.course0">
+            <el-radio v-for="item in dict.type.multiple_way_course0" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="score" label="成绩">
+          <el-select ref="score" v-model="form0.score">
+            <el-option
+              v-for="item in dict.type.multiple_way_score"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+          <span class="ml12 f-999">以满分750为准</span>
+        </el-form-item>
+        <el-form-item prop="foreignL" label="外语语种">
+          <el-radio-group ref="foreignL" v-model="form0.foreignL">
+            <el-radio v-for="item in dict.type.multiple_way_foreign_language" :key="item.value" :label="item.value">
+              {{ item.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="foreignLScore" label="外语成绩">
+          <el-input-number
+            ref="foreignLScore"
+            v-model="form0.foreignLScore"
+            :min="0"
+            :max="150"
+            controls-position="right"
+          />
+          <span class="ml12 f-999">以满分150为准</span>
+        </el-form-item>
+        <el-form-item prop="foreignLSpoken" label="外语口语考试">
+          <el-radio-group ref="foreignLSpoken" v-model="form0.foreignLSpoken">
+            <el-radio
+              v-for="item in dict.type.multiple_way_foreign_language_spoken"
+              :key="item.value"
+              :label="item.value"
+            >{{ item.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="height" label="身高">
+          <el-input-number ref="height" v-model="form0.height" :min="0" :max="300" controls-position="right" />
+          <span class="ml12 f-999">厘米(CM)</span>
+        </el-form-item>
+        <el-form-item prop="weight" label="体重">
+          <el-input-number ref="weight" v-model="form0.weight" :min="0" :max="200" controls-position="right" />
+          <span class="ml12 f-999">公斤(KG)</span>
+        </el-form-item>
+        <el-form-item label="视力">
+          <div class="fx-row">
+            <span class="mr12 f12">左眼</span>
+            <el-form-item prop="eyesightL">
+              <el-input-number
+                ref="eyesightL"
+                v-model="form0.eyesightL"
+                :step="0.1"
+                :precision="1"
+                :min="0"
+                :max="5"
+                controls-position="right"
+              />
+            </el-form-item>
+            <span class="ml20 mr12 f12">右眼</span>
+            <el-form-item prop="eyesightR">
+              <el-input-number
+                ref="eyesightR"
+                v-model="form0.eyesightR"
+                :step="0.1"
+                :precision="1"
+                :min="0"
+                :max="5"
+                controls-position="right"
+              />
+            </el-form-item>
+            <span class="ml12 f-999">C字表,以满分5.0为准</span>
+          </div>
+        </el-form-item>
+        <el-form-item prop="disease" label="疾病情况">
+          <el-checkbox-group ref="disease" v-model="form0.disease">
+            <el-checkbox v-for="item in dict.type.multiple_way_disease" :key="item.value" :label="item.value">
+              {{ item.label }}
+              <el-popover v-if="item.raw.remark" trigger="hover" popper-class="multiple-way-pop">
+                {{ item.raw.remark }}
+                <i slot="reference" class="el-icon-question" />
+              </el-popover>
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item>
+          <el-button v-loading="loading" type="primary" class="step-button" @click="handleNext(0)">下一步</el-button>
+          <el-button v-has-history v-loading="loading" class="step-button" @click="$router.back()">返回</el-button>
+        </el-form-item>
+      </el-form>
+      <el-form v-show="step==1" ref="form1" :model="form1" :rules="rule1" label-width="220px">
+        <div class="pl60 mb20">
+          <span class="f-333 f20 bold">请确定您的家庭情况</span>
+          <span class="f-999 f18">(家庭环境信息是保证测评结果准确的关键请您认真填写,平台将对您的信息进行严格保密)</span>
+        </div>
+        <el-form-item label="户籍所在县市">
+          <div class="fx-row">
+            <el-form-item prop="province">
+              <el-select ref="province" v-model="form1.province" filterable @change="getCity">
+                <el-option v-for="item in provinces" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+            <span class="ml20 mr12">所在城市</span>
+            <el-form-item prop="city">
+              <el-select ref="city" v-model="form1.city" filterable @change="getDistrict">
+                <el-option v-for="item in cities" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item prop="district" class="ml12">
+              <el-select ref="district" v-model="form1.district">
+                <el-option v-for="item in districts" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </div>
+        </el-form-item>
+        <el-form-item prop="nativeType" label="学生本人户籍类型">
+          <el-radio-group ref="nativeType" v-model="form1.nativeType">
+            <el-radio v-for="item in dict.type.multiple_way_native_type" :key="item.value" :label="item.value">
+              {{ item.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="nativeSWP" label="父母户籍是否与本人一致">
+          <el-radio-group ref="nativeSWP" v-model="form1.nativeSWP">
+            <el-radio v-for="item in dict.type.sys_yes_no" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="nativeLocal" label="户籍、学籍高中是否在本地">
+          <el-radio-group ref="nativeLocal" v-model="form1.nativeLocal">
+            <el-radio v-for="item in dict.type.sys_yes_no" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="feeExpectable" label="可接受学费范围">
+          <el-radio-group ref="feeExpectable" v-model="form1.feeExpectable">
+            <el-radio v-for="item in dict.type.multiple_way_fee_expectable" :key="item.value" :label="item.value">
+              {{ item.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="criminalRecords" label="三代直系亲属是否有刑事记录">
+          <el-radio-group ref="criminalRecords" v-model="form1.criminalRecords">
+            <el-radio v-for="item in dict.type.sys_yes_no" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="frontierChildren" label="是否为边防子女">
+          <el-radio-group ref="frontierChildren" v-model="form1.frontierChildren">
+            <el-radio v-for="item in dict.type.sys_yes_no" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item>
+          <el-button v-loading="loading" type="primary" class="step-button" @click="handleNext(1)">下一步</el-button>
+          <el-button v-loading="loading" class="step-button" @click="handlePrev">上一步</el-button>
+        </el-form-item>
+      </el-form>
+      <el-form v-show="step==2" ref="form2" :model="form2" :rules="rule2" label-width="120px">
+        <div class="pl60 mb20">
+          <span class="f-333 f20 bold">请确定您的升学意向</span>
+          <span class="f-999 f18">(个人信息是保证测评结果准确的关键请您认真填写,平台将对您的信息进行严格保密)</span>
+        </div>
+        <el-form-item prop="toBeTeacher" label="师范生意向">
+          <el-radio-group ref="toBeTeacher" v-model="form2.toBeTeacher">
+            <el-radio v-for="item in dict.type.sys_yes_no" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="toBeGraduate" label="考研意向">
+          <el-radio-group ref="toBeGraduate" v-model="form2.toBeGraduate">
+            <el-radio v-for="item in dict.type.sys_yes_no" :key="item.value" :label="item.value">{{
+              item.label
+            }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item prop="awards" label="奖项/荣誉情况">
+          <el-checkbox-group ref="awards" v-model="form2.awards">
+            <el-checkbox v-for="item in dict.type.multiple_way_awards" :key="item.value" :label="item.value">
+              {{ item.label }}
+              <el-popover v-if="item.raw.remark" trigger="hover" popper-class="multiple-way-pop">
+                {{ item.raw.remark }}
+                <i slot="reference" class="el-icon-question" />
+              </el-popover>
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item>
+          <el-button v-loading="loading" type="primary" class="step-button" @click="handleSave(2)">查看报告</el-button>
+          <el-button v-loading="loading" class="step-button" @click="handlePrev">上一步</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getMultipleWayArea, submitMultipleWayForm } from '@/api/webApi/multiple-way'
+
+export default {
+  name: 'MultipleWayForm',
+  dicts: ['multiple_way_gender', 'multiple_way_nation', 'multiple_way_course0', 'multiple_way_score',
+    'multiple_way_foreign_language', 'multiple_way_foreign_language_spoken', 'multiple_way_disease',
+    'multiple_way_native_type', 'multiple_way_fee_expectable', 'multiple_way_awards', 'sys_yes_no'],
+  data() {
+    return {
+      loading: false,
+      step: 0,
+      form0: {
+        gender: null,
+        age: 18,
+        nation: null,
+        course0: null,
+        score: null,
+        foreignL: null,
+        foreignLScore: 0,
+        foreignLSpoken: null,
+        height: 0,
+        weight: 0,
+        eyesightL: 0,
+        eyesightR: 0,
+        disease: ['无疾病']
+      },
+      rule0: {
+        gender: [{ required: true, message: '请选择性别' }],
+        age: [{ required: true, message: '请输入年龄' }],
+        nation: [{ required: true, message: '请选择民族' }],
+        course0: [{ required: true, message: '请选择科类' }],
+        score: [{ required: true, message: '请选择分数情况' }],
+        foreignL: [{ required: true, message: '请选择外语语种' }],
+        foreignLScore: [{ required: true, message: '请输入英语成绩' }],
+        foreignLSpoken: [{ required: true, message: '请选择外语口语考试情况' }],
+        height: [{ required: true, message: '请输入身高' }],
+        weight: [{ required: true, message: '请输入体重' }],
+        eyesightL: [{ required: true, message: '请输入左眼视力' }],
+        eyesightR: [{ required: true, message: '请输入右眼视力' }],
+        disease: [{ required: true, message: '请选择疾病情况' }, {
+          validator: (r, v, cb) => {
+            if (v.includes('无疾病') && v.length > 1) cb('无疾病不能与其它选项同时选择')
+            else cb() // NOTE: PC must call this function while valid
+          }
+        }]
+      },
+      form1: {
+        province: null,
+        city: null,
+        district: null,
+        nativeType: null,
+        nativeSWP: null,
+        nativeLocal: null,
+        feeExpectable: null,
+        criminalRecords: null,
+        frontierChildren: null
+      },
+      rule1: {
+        province: [{ required: true, message: '请选择省份' }],
+        city: [{ required: true, message: '请选择城市' }],
+        district: [{ required: true, message: '请选择区/县' }],
+        nativeType: [{ required: true, message: '请选择户籍类型' }],
+        nativeSWP: [{ required: true, message: '请选择父母户籍是否与本人一致' }],
+        nativeLocal: [{ required: true, message: '请选择户籍、学籍高中是否在本地' }],
+        feeExpectable: [{ required: true, message: '请选择可接受学费范围' }],
+        criminalRecords: [{ required: true, message: '请选择三代直系亲属是否有刑事记录' }],
+        frontierChildren: [{ required: true, message: '请选择是否为边防子女' }]
+      },
+      form2: {
+        toBeTeacher: null,
+        toBeGraduate: null,
+        awards: ['无']
+      },
+      rule2: {
+        toBeTeacher: [{ required: true, message: '请选择师范生意向' }],
+        toBeGraduate: [{ required: true, message: '请选择考研意向' }],
+        awards: [{ required: true, message: '请选择奖项/荣誉情况' }, {
+          validator: (r, v, cb) => {
+            if (v.includes('无') && v.length > 1) cb('无不能与其它选项同时选择')
+            else cb() // NOTE: PC must call this function while valid
+          }
+        }]
+      },
+      provinces: [],
+      cities: [],
+      districts: []
+    }
+  },
+  mounted() {
+    this.getProvince({ keyword: '' })
+  },
+  methods: {
+    formErrorHandler(errors) {
+      const errorKeys = Object.keys(errors)
+      if (!errorKeys.length) return
+      const errorKeyDemo = errorKeys[0]
+      const errorEl = this.$refs[errorKeyDemo]?.$el
+      if (errorEl) this.$scrollTo(errorEl, { offset: -90 - 40 })
+      const errorDemo = errors[errorKeyDemo][0]?.message
+      if (errorDemo) setTimeout(() => this.msgError(errorDemo), 50)
+    },
+    async validateForm(step) {
+      try {
+        const formName = 'form' + step
+        const form = this.$refs[formName]
+        if (form) await form.validate()
+      } catch (e) {
+        console.log('validation error', e)
+        this.formErrorHandler(e)
+        return Promise.reject(e)
+      }
+    },
+    async handleNext(step) {
+      await this.validateForm(step)
+      this.step += 1
+    },
+    async handlePrev() {
+      this.step -= 1
+    },
+    async handleJump(step) {
+      if (this.step < step) {
+        for (let i = this.step; i < step; i++) {
+          this.step = i
+          await this.validateForm(i) // invalid form will stop here
+        }
+      }
+      this.step = step
+    },
+    async getProvince() {
+      const res = await getMultipleWayArea()
+      this.provinces = res.data
+    },
+    async getCity() {
+      this.form1.city = null
+      this.form1.district = null
+      this.cities = []
+      this.districts = []
+      const res = await getMultipleWayArea({ parentId: this.form1.province })
+      this.cities = res.data
+    },
+    async getDistrict() {
+      this.form1.district = null
+      this.districts = []
+      const res = await getMultipleWayArea({ parentId: this.form1.city })
+      this.districts = res.data
+    },
+    async handleSave(step) {
+      await this.validateForm(step)
+      const postForm = {
+        ...this.form0,
+        ...this.form1,
+        ...this.form2
+      }
+      if (postForm.disease) postForm.disease = postForm.disease.toString()
+      if (postForm.awards) postForm.awards = postForm.awards.toString()
+      postForm.provinceName = this.provinces.find(i => i.id == postForm.province)?.name
+      postForm.cityName = this.cities.find(i => i.id == postForm.city)?.name
+      postForm.districtName = this.districts.find(i => i.id == postForm.district)?.name
+      this.loading = true
+      try {
+        const res = await submitMultipleWayForm(postForm)
+        this.$router.push({ path: '/sygh/multiple-way/report', query: { formId: res.data }})
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.step-button {
+  width: 140px;
+}
+
+/deep/ .el-form-item__label {
+  padding-right: 20px;
+  font-weight: 400;
+}
+</style>
+<style lang="scss">
+.multiple-way-pop {
+  max-width: 40vw;
+  background-color: rgba(0, 0, 0, 0.7) !important;
+  color: #FFFFFF !important;
+}
+
+/* 三角箭头 */
+.multiple-way-pop[x-placement^="bottom"] .popper__arrow::after,
+.multiple-way-pop[x-placement^="bottom"] .popper__arrow {
+  border-bottom-color: rgba(0, 0, 0, 0.7) !important;
+}
+</style>

+ 143 - 0
src/views/career/MultipleWay/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="app-container bg-page fx-column fx-cen-cen">
+    <index-card class="width100" title="多元升学路径规划(Way)">
+      <div slot="more" class="fx-row">
+        <el-button size="small" round type="primary" @click="goForm()">进入评测</el-button>
+        <el-button size="small" round plain @click="historyVisible=true,getList()">查看记录</el-button>
+      </div>
+      <div class="fx-row fx-bet-sta">
+        <el-image style="width: 240px" :src="require('@/assets/images/career/img_way.png')" fit="contain" />
+        <div class="ml20 fx-1 fx-column">
+          <div class="text" style="display:flex;align-items: center;">
+            <div>
+              <span style="width:200px;display:inline-block">测评方向:学业规划</span>
+              <span style="width:200px;display:inline-block">测评时间:3-5分钟</span>
+            </div>
+            <div />
+          </div>
+          <div class="tabBox">
+            <template>
+              <el-tabs v-model="activeName">
+                <el-tab-pane label="测前说明" name="first">
+                  <ul class="cp-rule">
+                    <li>1、请确保在一个独立、安静的环境下一次性完成,勿受他人干扰。</li>
+                    <li>2、如遇到无法确定内容,根据个人想法选择填写即可。</li>
+                    <li>3、参加测试的人员请务必诚实、独立的回答问题,只有如此,才能得到有效的结果。</li>
+                  </ul>
+                </el-tab-pane>
+                <el-tab-pane label="测评介绍" name="second">
+                  <ul class="cp-rule">
+                    <li>随着新高考改革的推进,以“多元录取”为核心的升学方式替代了传统千军万马过独木桥的录取方式,国家鼓励更多的学生能够结合自身的情况选择合适的升学路径,本学业生涯路径测评会通过家庭环境、个人情况共计十余项指标精准定位最适合自己的升学路径,助力考生合理规划高中的学业,促进低分高就及科学的学业生涯规划。</li>
+                  </ul>
+                </el-tab-pane>
+                <el-tab-pane label="测评目的" name="third">
+                  <ul class="cp-rule">
+                    <li>1、本测评仅需准确填写个人信息即可,简单快捷帮您分析出多元的升学途径。</li>
+                    <li>2、本测评结论提供了高中学业的规划表,可与多元升学路径结合使用。</li>
+                  </ul>
+                </el-tab-pane>
+              </el-tabs>
+            </template>
+          </div>
+        </div>
+      </div>
+    </index-card>
+    <!--  历史  -->
+    <el-drawer :visible.sync="historyVisible" title="多元升学路径规划历史" size="70%" append-to-body>
+      <dynamic-table :rows="list" :columns="columns">
+        <template #action="{row}">
+          <el-button type="text" icon="el-icon-view" @click="goReport(row)">查看</el-button>
+        </template>
+      </dynamic-table>
+      <pagination v-if="total>queryParams.pageSize" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import IndexCard from '@/views/login/components/modules/shared/IndexCard.vue'
+import DynamicTable from '@/components/dynamic-table/index.vue'
+import { getMultipleWayHistories } from '@/api/webApi/multiple-way'
+
+export default {
+  name: 'MultipleWay',
+  components: { DynamicTable, IndexCard },
+  data() {
+    return {
+      activeName: 'first',
+      // history
+      historyVisible: false,
+      list: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 20
+      },
+      columns: [
+        { prop: 'createdTime', label: '时间' },
+        { prop: 'acton', label: '操作', slotBody: 'action' }
+      ]
+    }
+  },
+  methods: {
+    goForm() {
+      this.$router.push('/sygh/multiple-way/form')
+    },
+    goReport(row) {
+      const path = this.$router.resolve({ path: '/sygh/multiple-way/report', query: { formId: row.formId }})
+      window.open(path.href, '_blank')
+    },
+    async getList() {
+      const res = await getMultipleWayHistories(this.queryParams)
+      this.list = res.rows
+      this.total = res.total
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.cp-rule {
+  font-size: 14px;
+  color: #414141;
+  font-family: PingFangSC-Regular, PingFang SC, Helvetica, serif;
+  font-weight: 400;
+  padding: 0px;
+  width: 100%;
+  li {
+    width: 100%;
+    list-style: none;
+    padding: 8px 0;
+    line-height: 1.75;
+    word-wrap: normal;
+    white-space: pre-wrap;
+  }
+}
+.text {
+  margin-bottom: 16px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC, Helvetica, serif;
+  font-weight: 400;
+  justify-content: space-between;
+}
+</style>
+<style lang="scss" scoped>
+.tabBox {
+  .el-tabs {
+    .el-tabs__header {
+      .el-tabs__nav-wrap {
+        .el-tabs__nav-scroll {
+          .el-tabs__nav {
+            .el-tabs__active-bar {
+              background-color: var(--themeColor) !important;
+            }
+            .el-tabs__item.is-active {
+              color: var(--themeColor) !important;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 291 - 0
src/views/career/MultipleWay/report.vue

@@ -0,0 +1,291 @@
+<template>
+  <div class="app-container bg-page fx-column fx-cen-cen">
+    <index-card class="index-block" title="多元升学路径报告" :sub-title="nickName">
+      <div slot="more">
+        <el-button v-loading="loading" size="mini" type="primary" round @click="handleDownload">下载</el-button>
+        <el-button v-has-history size="mini" round @click="$router.push('/sygh/multiple-way/index')">返回</el-button>
+      </div>
+      <div class="fx-column">
+        <div class="f-666 report-content">
+          <i class="el-icon-info f-yellow" />
+          本报告是基于各省招生考试院及院校招生简章历史数据分析整理得出,并不能代表当前年份的精准录取条件要求,请以省招生考试院及院校发布最新数据为准。
+        </div>
+        <div v-if="report.createdTime" class="f-666 mt10 fx-row jc-end">
+          测评时间: {{ report.createdTime }}
+        </div>
+        <!--    introduce    -->
+        <div class="fx-row ai-center mt50">
+          <el-divider direction="vertical" class="multiway-title-divider" />
+          <span class="f24 bold f-333">多元升学路径介绍</span>
+        </div>
+        <div class="mt10 report-content">
+          随着新高考改革的推进,以"多元录取"为核心的升学方式替代了传统千军万马过独木桥的录取方式,国家鼓励更多的学生能够结合自身的情况选择合适的升学路径,本学业生涯路径测评会通过家庭环境,个人情况共计十余项指标精准定位最适合自己的升学路径,助力考生合理规划高中的学业,促进低分高就及科学的学业生涯规划。
+        </div>
+        <!--    form    -->
+        <div class="fx-row ai-center mt50">
+          <el-divider direction="vertical" class="multiway-title-divider" />
+          <span class="f24 bold f-333">个人信息汇总</span>
+        </div>
+        <el-divider content-position="left" class="mt30">
+          <span class="f20 bold">个人信息</span>
+        </el-divider>
+        <el-descriptions direction="vertical" :column="4" class="mt10" border>
+          <el-descriptions-item label="姓名">
+            {{ nickName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="性别">
+            <dict-tag :options="dict.type.multiple_way_gender" :value="formData.gender" />
+          </el-descriptions-item>
+          <el-descriptions-item label="年龄">
+            {{ formData.age }}
+          </el-descriptions-item>
+          <el-descriptions-item label="民族">
+            <dict-tag :options="dict.type.multiple_way_nation" :value="formData.nation" />
+          </el-descriptions-item>
+          <el-descriptions-item label="阶段">
+            {{ firstGradeName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="科类">
+            <dict-tag :options="dict.type.multiple_way_course0" :value="formData.course0" />
+          </el-descriptions-item>
+          <el-descriptions-item label="成绩情况">
+            <dict-tag :options="dict.type.multiple_way_score" :value="formData.score" />
+          </el-descriptions-item>
+          <el-descriptions-item label="外语语种">
+            <dict-tag :options="dict.type.multiple_way_foreign_language" :value="formData.foreignL" />
+          </el-descriptions-item>
+          <el-descriptions-item label="英语成绩">
+            {{ formData.foreignLScore }}
+          </el-descriptions-item>
+          <el-descriptions-item label="外语口语考试">
+            <dict-tag :options="dict.type.multiple_way_foreign_language_spoken" :value="formData.foreignLSpoken" />
+          </el-descriptions-item>
+          <el-descriptions-item label="身高">
+            {{ formData.height }}cm
+          </el-descriptions-item>
+          <el-descriptions-item label="体重">
+            {{ formData.weight }}kg
+          </el-descriptions-item>
+          <el-descriptions-item label="视力">
+            左眼{{ formData.eyesightL }} 右眼{{ formData.eyesightR }}
+          </el-descriptions-item>
+          <el-descriptions-item label="疾病情况" :span="3">
+            <dict-tag :options="dict.type.multiple_way_disease" :value="formData.disease" />
+          </el-descriptions-item>
+        </el-descriptions>
+        <el-divider content-position="left" class="mt30">
+          <span class="f20 bold">家庭情况</span>
+        </el-divider>
+        <el-descriptions direction="vertical" :column="3" class="mt10" border>
+          <el-descriptions-item label="户籍所在省">{{ formData.provinceName }}</el-descriptions-item>
+          <el-descriptions-item label="户籍所在市">{{ formData.cityName }}</el-descriptions-item>
+          <el-descriptions-item label="户籍所在县、区">{{ formData.districtName }}</el-descriptions-item>
+          <el-descriptions-item label="学生本人户籍类型">
+            <dict-tag :options="dict.type.multiple_way_native_type" :value="formData.nativeType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="父母户籍是否与本人一致">
+            <dict-tag :options="dict.type.sys_yes_no" :value="formData.nativeSWP" clear-style />
+          </el-descriptions-item>
+          <el-descriptions-item label="户籍、学籍高中是否在本地">
+            <dict-tag :options="dict.type.sys_yes_no" :value="formData.nativeLocal" clear-style />
+          </el-descriptions-item>
+          <el-descriptions-item label="可接受学费范围">
+            <dict-tag :options="dict.type.multiple_way_fee_expectable" :value="formData.feeExpectable" />
+          </el-descriptions-item>
+          <el-descriptions-item label="三代直系亲属是否有刑事记录">
+            <dict-tag :options="dict.type.sys_yes_no" :value="formData.criminalRecords" clear-style />
+          </el-descriptions-item>
+          <el-descriptions-item label="是否为边防子女">
+            <dict-tag :options="dict.type.sys_yes_no" :value="formData.frontierChildren" clear-style />
+          </el-descriptions-item>
+        </el-descriptions>
+        <el-divider content-position="left" class="mt30">
+          <span class="f20 bold">升学意向</span>
+        </el-divider>
+        <el-descriptions direction="vertical" :column="5" class="mt10" border>
+          <el-descriptions-item label="师范生意向">
+            <dict-tag :options="dict.type.sys_yes_no" :value="formData.toBeTeacher" clear-style />
+          </el-descriptions-item>
+          <el-descriptions-item label="考研意向">
+            <dict-tag :options="dict.type.sys_yes_no" :value="formData.toBeGraduate" clear-style />
+          </el-descriptions-item>
+          <el-descriptions-item label="奖项/荣誉情况" :span="3">
+            <dict-tag :options="dict.type.multiple_way_awards" :value="formData.awards" />
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+      <!--   passed   -->
+      <template v-if="validConclusions.length">
+        <div class="fx-row ai-center mt50">
+          <el-divider direction="vertical" class="multiway-title-divider" />
+          <span class="f24 bold f-333">多元升学路径介绍</span>
+        </div>
+        <div class="f20 bold f-success mt20">以下是适合您的多元升学路径项目</div>
+        <div v-for="c in validConclusions" :key="c.direction" class="mt30">
+          <div class="f20 bold f-333">{{ c.direction }}</div>
+          <template v-if="c.config">
+            <div class="f16 bold f-333 mt20">介绍</div>
+            <div class="f-666 mt20 report-content">{{ c.config.introduce }}</div>
+            <div class="f16 bold f-333 mt20">报考条件</div>
+            <div class="f-666 mt20 report-content">
+              <div
+                v-for="(r,idx) in c.config.rules"
+                :key="idx"
+                :class="{'mt10': idx>0}"
+                :style="{paddingLeft: (r.indent*20)+'px'}"
+              >{{ r.rule }}
+              </div>
+            </div>
+          </template>
+          <div class="mt20 pd20 rd12 bold f-primary bg-success-lighter">
+            <div>适配解析</div>
+            <div class="mt10">您符合{{ c.direction }}的报考条件</div>
+          </div>
+        </div>
+      </template>
+      <!--   failed   -->
+      <template v-if="invalidConclusions.length">
+        <div class="fx-row ai-center mt50">
+          <el-divider direction="vertical" class="multiway-title-divider" />
+          <span class="f24 bold f-333">多元升学路径介绍</span>
+        </div>
+        <div class="f20 bold f-red mt20">以下是不适合您的多元升学路径项目</div>
+        <div v-for="c in invalidConclusions" :key="c.direction" class="mt30">
+          <div class="f20 bold f-333">{{ c.direction }}</div>
+          <template v-if="c.config">
+            <div class="f16 bold f-333 mt20">介绍</div>
+            <div class="f-666 mt20 report-content">{{ c.config.introduce }}</div>
+            <div class="f16 bold f-333 mt20">报考条件</div>
+            <div class="f-666 mt20 report-content">
+              <div
+                v-for="(r,idx) in c.config.rules"
+                :key="idx"
+                :class="{'mt10': idx>0}"
+                :style="{paddingLeft: (r.indent*20)+'px'}"
+              >{{ r.rule }}
+              </div>
+            </div>
+          </template>
+          <div class="mt20 pd20 rd12 bold f-red bg-red-lighter">
+            <div>适配解析</div>
+            <div v-for="(f,idx) in c.failures" :key="idx" class="mt10">{{ idx+1 }}、{{ f }}</div>
+          </div>
+        </div>
+      </template>
+      <!--   schedule   -->
+      <div class="fx-row ai-center mt50">
+        <el-divider direction="vertical" class="multiway-title-divider" />
+        <span class="f24 bold f-333">多元升学路径月度规划</span>
+      </div>
+      <dynamic-table :columns="columns" :rows="schedules" border class="mt20" />
+    </index-card>
+  </div>
+</template>
+
+<script>
+import IndexCard from '@/views/login/components/modules/shared/IndexCard.vue'
+import { mapGetters } from 'vuex'
+import DynamicTable from '@/components/dynamic-table/index.vue'
+import { exportMultipleWayReport, getMultipleWayReport } from '@/api/webApi/multiple-way'
+import { downloadBlobFile } from '@/utils/blob'
+
+export default {
+  name: 'MultipleWayReport',
+  components: { DynamicTable, IndexCard },
+  dicts: ['multiple_way_gender', 'multiple_way_nation', 'multiple_way_course0', 'multiple_way_score',
+    'multiple_way_foreign_language', 'multiple_way_foreign_language_spoken', 'multiple_way_disease',
+    'multiple_way_native_type', 'multiple_way_fee_expectable', 'multiple_way_awards', 'sys_yes_no'],
+  data() {
+    return {
+      loading: false,
+      report: {},
+      directions: {},
+      schedules: [],
+      columns: [
+        { prop: 'period', label: '阶段', width: '150px' },
+        { prop: 'months', label: '月份', width: '150px' },
+        { prop: 'events', label: '事件' },
+        { prop: 'attentions', label: '注意事项' }
+      ]
+    }
+  },
+  computed: {
+    ...mapGetters(['nickName', 'firstGradeName']),
+    formId() {
+      return this.$route.query.formId
+    },
+    formData() {
+      // disease and awards are multiple choices, set default values to avoid split error
+      const rawForm = this.report?.content || { disease: '', awards: '' }
+      if (rawForm.disease) rawForm.disease = rawForm.disease.split(',')
+      if (rawForm.awards) rawForm.awards = rawForm.awards.split(',')
+      return rawForm
+    },
+    validConclusions() {
+      const validList = this.report?.validList || []
+      validList.forEach(c => c.config = this.directions[c.direction])
+      return validList
+    },
+    invalidConclusions() {
+      const invalidList = this.report?.invalidList || []
+      invalidList.forEach(c => c.config = this.directions[c.direction])
+      return invalidList
+    }
+  },
+  async mounted() {
+    await this.loadDirections()
+    await this.loadSchedules()
+    await this.getReport()
+  },
+  methods: {
+    async loadDirections() {
+      const res = await this.getConfigKey('multiple-way-directions')
+      this.directions = JSON.parse(res.msg)
+    },
+    async loadSchedules() {
+      const res = await this.getConfigKey('multiple-way-schedules')
+      this.schedules = JSON.parse(res.msg)
+    },
+    async handleDownload() {
+      if (!this.formId) return
+      this.loading = true
+      try {
+        const res = await exportMultipleWayReport({ formId: this.formId })
+        const fileName = `${this.nickName}多元升学路径规划`
+        downloadBlobFile(res, fileName)
+      } finally {
+        this.loading = false
+      }
+    },
+    async getReport() {
+      if (!this.formId) return
+      this.loading = true
+      try {
+        const res = await getMultipleWayReport({ formId: this.formId })
+        this.report = res.data
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.multiway-title-divider {
+  width: 6px;
+  background-color: var(--themeColor);
+}
+
+.report-content {
+  line-height: 24px;
+}
+
+.multiway-checkbox {
+  max-width: 60%;
+
+  .checkbox-item + .checkbox-item {
+    margin-left: 12px;
+  }
+}
+</style>

+ 0 - 13
src/views/career/index.vue

@@ -99,17 +99,4 @@ export default {
     border: 1px solid #47C6A2;
   }
 }
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>

+ 0 - 13
src/views/career/main.vue

@@ -81,17 +81,4 @@ export default {
     }
   }
 }
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>

+ 0 - 13
src/views/elective/main.vue

@@ -92,17 +92,4 @@ export default {
     border: 1px solid #00CCB4;
   }
 }
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>

+ 0 - 14
src/views/evaluating/main.vue

@@ -146,18 +146,4 @@ export default {
 .right {
   display: flex;
 }
-
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>

+ 0 - 14
src/views/index/login.vue

@@ -163,18 +163,4 @@ export default {
   max-height: 500px;
   width: 800px;
 }
-
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>

+ 71 - 0
src/views/login/components/modules/shared/IndexCard.vue

@@ -0,0 +1,71 @@
+<template>
+  <el-card shadow="never" class="bg-white vs-index-card">
+    <template #header>
+      <div class="fx-row fx-bet-base pf bold">
+        <slot name="title">
+          <div class="fx-row ai-base">
+            <span class="f-333 f24">{{ title }}</span>
+            <slot name="sub-title">
+              <span class="f-999 f18 ml30">{{ subTitle }}</span>
+            </slot>
+          </div>
+        </slot>
+        <slot name="more">
+          <div v-if="moreText" class="f-333 f12 pf bold pointer" @click="$emit('more')">
+            <span>{{ moreText }}</span>
+            <i class="el-icon-d-arrow-right index-card-more bold ml3" />
+          </div>
+        </slot>
+      </div>
+    </template>
+    <slot />
+  </el-card>
+</template>
+
+<script>
+export default {
+  name: 'IndexCard',
+  props: {
+    'title': {
+      type: String,
+      default: ''
+    },
+    'subTitle': {
+      type: String,
+      default: ''
+    },
+    'moreText': {
+      type: String,
+      default: ''
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.vs-index-card.el-card {
+  //border-radius: 0 !important;
+  //border: 0 !important;
+}
+
+.vs-index-card .el-card__header {
+  min-height: 60px;
+  padding: 25px 12px 10px 12px;
+}
+
+.vs-index-card .el-card__body {
+  padding: 28px 12px;
+}
+
+.vs-index-card .index-card-more {
+  color: #333333;
+}
+
+.vs-index-card .pointer:hover {
+  color: var(--themeColor);
+
+  .index-card-more {
+    color: var(--themeColor);
+  }
+}
+</style>

+ 36 - 0
src/views/login/components/modules/shared/IndexCardContent.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-row :gutter="gutter">
+    <el-col v-for="(item,idx) in list" :key="idx" :span="span(item)">
+      <slot v-bind="{item, index:idx}" />
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+export default {
+  name: 'IndexCardContent',
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    },
+    lineSize: {
+      type: Number,
+      default: 1
+    },
+    gutter: {
+      type: Number,
+      default: 20
+    }
+  },
+  methods: {
+    span(item) {
+      return item.span || (24 / this.lineSize)
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 0 - 14
src/views/questioncenter/main.vue

@@ -202,18 +202,4 @@ export default {
   padding: 5px 0;
   box-shadow: 0px 1px 4px 0px rgba(47, 78, 154, 0.14);
 }
-
-@media screen and (min-width: 1440px) {
-  .index-block {
-    width: 1350px;
-    overflow: hidden;
-  }
-}
-
-@media screen and (max-width: 1439px) {
-  .index-block {
-    width: calc(100vw - 80px);
-    overflow: hidden;
-  }
-}
 </style>