Pārlūkot izejas kodu

sop动态表单组件coding...

刘洋 1 gadu atpakaļ
vecāks
revīzija
02771d877b

+ 5 - 0
src/api/sop.js

@@ -0,0 +1,5 @@
+import { http } from '@/service/request.js'
+//sop列表
+export const getSopList = (data) => http.post('/api/admin/sop/list', data, { custom: { loading: true } })
+//流程详细信息接口
+export const getSopFlowView = (params) => http.post('/api/admin/flow/view', {}, { params, custom: { loading: true } })

+ 32 - 0
src/components/low-code/DATE.vue

@@ -0,0 +1,32 @@
+<template>
+  <view>
+    <u-input v-model="valueStr" type="select" :border="true" :disabled="!config.writable" @click="show = true" />
+    <!-- <u-select v-model="show" :list="options" @confirm="confirm"></u-select> -->
+    <u-calendar v-model="show" mode="date" @change="change"></u-calendar>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'DATE',
+    props: ['config', 'onChange'],
+    data() {
+      return {
+        value: '',
+        valueStr: '',
+        show: false
+      }
+    },
+    created() {
+      this.value = this.config.value || ''
+    },
+    methods: {
+      change(obj) {
+        this.valueStr = obj.result
+        this.value = new Date(obj.result).getTime()
+      }
+    }
+  }
+</script>
+
+<style></style>

+ 35 - 0
src/components/low-code/MULTIPLE_SELECT.vue

@@ -0,0 +1,35 @@
+<template>
+  <view>
+    <u-input v-model="value" type="select" :border="true" :disabled="!config.writable" @click="show = true" />
+    <MultPicker :show="show" :columns="options" @confirm="confirm" @cancel="show = false"></MultPicker>
+  </view>
+</template>
+
+<script>
+  import MultPicker from '@/components/mult-picker.vue'
+  export default {
+    name: 'MULTIPLESELECT',
+    props: ['config', 'onChange'],
+    components: { MultPicker },
+    data() {
+      return {
+        value: '',
+        options: [],
+        show: false
+      }
+    },
+    created() {
+      this.value = this.config.value || ''
+      this.options = this.config.options || []
+    },
+    methods: {
+      confirm(obj) {
+        this.onChange({ prop: this.config.formName, value: obj.value })
+        this.value = obj.value
+        this.show = false
+      }
+    }
+  }
+</script>
+
+<style></style>

+ 32 - 0
src/components/low-code/RADIO.vue

@@ -0,0 +1,32 @@
+<template>
+  <u-radio-group v-model="value" @change="change" :disabled="!config.writable">
+    <u-radio v-for="(item, index) in options" :key="index" :name="item.value" :disabled="item.disabled">
+      {{ item.label }}
+    </u-radio>
+  </u-radio-group>
+</template>
+
+<script>
+  export default {
+    name: 'RADIO',
+    props: ['config', 'onChange'],
+    data() {
+      return {
+        value: '',
+        options: [],
+        show: false
+      }
+    },
+    created() {
+      this.value = this.config.value || ''
+      this.options = this.config.options || []
+    },
+    methods: {
+      change(val) {
+        this.onChange({ prop: this.config.formName, value: val })
+      }
+    }
+  }
+</script>
+
+<style></style>

+ 32 - 0
src/components/low-code/SELECT.vue

@@ -0,0 +1,32 @@
+<template>
+  <view>
+    <u-input v-model="value" type="select" :border="true" :disabled="!config.writable" @click="show = true" />
+    <u-select v-model="show" :list="options" @confirm="confirm"></u-select>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'SELECT',
+    props: ['config', 'onChange'],
+    data() {
+      return {
+        value: '',
+        options: [],
+        show: false
+      }
+    },
+    created() {
+      this.value = this.config.value || ''
+      this.options = this.config.options || []
+    },
+    methods: {
+      confirm(arr) {
+        this.onChange({ prop: this.config.formName, value: arr[0].value })
+        this.value = arr[0].value
+      }
+    }
+  }
+</script>
+
+<style></style>

+ 25 - 0
src/components/low-code/TEXT.vue

@@ -0,0 +1,25 @@
+<template>
+  <u-input v-model="value" type="text" :border="true" @input="onInput" placeholder="请输入" :disabled="!config.writable" />
+</template>
+
+<script>
+  export default {
+    name: 'TEXT',
+    props: ['config', 'onChange'],
+    data() {
+      return {
+        value: ''
+      }
+    },
+    created() {
+      this.value = this.config.value || ''
+    },
+    methods: {
+      onInput() {
+        this.onChange({ prop: this.config.formName, value: this.value })
+      }
+    }
+  }
+</script>
+
+<style></style>

+ 52 - 0
src/components/low-code/my-form-item.vue

@@ -0,0 +1,52 @@
+<template>
+  <u-form-item class="my-form-item" label=" " label-width="1rpx" :border-bottom="false" :prop="config.formName">
+    <view>
+      <view class="top-label flex items-center" :class="{ 'm-t-40rpx m-b-30rpx': isBigTitle }">
+        <text>{{ config.title }}</text>
+      </view>
+      <!-- <TEXT :config="config" :onChange="onChange"></TEXT> -->
+      <!-- <SELECT :config="config" :onChange="onChange"></SELECT> -->
+      <!-- <MULTIPLESELECT :config="config" :onChange="onChange"></MULTIPLESELECT> -->
+      <!-- <DATE :config="config" :onChange="onChange"></DATE> -->
+      <RADIO :config="config" :onChange="onChange"></RADIO>
+    </view>
+  </u-form-item>
+</template>
+
+<script>
+  import TEXT from './TEXT.vue'
+  import SELECT from './SELECT.vue'
+  import MULTIPLESELECT from './MULTIPLE_SELECT.vue'
+  import DATE from './DATE.vue'
+  import RADIO from './RADIO.vue'
+  export default {
+    name: 'MyFormItem',
+    props: ['config'],
+    components: { TEXT, SELECT, MULTIPLESELECT, DATE, RADIO },
+    computed: {
+      isBigTitle() {
+        return this.bigTitles.includes(this.config.code)
+      }
+    },
+    data() {
+      return {
+        bigTitles: ['FORM_GROUP_TITLE', 'ONLY_TITLE']
+      }
+    },
+    methods: {
+      onChange(obj) {
+        this.$emit('change', obj)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .my-form-item {
+    .top-label {
+      color: #595959;
+      font-size: 28rpx;
+      line-height: 60rpx;
+    }
+  }
+</style>

+ 30 - 0
src/components/radius-cell.vue

@@ -0,0 +1,30 @@
+<template>
+  <view class="radius-cell flex items-center" @click="$emit('click')">
+    <view class="flex-1 title">{{ title }}</view>
+    <u-icon name="arrow-right" color="#8C8C8C" size="28"></u-icon>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'RadiusCell',
+    props: ['title']
+  }
+</script>
+
+<style lang="scss" scoped>
+  .radius-cell {
+    height: 84rpx;
+    border-radius: 12rpx;
+    background-color: #fff;
+    padding: 0 24rpx;
+    margin-bottom: 24rpx;
+    .title {
+      color: #262626;
+      font-size: 28rpx;
+    }
+    &:active {
+      background-color: #eee;
+    }
+  }
+</style>

+ 7 - 0
src/pages.json

@@ -33,6 +33,13 @@
         "enablePullDownRefresh": false
       }
     },
+    {
+      "path": "pages/sop-step/sop-step",
+      "style": {
+        "navigationBarTitleText": "SOP填报",
+        "enablePullDownRefresh": false
+      }
+    },
     {
       "path": "pages/home/home",
       "style": {

+ 2 - 4
src/pages/index/tab1-content.vue

@@ -9,7 +9,6 @@
       <view class="tag-item" :class="{ active: params.status === true }" @click="toggleStatus(true)">已读</view>
     </view>
     <scroll-view
-      :scroll-top="scrollTop"
       scroll-y="true"
       refresher-enabled="true"
       class="scroll-Y"
@@ -108,8 +107,7 @@
         triggered: false,
         showPopup: false,
         showParamsType: false,
-        showParamsServiceId: false,
-        serviceList: []
+        showParamsServiceId: false
       }
     },
     mounted() {
@@ -212,7 +210,7 @@
         background: #fff;
         margin-bottom: 24rpx;
         .m-foot {
-          padding-top: 14rpx;
+          padding-top: 20rpx;
         }
         .m-body {
           background: #f7f7f7;

+ 1 - 2
src/pages/index/tab2-content.vue

@@ -6,7 +6,6 @@
       <view class="tag-item" :class="{ active: params.flowTaskTypeEnum === 'DRAFT' }" @click="toggleStatus('DRAFT')">暂存</view>
     </view>
     <scroll-view
-      :scroll-top="scrollTop"
       scroll-y="true"
       refresher-enabled="true"
       class="scroll-Y"
@@ -211,7 +210,7 @@
         background: #fff;
         margin-bottom: 24rpx;
         .m-foot {
-          padding-top: 14rpx;
+          padding-top: 20rpx;
         }
         .m-body {
           background: #f7f7f7;

+ 0 - 1
src/pages/index/tab3-content.vue

@@ -9,7 +9,6 @@
       <view class="tag-item" :class="{ active: params.status === true }" @click="toggleStatus(true)">已读</view>
     </view>
     <scroll-view
-      :scroll-top="scrollTop"
       scroll-y="true"
       refresher-enabled="true"
       class="scroll-Y"

+ 222 - 0
src/pages/sop-step/sop-step.vue

@@ -0,0 +1,222 @@
+<template>
+  <view class="sop-step">
+    <u-tabs :list="tabs" :is-scroll="true" :current="current" @change="tabChange"></u-tabs>
+    <scroll-view :scroll-top="scrollTop" scroll-y="true" class="scroll-Y">
+      <view class="main">
+        <RadiusCell title="SOP各协作项目角色说明" @click="showPopup1 = true"></RadiusCell>
+        <RadiusCell title="项目派单信息" @click="showPopup2 = true"></RadiusCell>
+        <view class="form-wrap">
+          <u-form :model="formData" ref="form">
+            <MyFormItem :config="config" @change="itemValueChange" v-for="config in curFormConfig" :key="config.formName"></MyFormItem>
+          </u-form>
+        </view>
+      </view>
+    </scroll-view>
+
+    <u-popup v-model="showPopup1" mode="bottom" :mask-close-able="false" :closeable="true" :border-radius="28">
+      <view class="popup-box1">
+        <view class="title">SOP各协作项目角色说明</view>
+        <view class="content">
+          <view>1.大区经理:研究生项目前置流程、项目必要信息工作交接、项目内审、SOP产出物检查;</view>
+          <view>2.区域协调人:根据与客户沟通确认的项目关键信息表,组织项目成员共同进行项目风险评估,对SOP产出物进行检查跟进;</view>
+          <view>3.实施工程师:完成现场实施工作,执行SOP及产出物提交;</view>
+        </view>
+        <view class="content red m-t-30rpx">
+          <view> 注意: </view>
+          <view>
+            1.若存在暂时无法确定的需求为必填项,大区经理可先按照经验填写,客户确定后,可对关键信息进行补充和修改,具体可参考项目计划变更报备流程。
+          </view>
+          <view> 2.区域协调人可能会由大区经理兼任。 </view>
+        </view>
+      </view>
+    </u-popup>
+
+    <u-popup v-model="showPopup2" mode="bottom" :mask-close-able="false" :closeable="true" :border-radius="28">
+      <view class="popup-box2">
+        <view class="title">项目派单信息</view>
+        <view class="content">
+          <view class="item">项目单号:{{ crmInfo.crmNo }}</view>
+          <view class="item">项目名称:{{ crmInfo.crmName }}</view>
+          <view class="item">派单时间:{{ crmInfo.crmBeginTime }}</view>
+          <view class="item">客户经理:{{ crmInfo.customManagerName }}</view>
+          <view class="item">客户类型:{{ CUSTOMER_TYPE[crmInfo.customType] }}</view>
+          <view class="item">客户名称:{{ crmInfo.customName }}</view>
+          <view class="item">考试时间:{{ crmInfo.examStartTime + ' 至 ' + crmInfo.examEndTime }}</view>
+          <view class="item">实施产品:{{ crmInfo.productName }}</view>
+          <view class="item">服务单元:{{ crmInfo.serviceUnitName }}</view>
+        </view>
+      </view>
+    </u-popup>
+  </view>
+</template>
+
+<script>
+  import RadiusCell from '@/components/radius-cell.vue'
+  import { getSopFlowView } from '@/api/sop'
+  import { CUSTOMER_TYPE } from '@/utils/constants'
+  import testData from './test'
+  import MyFormItem from '@/components/low-code/my-form-item.vue'
+  const needValueCodes = [
+    'NUMBER', //新增
+    'TEXT',
+    'DATE',
+    'SELECT',
+    'CHECKBOX',
+    'TEXTAREA',
+    'TABLE',
+    'RADIO',
+    'RADIO_WITH_INPUT',
+    'SIGN',
+    'DEVICE_OUT_TABLE',
+    'DEVICE_IN_TABLE',
+    'FILE'
+  ]
+  const fullWidthCodes = ['TABLE', 'FORM_GROUP_TITLE', 'TEXTAREA', 'ONLY_TITLE', 'DEVICE_OUT_TABLE', 'DEVICE_IN_TABLE']
+  export default {
+    name: 'SopStep',
+    components: { RadiusCell, MyFormItem },
+    data() {
+      return {
+        CUSTOMER_TYPE,
+        showPopup1: false,
+        showPopup2: false,
+        flowId: '433300909770932224',
+        current: 0,
+        allSteps: [], //流程步骤数组全数据
+        tabs: [],
+        crmInfo: {},
+        formData: {},
+        rules: {}
+      }
+    },
+    computed: {
+      curFormConfig() {
+        const formProperty = this.allSteps[this.current]?.formProperty || []
+        return formProperty
+      }
+    },
+    watch: {
+      curFormConfig(val) {
+        this.formData = val.reduce((obj, item) => {
+          if (needValueCodes.includes(item.code)) {
+            obj[item.formName] = ''
+          }
+          return obj
+        }, {})
+        this.rules = val.reduce((obj, item) => {
+          let ruleItem =
+            item.required && needValueCodes.includes(item.code)
+              ? {
+                  required: true,
+                  message: `${item.title}不能为空`,
+                  trigger: 'change'
+                }
+              : null
+          if (ruleItem) {
+            obj[item.formName] = [ruleItem]
+          }
+          return obj
+        }, {})
+        this.$nextTick(() => {
+          this.$refs.uForm?.setRules(this.rules)
+        })
+      }
+    },
+    methods: {
+      tabChange(index) {
+        this.current = index
+      },
+      init() {
+        getSopFlowView({ flowId: this.flowId }).then((res) => {
+          res = { crmInfo: res.crmInfo, ...testData }
+          this.crmInfo = res.crmInfo || {}
+          res.flowTaskHistoryList = res.flowTaskHistoryList || []
+          res.flowTaskHistoryList.forEach((item) => {
+            item.formProperty.forEach((v) => {
+              v.writable = false
+            })
+          })
+          this.allSteps = [...res.flowTaskHistoryList, res.currFlowTaskResult]
+          this.tabs = [
+            ...res.flowTaskHistoryList.map((item) => {
+              return { name: item.taskName }
+            }),
+            {
+              name: res.currFlowTaskResult.taskName
+            }
+          ]
+          this.current = this.tabs.length - 1
+        })
+      },
+      itemValueChange({ prop, value }) {
+        console.log(prop, ':', value)
+        this.formData[prop] = value
+      }
+    },
+    mounted() {
+      console.log('query', this.$Route.query)
+      this.init()
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .sop-step {
+    height: 100vh;
+    .scroll-Y {
+      height: calc(100% - 88rpx);
+      .main {
+        padding: 24rpx;
+        padding-top: 40rpx;
+        .form-wrap {
+          background-color: #fff;
+          border-radius: 12rpx;
+          padding: 0 24rpx;
+          ::v-deep .u-form-item {
+            padding-top: 0 !important;
+          }
+        }
+      }
+    }
+    .popup-box2 {
+      padding: 24rpx;
+      padding-top: 60rpx;
+      .title {
+        color: #262626;
+        text-align: center;
+        font-size: 28rpx;
+        font-weight: bold;
+        margin-bottom: 30rpx;
+      }
+      .content {
+        background-color: #f7f7f7;
+        border-radius: 12rpx;
+        padding: 24rpx;
+        .item {
+          color: #262626;
+          font-size: 24rpx;
+          line-height: 60rpx;
+        }
+      }
+    }
+    .popup-box1 {
+      padding: 24rpx;
+      padding-top: 60rpx;
+      .title {
+        color: #262626;
+        text-align: center;
+        font-size: 28rpx;
+        font-weight: bold;
+        margin-bottom: 30rpx;
+      }
+      .content {
+        color: #262626;
+        font-size: 24rpx;
+        line-height: 44rpx;
+        &.red {
+          color: #f53f3f;
+        }
+      }
+    }
+  }
+</style>

+ 726 - 0
src/pages/sop-step/test.js

@@ -0,0 +1,726 @@
+/**
+ * 【code取值包含以下】:
+ * FORM_GROUP_TITLE  带虚线分割线的标题
+ * TEXT  文本输入框
+ * DATE 日期选择器
+ * SELECT 下拉框,可能需要提供对应的api获取下拉列表
+ * ONLY_TITLE  普通标题,独占一行
+ * CHECKBOX 复选框组
+ * TEXTAREA  多行文本输入域
+ * TABLE 可编辑表格
+ * RADIO 单选框
+ * RADIO_WITH_INPUT  带1个输入框的单选框,比如选“其他”选项时,需要输入一定的文案
+ * SIGN 手写签名
+ * DEVICE_OUT_TABLE 设备出库的表格组件,内部较为复杂,于是单独封装前端业务组件,不细化成table配置数据
+ * DEVICE_IN_TABLE 设备出库的表格组件,内部较为复杂,于是单独封装前端业务组件,不细化成table配置数据
+ * FILE  上传图片组件,同时需要limit标记所需上传的数量值
+ */
+export default {
+  flowId: '416278876461727744',
+  status: 'AUDITING', //流程状态
+  statusStr: '审核中',
+  sopNo: '416278876860186625', //sop单号
+  taskIdList: '[416279837896867847, 416279837905256450]', //待审批流程任务
+  currFlowTaskResult: {
+    taskName: '设备入库登记',
+    taskKey: 'XXX',
+    setup: 10,
+    formKey: 'XXX',
+    formProperty: [
+      {
+        code: 'RADIO',
+        title: '设备出入库',
+        options: [
+          { value: '1', label: '出库' },
+          { value: '2', label: '入库' }
+        ],
+        disabled: true
+      },
+      {
+        code: 'DATE',
+        title: '设备出入库时间'
+      },
+      {
+        code: 'DEVICE_IN_TABLE',
+        title: '设备入库登记',
+        api: '/api/******' //获取表格整体数据的api
+      }
+    ]
+  },
+  flowTaskHistoryList: [
+    {
+      taskName: '项目初审',
+      taskKey: 'XXX',
+      setup: 1,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '项目基本信息'
+        },
+        {
+          code: 'TEXT',
+          title: '科目数量'
+        },
+        {
+          code: 'TEXT',
+          title: '考生科次'
+        },
+        {
+          code: 'RADIO',
+          title: '条码标准张数',
+          options: [
+            { value: '1', label: '3张' },
+            { value: '2', label: '4张' }
+          ]
+        },
+        {
+          code: 'RADIO_WITH_INPUT',
+          title: '条码粘贴方式',
+          span: 12,
+          options: [
+            { value: '1', label: '考生自贴/数码印刷' },
+            { value: '2', label: '回卷后粘贴' },
+            { value: '3', label: '其他' }
+          ]
+        },
+        {
+          code: 'RADIO',
+          title: '有无客观题卡',
+          options: [
+            { value: '1', label: '有' },
+            { value: '2', label: '无' }
+          ]
+        },
+        {
+          code: 'TEXT',
+          title: '扫描仪数量(台)'
+        },
+        {
+          code: 'DATE',
+          title: '考生数据交接时间'
+        },
+        {
+          code: 'TEXT',
+          title: '现场标准服务周期(人天)'
+        },
+        {
+          code: 'DATE',
+          title: '项目关键信息提交截止时间'
+        },
+        {
+          code: 'DATE',
+          title: '现场服务完成撤场计划时间'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '项目人员安排及风险评估'
+        },
+        {
+          code: 'SELECT',
+          title: '区域协调人',
+          api: '/api/******',
+          span: 4
+        },
+        {
+          code: 'SELECT',
+          title: '实施工程师',
+          api: '/api/******',
+          span: 4
+        },
+        {
+          code: 'SELECT',
+          title: '助理实施工程师',
+          api: '/api/******',
+          span: 4
+        },
+        {
+          code: 'ONLY_TITLE',
+          title: '项目风险评估(仅供参考)'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '延期风险',
+          options: [
+            { value: '1', label: '低' },
+            { value: '2', label: '中' },
+            { value: '3', label: '高' }
+          ]
+        },
+        {
+          code: 'CHECKBOX',
+          title: '实施难度',
+          options: [
+            { value: '1', label: '低' },
+            { value: '2', label: '中' },
+            { value: '3', label: '高' }
+          ]
+        },
+        {
+          code: 'TEXTAREA',
+          title: '其它备注(建议关注的其它方面)'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '项目联系人'
+        },
+        {
+          code: 'TABLE',
+          title: '',
+          tablePropList: [
+            {
+              id: 'XXX',
+              widgetId: 'XXX',
+              tdIndex: 1,
+              tdId: 'XXX',
+              tdName: 'XXX',
+              title: '学院/分(子)机构',
+              tdOrder: true,
+              tdSearch: true,
+              editWidgetId: '19',
+              tdFormWidget: {
+                code: 'TEXT',
+                name: '文本'
+              }
+            },
+            {
+              id: 'XXX',
+              widgetId: 'XXX',
+              tdIndex: 2,
+              tdId: 'XXX',
+              tdName: 'XXX',
+              title: '姓名',
+              tdOrder: true,
+              tdSearch: true,
+              editWidgetId: 'XXX',
+              tdFormWidget: {
+                code: 'TEXT',
+                name: '文本'
+              }
+            },
+            {
+              id: 'XXX',
+              widgetId: 'XXX',
+              tdIndex: 3,
+              tdId: 'XXX',
+              tdName: 'XXX',
+              title: '职务',
+              tdOrder: true,
+              tdSearch: true,
+              editWidgetId: 'XXX',
+              tdFormWidget: {
+                code: 'TEXT',
+                name: '文本'
+              }
+            },
+            {
+              id: 'XXX',
+              widgetId: 'XXX',
+              tdIndex: 4,
+              tdId: 'XXX',
+              tdName: 'XXX',
+              title: '手机',
+              tdOrder: true,
+              tdSearch: true,
+              editWidgetId: 'XXX',
+              tdFormWidget: {
+                code: 'TEXT',
+                name: '文本'
+              }
+            }
+          ]
+        }
+      ]
+    },
+    {
+      taskName: '项目关键信息',
+      taskKey: 'XXX',
+      setup: 2,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '云阅卷(填写前请先认真查阅移交的项目初审及项目基本信息)'
+        },
+        {
+          code: 'RADIO',
+          title: '运行环境调研是否符合要求',
+          options: [
+            { value: '1', label: '符合' },
+            { value: '2', label: '不符合' }
+          ]
+        },
+        {
+          code: 'DATE',
+          title: '环境部署时间'
+        },
+        {
+          code: 'DATE',
+          title: '扫描开始时间'
+        },
+        {
+          code: 'DATE',
+          title: '扫描结束时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷机房检查时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷机房数量(个)'
+        },
+        {
+          code: 'TEXT',
+          title: '最大评卷教师数量'
+        },
+        {
+          code: 'DATE',
+          title: '评卷参数提供时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷开始时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷结束时间'
+        },
+        {
+          code: 'TEXT',
+          title: '有选做题的科目数量(门)'
+        },
+        {
+          code: 'TEXTAREA',
+          title: '其他特殊要求'
+        },
+        {
+          code: 'FILE',
+          title: '上传项目关键信息表(纸质)拍照',
+          limit: 3
+        }
+      ]
+    },
+    {
+      taskName: '项目内审',
+      taskKey: 'XXX',
+      setup: 3,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '项目基本信息'
+        },
+        {
+          code: 'TEXT',
+          title: '科目数量'
+        },
+        {
+          code: 'TEXT',
+          title: '考生科次'
+        },
+        {
+          code: 'RADIO',
+          title: '条码标准张数',
+          options: [
+            { value: '1', label: '3张' },
+            { value: '2', label: '4张' }
+          ]
+        },
+        {
+          code: 'RADIO_WITH_INPUT',
+          title: '条码粘贴方式',
+          span: 12,
+          options: [
+            { value: '1', label: '考生自贴/数码印刷' },
+            { value: '2', label: '回卷后粘贴' },
+            { value: '3', label: '其他' }
+          ]
+        },
+        {
+          code: 'RADIO',
+          title: '有无客观题卡',
+          options: [
+            { value: '1', label: '有' },
+            { value: '2', label: '无' }
+          ]
+        },
+        {
+          code: 'TEXT',
+          title: '扫描仪数量(台)'
+        },
+        {
+          code: 'DATE',
+          title: '考生数据交接时间'
+        },
+        {
+          code: 'TEXT',
+          title: '现场标准服务周期(人天)'
+        },
+        {
+          code: 'DATE',
+          title: '项目关键信息提交截止时间'
+        },
+        {
+          code: 'DATE',
+          title: '现场服务完成撤场计划时间'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '云阅卷'
+        },
+        {
+          code: 'RADIO',
+          title: '运行环境调研是否符合要求',
+          options: [
+            { value: '1', label: '符合' },
+            { value: '2', label: '不符合' }
+          ]
+        },
+        {
+          code: 'DATE',
+          title: '环境部署时间'
+        },
+        {
+          code: 'DATE',
+          title: '扫描开始时间'
+        },
+        {
+          code: 'DATE',
+          title: '扫描结束时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷机房检查时间'
+        },
+        {
+          code: 'TEXT',
+          title: '评卷机房数量'
+        },
+        {
+          code: 'TEXT',
+          title: '最大评卷教师数量'
+        },
+        {
+          code: 'DATE',
+          title: '评卷参数提供时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷开始时间'
+        },
+        {
+          code: 'DATE',
+          title: '评卷结束时间'
+        },
+        {
+          code: 'TEXT',
+          title: '有选做题的科目数量'
+        },
+        {
+          code: 'TEXTAREA',
+          title: '其它特殊要求'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '项目内审(请于24小时内完成内审)'
+        },
+        {
+          code: 'RADIO',
+          title: '我对以上项目关键信息已审核,确认内容无误',
+          options: [
+            { value: '1', label: '同意' },
+            { value: '2', label: '不同意' }
+          ]
+        },
+        {
+          code: 'SIGN',
+          title: '手写签名'
+        },
+        {
+          code: 'TEXTAREA',
+          title: '审批意见'
+        }
+      ]
+    },
+    {
+      taskName: '设备出库登记',
+      taskKey: 'XXX',
+      setup: 4,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'RADIO',
+          title: '设备出入库',
+          options: [
+            { value: '1', label: '出库' },
+            { value: '2', label: '入库' }
+          ],
+          disabled: true
+        },
+        {
+          code: 'DATE',
+          title: '设备出入库时间',
+          api: '/api/******' //获取设备列表的api
+        },
+        {
+          code: 'DEVICE_OUT_TABLE',
+          title: '设备出库登记'
+        }
+      ]
+    },
+    {
+      taskName: '扫描准备',
+      taskKey: 'XXX',
+      setup: 5,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '导入数据'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          options: [{ value: '1', label: '导入数据与原始数据是否一致' }]
+        },
+        {
+          code: 'TEXT',
+          title: '导入考生数量'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '扫描仪设置'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '扫描仪设置'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          span: 12,
+          options: [
+            {
+              value: '1',
+              label: '1、扫描仪分辨率设置是否150、扫描模式是否双面、图像类型是否灰度'
+            },
+            { value: '2', label: '2、选择“扫描模式”-“设置”-“装订边缘”-“上”' }
+          ]
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '卡格式制作'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          span: 12,
+          options: [
+            {
+              value: '1',
+              label: '1、定位点、校验点是否制作'
+            },
+            { value: '2', label: '2、缺考区域是否框选' },
+            {
+              value: '3',
+              label: '3、客观题区域是否正确框选、行列以及单选多选是否正确'
+            },
+            { value: '4', label: '4、条码识别区是否制作' },
+            { value: '5', label: '5、屏蔽区是否制作' },
+            { value: '6', label: '6、卷型:若有,则框选' },
+            { value: '7', label: '7、页码:若答题卡超过1张,则框选' }
+          ]
+        }
+      ]
+    },
+    {
+      taskName: '正式扫描',
+      taskKey: 'XXX',
+      setup: 6,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '试扫检查'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          span: 12,
+          options: [
+            {
+              value: '1',
+              label: '1、查看条码是否识别正确'
+            },
+            { value: '2', label: '2、缺考、客观题是否识别正确' },
+            { value: '3', label: '3、扫描原图正反面是否正确显示' },
+            { value: '4', label: '4、裁切图屏蔽区是否正确' },
+            { value: '5', label: '5、卷型:若有,是否识别正确' },
+            { value: '6', label: '6、页码:若答题卡超过1张,是否页码识别正确' }
+          ]
+        }
+      ]
+    },
+    {
+      taskName: '校验收尾',
+      taskKey: 'XXX',
+      setup: 7,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '未上传考生核对'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          options: [
+            {
+              value: '1',
+              label: '1、未上传考生是否与签到表进行核对,是否为缺考、违纪、免考等情况'
+            },
+            { value: '2', label: '2、是否将未上传考生发给学校确认' }
+          ]
+        },
+        {
+          code: 'TEXT',
+          title: '完成数量'
+        },
+        {
+          code: 'FILE',
+          title: '上传学校未上传考生沟通确认截图',
+          limit: 1
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '识别对照'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          options: [{ value: '1', label: '是否将客观题识别异常的全部人工修改处理' }]
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '缺考名单对比'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          options: [
+            {
+              value: '1',
+              label: '核对未上传条数是否与学校提供的缺考名单吻合,人工指定为缺考(考生管理>导入名单>导入缺考名单)'
+            }
+          ]
+        },
+        {
+          code: 'TEXT',
+          title: '缺考科次数量'
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '手工输入条码确认'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          options: [
+            {
+              value: '1',
+              label: '“数据检查" -"人工确认“,确认完成'
+            }
+          ]
+        }
+      ]
+    },
+    {
+      taskName: '评卷准备',
+      taskKey: 'XXX',
+      setup: 8,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '评卷模式确认'
+        },
+        {
+          code: 'RADIO',
+          label: '评卷模式',
+          options: [
+            { value: '1', label: '轨迹模式' },
+            { value: '2', label: '普通模式' }
+          ]
+        },
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '评卷参数核对'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          span: 12,
+          options: [
+            {
+              value: '1',
+              label: '1、核查每个科目的满分是否正确(大部分科目都是100分)'
+            },
+            {
+              value: '2',
+              label: '2、核对客观题标答以及分数是否录入正确、多选题判分规则(漏选是否得分、任选得分)'
+            },
+            {
+              value: '3',
+              label: '3、检查所有科目结构和分组是否全部导入。(评卷管理>评卷进度查看分组状态)'
+            }
+          ]
+        }
+      ]
+    },
+    {
+      taskName: '评卷收尾',
+      taskKey: 'XXX',
+      setup: 9,
+      formKey: 'XXX',
+      formProperty: [
+        {
+          code: 'FORM_GROUP_TITLE',
+          title: '成绩提交核查'
+        },
+        {
+          code: 'CHECKBOX',
+          title: '',
+          span: 12,
+          options: [
+            {
+              value: '1',
+              label: '1、是否已核对不缺考、客观题为0分的情况。'
+            },
+            {
+              value: '2',
+              label: '2、是否已核对主观题为0、客观题有分的情况。'
+            },
+            {
+              value: '3',
+              label: '3、是否检查客观题小题得分率低于20%的情况。'
+            },
+            {
+              value: '4',
+              label: '4、核对导出成绩表数据条数是否与考生表数量一致,不能带有“未导出全量数据”字样;请填写导出成绩数量以及完成时间'
+            },
+            {
+              value: '5',
+              label: '5、核对导出图片工具是否正常运行'
+            }
+          ]
+        },
+        {
+          code: 'FILE',
+          title: '上传验收报告(纸质)拍照',
+          limit: 5
+        }
+      ]
+    }
+  ]
+}

+ 214 - 0
src/pages/sop/office-sop-list.vue

@@ -0,0 +1,214 @@
+<template>
+  <view class="office-sop-list flex flex-col">
+    <scroll-view
+      :scroll-top="scrollTop"
+      scroll-y="true"
+      refresher-enabled="true"
+      class="scroll-Y"
+      :refresher-triggered="triggered"
+      refresher-background="transparent"
+      @refresherrefresh="onRefresh"
+      @scrolltolower="scrolltolower"
+    >
+      <view class="msg-item" v-for="(item, index) in list" :key="index">
+        <view class="m-head flex items-center justify-between">
+          <view class="m-title">
+            <text class="title">{{ item.sopNo }}</text>
+          </view>
+          <view class="m-time">{{ dateFormat(item.beginTime, 'yyyy-MM-dd hh:mm') }}</view>
+        </view>
+        <view class="m-body rd-12rpx p-24rpx flex flex-wrap">
+          <view class="key-value"> 服务单元:{{ item.serviceName }} </view>
+          <view class="key-value"> 客户经理:{{ item.customManagerName }} </view>
+          <view class="key-value"> 客户类型:{{ item.customManagerTypeStr }} </view>
+          <view class="key-value"> 客户名称:{{ item.customName }} </view>
+          <view class="key-value"> 项目名称:{{ item.crmName }} </view>
+          <view class="key-value"> 实施产品:{{ item.productName }} </view>
+          <view class="key-value"> 考试时间:{{ item.examStartTime + '至' + item.examEndTime }} </view>
+        </view>
+        <view class="m-foot text-right">
+          <u-button type="primary" size="mini" @click="toCurSopFlow(item)">填报</u-button>
+        </view>
+      </view>
+      <view class="bottom">
+        <u-loading v-if="loadingFlag == 1" mode="flower" size="44"></u-loading>
+        <view v-if="loadingFlag == 2" class="text flex items-center"> <view class="line"></view>我是有底线的<view class="line"></view></view>
+      </view>
+    </scroll-view>
+
+    <u-toast ref="uToast" />
+  </view>
+</template>
+
+<script>
+  import { getSopList } from '@/api/sop'
+  import { dictToOptionList } from '@/utils/utils'
+  import { dateFormat } from '@/utils/utils'
+  export default {
+    name: 'OfficeSopList',
+    data() {
+      return {
+        dateFormat,
+        dictToOptionList,
+        params: { serviceId: '', type: 'OFFICE_SOP_FLOW' },
+        pageNumber: 1,
+        pageSize: 10,
+        loadingFlag: 0,
+        list: [],
+        triggered: false
+      }
+    },
+    mounted() {
+      this.search()
+    },
+    methods: {
+      toCurSopFlow(item) {
+        console.log('iii', item)
+        this.$Router.push({ path: '/pages/sop-step/sop-step' })
+      },
+      scrolltolower() {
+        if (this.loadingFlag > 0) {
+          return
+        }
+
+        this.loadingFlag = 1
+        this.pageNumber++
+        this.getList()
+      },
+      search(bool) {
+        this.pageNumber = 1
+        this.list = []
+        this.getList(bool)
+      },
+      getList(bool) {
+        getSopList({ ...this.params, pageNumber: this.pageNumber, pageSize: this.pageSize }).then((res) => {
+          this.loadingFlag = res.pages == this.pageNumber ? 2 : 0
+          console.log('this.loadingFlag:', this.loadingFlag)
+          this.list.push(...(res.records || []))
+          if (bool) {
+            this.triggered = false
+            this.$refs.uToast.show({
+              title: '刷新成功',
+              type: 'success'
+            })
+          }
+        })
+      },
+      onRefresh() {
+        if (this.triggered) return
+        this.triggered = true
+        this.search(true)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .office-sop-list {
+    height: calc(100% - 80rpx);
+    padding: 24rpx;
+    .search-box {
+      padding: 30rpx 20rpx;
+    }
+    .tag-box {
+      height: 84rpx;
+      display: flex;
+      .tag-item {
+        height: 60rpx;
+        width: 96rpx;
+        font-size: 24rpx;
+        text-align: center;
+        line-height: 60rpx;
+        border-radius: 30rpx;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-right: 24rpx;
+        background: #fff;
+        position: relative;
+        color: #bfbfbf;
+        &.active {
+          background: #165dff;
+          color: #fff;
+        }
+        .red {
+          position: absolute;
+          width: 12rpx;
+          height: 12rpx;
+          right: 10rpx;
+          top: 10rpx;
+          background: red;
+          border-radius: 6px;
+        }
+      }
+    }
+    .scroll-Y {
+      height: calc(100% - 184rpx);
+      .msg-item {
+        padding: 24rpx;
+        border-radius: 12rpx;
+        background: #fff;
+        margin-bottom: 24rpx;
+        .m-foot {
+          padding-top: 20rpx;
+        }
+        .m-body {
+          background: #f7f7f7;
+          margin-top: 16rpx;
+          .key-value {
+            color: #8c8c8c;
+            font-size: 24rpx;
+            width: 100%;
+            &:not(:last-child) {
+              margin-bottom: 16rpx;
+            }
+          }
+        }
+        .m-head {
+          .m-time {
+            color: #8c8c8c;
+            font-size: 24rpx;
+          }
+          .m-title {
+            display: flex;
+            align-items: center;
+            .title {
+              color: #262626;
+              font-size: 32rpx;
+              margin-right: 10rpx;
+            }
+            // .tag {
+            //   background: #ffece8;
+            //   border-radius: 6rpx;
+            //   text-align: center;
+            //   line-height: 18px;
+            //   height: 18px;
+            //   color: #f53f3f;
+            //   font-size: 20rpx;
+            //   padding: 0 8rpx;
+            //   margin-left: 10rpx;
+            // }
+          }
+        }
+      }
+      .bottom {
+        padding: 20rpx 0 30rpx 0;
+        text-align: center;
+        .text {
+          color: #aaa;
+          font-size: 24rpx;
+          .line {
+            border-bottom: 1px dashed #ccc;
+            flex: 1;
+            &:first-child {
+              margin-right: 20rpx;
+            }
+            &:last-child {
+              margin-left: 20rpx;
+            }
+          }
+        }
+      }
+    }
+  }
+</style>

+ 18 - 7
src/pages/sop/sop.vue

@@ -1,18 +1,29 @@
 <template>
-  <view> SOP </view>
+  <view class="sop">
+    <u-tabs :list="tabList" :is-scroll="true" :current="current" @change="tabChange"></u-tabs>
+    <officeSopList v-if="current == 0"></officeSopList>
+  </view>
 </template>
 
 <script>
+  import OfficeSopList from './office-sop-list.vue'
   export default {
     name: 'Sop',
-
+    components: { OfficeSopList },
     data() {
-      return {}
+      return {
+        tabList: [{ name: '教务处SOP' }, { name: '研究生SOP' }, { name: '项目计划变更' }, { name: '延期预警' }, { name: '违规登记' }],
+        current: 0
+      }
     },
-    created() {},
-    onShow() {},
-    methods: {}
+    methods: {
+      tabChange() {}
+    }
   }
 </script>
 
-<style></style>
+<style lang="scss" scoped>
+  .sop {
+    height: 100vh;
+  }
+</style>

+ 12 - 10
src/router/permission.js

@@ -2,10 +2,12 @@ import store from '@/store'
 
 export default (router) => {
   // 路由白名单,可以考虑不同环境配置不同白名单
-  const whiteList = ['/pages/index/index', '/pages/home/home', '/pages/login/login'] // no redirect whitelist
+  const whiteList = ['/pages/index/index', '/pages/sop/sop', '/pages/home/home', '/pages/login/login'] // no redirect whitelist
 
   // 全局路由前置守卫
   router.beforeEach(async (to, from, next) => {
+    next() //小程序登录接口还没有出来,先放开路由拦截器
+    return
     const token = store.state.vuex_access_token
     const userId = store.state.vuex_user_info
 
@@ -35,15 +37,15 @@ export default (router) => {
       // 在免登录白名单,直接进入
       next()
     } else {
-      next({
-        path: '/pages/account/login/index',
-        query: {
-          redirectView: JSON.stringify({
-            path: to.path,
-            query: to.query
-          })
-        }
-      })
+      // next({
+      //   path: '/pages/account/login/index',
+      //   query: {
+      //     redirectView: JSON.stringify({
+      //       path: to.path,
+      //       query: to.query
+      //     })
+      //   }
+      // })
     }
   })