zhangjie 1 éve
szülő
commit
86f112719e

+ 1 - 1
src/api/common.js

@@ -1,3 +1,3 @@
 import { http } from '@/service/request.js'
 export const getServiceUnit = () => http.post('/api/admin/common/query_service_unit')
-export const getAttachmentList = (ids) => http.post('/api/admin/common/file/preview', { ids })
+export const getAttachmentList = (ids) => http.post('/api/admin/common/file/preview', ids)

+ 13 - 0
src/api/sop.js

@@ -27,3 +27,16 @@ export const delayWarnDetail = (id) => http.get('/api/admin/tb/delay/warn/get?id
 export const delayWarnDetailList = (id) => http.post('/api/admin/tb/delay/warn/detail/list?id=' + id)
 // 延期预警跟进提交
 export const flowDelayWarn = (data) => http.post('/api/admin/tb/delay/warn/detail/save', data)
+
+// 违规登记 ------------------->
+// 违规登记列表
+export const getViolationList = (data) => http.post('/api/admin/tb/violation/query', {}, { params: data })
+export const saveViolation = (data) => http.post('/api/admin/tb/violation/save', data)
+// 关闭单个违规登记
+export const closeViolation = (id) => http.post('/api/admin/tb/violation/close?id=' + id)
+// 重启违规登记
+export const restartViolation = (id) => http.post('/api/admin/tb/violation/restart?id=' + id)
+// 违规登记明细表
+export const violationDetailList = (id) => http.post('/api/admin/tb/violation/detail/query?id=' + id)
+// 违规登记跟进提交
+export const flowViolation = (data) => http.post('/api/admin/tb/violation/detail/save', data)

+ 37 - 18
src/components/attachment-view.vue

@@ -1,31 +1,29 @@
 <template>
   <view class="attachment-view">
     <view v-if="attachments.images.length" class="attachment-image-list">
-      <u-image
-        v-for="(img, index) in attachments.images"
-        :key="index"
-        :src="img"
-        fit="scale-down"
-        :width="imgSize"
-        :height="imgSize"
-        @click="openView(index)"
-      >
-      </u-image>
+      <div v-for="(img, index) in attachments.images" :key="index" class="attachment-image-item">
+        <u-image :src="img" fit="scale-down" :width="imgSize" :height="imgSize" @click="openView(index)"> </u-image>
+      </div>
     </view>
     <view v-if="attachments.files.length" class="attachment-link-list">
       <t-link v-for="file in attachments.files" :key="file" theme="primary">
         {{ file }}
       </t-link>
     </view>
+
+    <!-- ImagePreview -->
+    <image-preview ref="ImagePreview" :image="curImage"></image-preview>
   </view>
 </template>
 
 <script>
   import { objTypeOf } from '@/utils/utils'
   import { getAttachmentList } from '@/api/common'
+  import ImagePreview from './image-preview.vue'
 
   export default {
     name: 'AttachmentView',
+    components: { ImagePreview },
     props: {
       ids: {
         type: [String, Array],
@@ -44,19 +42,25 @@
     },
     data() {
       return {
-        imgVisible: false,
-        imgIndex: 0,
         attachments: {
           images: [],
           files: []
-        }
+        },
+        curImage: ''
       }
     },
     computed: {
       attachmentIds() {
-        if (objTypeOf(this.ids) === 'string') return this.ids.split(',')
-        if (objTypeOf(this.ids) === 'array') return this.ids
-        return []
+        let data = []
+        if (objTypeOf(this.ids) === 'string') {
+          data = this.ids.split(',')
+        } else if (objTypeOf(this.ids) === 'array') {
+          data = this.ids
+        }
+
+        data = data.filter((item) => !!item)
+
+        return data
       }
     },
     watch: {
@@ -69,8 +73,10 @@
     },
     methods: {
       openView(index) {
-        this.imgIndex = index
-        this.imgVisible = true
+        this.curImage = this.attachments.images[index]
+        this.$refs.ImagePreview.open()
+        // const images = this.attachments.images
+        // this.curImages = [...images.slice(index), ...images.slice(0, index)]
       },
       async getAttachments() {
         this.attachments = {
@@ -105,3 +111,16 @@
     }
   }
 </script>
+
+<style lang="scss" scoped>
+  .attachment-image {
+    &-list {
+      font-size: 0;
+    }
+    &-item {
+      display: inline-block;
+      vertical-align: top;
+      margin: 10rpx;
+    }
+  }
+</style>

+ 39 - 16
src/components/dynamic-table.vue

@@ -1,16 +1,16 @@
 <template>
   <div class="dynamic-table">
     <view class="dynamic-header">
-      <h2></h2>
-      <u-button v-if="!readonly" @click="handleAdd">新增</u-button>
+      <h2>{{ header }}</h2>
+      <u-button v-if="!readonly" type="primary" size="mini" plain @click="handleAdd">新增</u-button>
     </view>
     <view class="dynamic-body">
-      <view v-for="(item, index) in tableData" :key="index" class="key-body">
-        <view class="key-title"></view>
-        <view class="key-info">
+      <view v-for="(item, index) in tableData" :key="index" class="dynamic-body-item">
+        <view class="dynamic-body-title">{{ fillZero(index + 1, 3) }}</view>
+        <view class="dynamic-body-info">
           <view v-for="col in columns" :key="col.colKey" class="key-value"> {{ col.title }}:{{ item[col.colKey] }} </view>
         </view>
-        <view v-if="!readonly" class="key-foolter">
+        <view v-if="!readonly" class="dynamic-body-foolter">
           <u-button theme="primary" hover="color" @click="handleEdit(row)"> 修改 </u-button>
           <u-button theme="primary" hover="color" @click="handleDelete(rowIndex)"> 删除 </u-button>
         </view>
@@ -18,22 +18,35 @@
     </view>
 
     <u-popup v-model="showEditColumnDialog" mode="bottom" :mask-close-able="false" :closeable="true" :border-radius="28">
-      <view class="popup-box">
-        <view class="title">{{ title }}</view>
-        <view class="content">
-          <u-form ref="formRef" :data="formData" :rules="rules" :labelWidth="140" colon>
-            <u-form-item v-for="item in columns" :key="item.colKey" :label="item.title" :prop="rules[item.colKey] ? item.colKey : undefined">
-              <u-input v-model="curRow[item.colKey]" />
+      <view class="my-popup">
+        <view class="popup-title">{{ title }}</view>
+        <view class="popup-content">
+          <u-form ref="formRef" :model="curRow" :border-bottom="false" label-position="top">
+            <u-form-item
+              v-for="item in columns"
+              :key="item.colKey"
+              :label="item.title"
+              :prop="rules[item.colKey] ? item.colKey : undefined"
+              :border-bottom="false"
+              :required="!!rules[item.colKey]"
+            >
+              <u-input v-model="curRow[item.colKey]" border placeholder="请输入" :maxlength="200" />
             </u-form-item>
           </u-form>
         </view>
+        <div class="popup-footer">
+          <u-row gutter="16">
+            <u-col :span="6"> <u-button type="default" @click="cancelHandle">取消</u-button> </u-col>
+            <u-col :span="6"><u-button type="primary" @click="submitHandle">保存</u-button></u-col>
+          </u-row>
+        </div>
       </view>
     </u-popup>
   </div>
 </template>
 
 <script>
-  import { randomCode } from '@/utils/utils'
+  import { randomCode, fillZero } from '@/utils/utils'
 
   export default {
     name: 'DynamicTable',
@@ -45,7 +58,8 @@
         type: Array
       },
       readonly: Boolean,
-      modelValue: { type: Array }
+      modelValue: { type: Array },
+      header: { type: String, default: '' }
     },
     data() {
       return {
@@ -79,7 +93,11 @@
         return rules
       }
     },
+    onReady() {
+      this.$refs.formRef.setRules(this.rules)
+    },
     methods: {
+      fillZero,
       getRowData() {
         return this.columns.reduce((row, item) => {
           row[item.colKey] = ''
@@ -112,10 +130,15 @@
         }
         this.emitChange()
       },
-      async save() {
-        const valid = await this.$ref.formRef.validate()
+      async submitHandle() {
+        const valid = await this.$refs.formRef.validate()
         if (!valid) return
         this.columnConfirm(this.curRow)
+        this.cancelHandle()
+      },
+      cancelHandle() {
+        this.curRow = {}
+        this.showEditColumnDialog = false
       }
     }
   }

+ 67 - 0
src/components/image-preview.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="image-preview">
+    <u-mask :show="show">
+      <view class="image-preview-container">
+        <div class="image-preview-body">
+          <u-image class="u-img" :src="image" height="100%" />
+        </div>
+        <div class="image-preview-close" @click="close">关闭</div>
+      </view>
+    </u-mask>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'ImagePreview',
+    props: {
+      image: {
+        type: String
+      }
+    },
+    data() {
+      return {
+        show: ''
+      }
+    },
+    methods: {
+      open() {
+        this.show = true
+      },
+      close() {
+        this.show = false
+      }
+    }
+  }
+</script>
+
+<style lang="scss">
+  .image-preview {
+    &-container {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+    }
+    &-close {
+      position: absolute;
+      color: #dd524d;
+      border: 2rpx solid #dd524d;
+      background: #d7acaa;
+      padding: 6rpx 12rpx;
+      top: 30rpx;
+      right: 30rpx;
+      border-radius: 10rpx;
+      z-index: 99;
+    }
+    &-body {
+      height: 100%;
+      .u-img {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 5 - 1
src/components/my-select.vue

@@ -1,6 +1,6 @@
 <template>
   <view>
-    <u-input :value="serviceLabel" :disabled="disabled" type="select" :border="true" placeholder="请选择" @click="show = !show" />
+    <u-input :value="serviceLabel" :disabled="disabled" type="select" :border="true" placeholder="请选择" @click="showSelect" />
     <u-select v-model="show" :disabled="disabled" :list="list" mode="single-column" @confirm="confirm"></u-select>
   </view>
 </template>
@@ -34,6 +34,10 @@
     methods: {
       confirm(arr) {
         this.$emit('update:value', arr[0].value)
+      },
+      showSelect() {
+        if (this.disabled) return
+        this.show = !this.show
       }
     },
     mounted() {}

+ 15 - 1
src/pages.json

@@ -56,6 +56,20 @@
         "enablePullDownRefresh": false
       }
     },
+    {
+      "path": "pages/sop/violation-registration/violation-detail",
+      "style": {
+        "navigationBarTitleText": "违规登记明细",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/sop/violation-registration/violation-history",
+      "style": {
+        "navigationBarTitleText": "违规登记历史明细",
+        "enablePullDownRefresh": false
+      }
+    },
     {
       "path": "pages/ding/ding",
       "style": {
@@ -125,4 +139,4 @@
       }
     ]
   }
-}
+}

+ 55 - 105
src/pages/sop/delay-warning/delay-detail.vue

@@ -1,47 +1,54 @@
 <template>
-  <view class="delay-detail sop-step">
-    <scroll-view :scroll-top="scrollTop" scroll-y="true" class="scroll-Y">
-      <view class="main">
-        <RadiusCell title="SOP信息" @click="showPopup1 = true"></RadiusCell>
-        <view class="form-wrap">
-          <u-form :model="formData" ref="formRef" :border-bottom="false" label-position="top">
-            <template v-if="!IS_NEW_MODE">
-              <u-form-item label="报备申请人"> {{ sopInfo.createRealName }}</u-form-item>
-              <u-form-item label="报备申请时间">
-                {{ sopInfo.createTime | timeFormat }}
-              </u-form-item>
-            </template>
+  <view class="my-page">
+    <view class="my-page-main">
+      <RadiusCell title="SOP信息" @click="showPopup1 = true"></RadiusCell>
+      <view class="my-card">
+        <u-form :model="formData" ref="formRef" :border-bottom="false" label-position="top">
+          <view class="title-info"> 变更处理结果</view>
+          <template v-if="!IS_NEW_MODE">
+            <view class="title-info"> 报备申请人:{{ sopInfo.createName }}</view>
+            <view class="title-info"> 报备申请时间:{{ dateFormat(sopInfo.createTime) }} </view>
+          </template>
 
-            <u-form-item label="跟进说明" prop="remark">
-              <u-input v-model="formData.remark" type="textarea" placeholder="请输入说明" :maxlength="100"></u-input>
-            </u-form-item>
+          <u-form-item label="跟进说明" prop="remark" :border-bottom="false" required>
+            <u-input v-model="formData.remark" type="textarea" placeholder="请输入说明" :maxlength="100" border></u-input>
+          </u-form-item>
 
-            <u-form-item label="附件说明">
-              <upload-image :config="{ length: 3 }" :onChange="fileChange"></upload-image>
-            </u-form-item>
-          </u-form>
-        </view>
-        <RadiusCell title="历史明细查询" @click="toHistory"></RadiusCell>
-      </view>
-      <view class="footer">
-        <u-button theme="primary" @click="submitHandle">提交</u-button>
-        <u-button theme="default" @click="cancelHandle">取消</u-button>
+          <u-form-item label="附件说明" :border-bottom="false">
+            <upload-image :config="{ length: 3 }" @get-lists="fileChange"> </upload-image>
+          </u-form-item>
+        </u-form>
       </view>
-    </scroll-view>
+      <RadiusCell title="历史明细查询" @click="toHistory"></RadiusCell>
+    </view>
+    <view class="my-page-footer">
+      <u-row gutter="16">
+        <u-col :span="6"> <u-button type="default" @click="cancelHandle">取消</u-button> </u-col>
+        <u-col :span="6"><u-button type="primary" @click="submitHandle">保存</u-button></u-col>
+      </u-row>
+    </view>
 
-    <u-popup v-model="showPopup1" mode="bottom" :mask-close-able="false" :closeable="true" :border-radius="28">
-      <view class="popup-box2">
-        <view class="title">SOP信息</view>
-        <view class="content">
+    <u-popup
+      v-model="showPopup1"
+      mode="bottom"
+      :mask-close-able="false"
+      :closeable="true"
+      close-icon-color="#000000"
+      close-icon-size="22"
+      border-radius="24"
+    >
+      <view class="my-popup">
+        <view class="popup-title">SOP信息</view>
+        <view class="popup-content pad">
           <view class="key-value"> 服务单元:{{ sopInfo.service }} </view>
           <view class="key-value"> SOP流水号:{{ sopInfo.sopNo }} </view>
-          <view class="key-value"> 客户类型:{{ sopInfo.customType }} </view>
+          <view class="key-value"> 客户类型:{{ CUSTOMER_TYPE[sopInfo.customType] }} </view>
           <view class="key-value"> 客户名称:{{ sopInfo.custom }} </view>
-          <view class="key-value"> 预警时间:{{ sopInfo.warnTime | timeFormat }} </view>
+          <view class="key-value"> 预警时间:{{ dateFormat(sopInfo.warnTime) }} </view>
           <view class="key-value"> 节点负责人:{{ sopInfo.userName }} </view>
           <!-- <view class="key-value"> 项目单号:{{ sopInfo.crmNo }} </view>
           <view class="key-value"> 项目名称:{{ sopInfo.crmName }} </view> -->
-          <view class="key-value"> 预警类型:{{ WARN_FLOW_STATUS[sopInfo.type] }} </view>
+          <view class="key-value"> 预警类型:{{ WARN_TYPE[sopInfo.type] }} </view>
           <view class="key-value"> 预警字段:{{ sopInfo.fieldObj }} </view>
         </view>
       </view>
@@ -52,52 +59,55 @@
 </template>
 
 <script>
-  // import { CUSTOMER_TYPE } from '@/utils/constants'
   import RadiusCell from '@/components/radius-cell.vue'
   import { flowDelayWarn } from '@/api/sop'
   import UploadImage from '@/components/low-code/UPLOAD_IMAGE.vue'
+  import { WARN_TYPE, CUSTOMER_TYPE } from '@/utils/constants'
+  import { dateFormat } from '@/utils/utils'
 
   export default {
     name: 'PlanChangeDetail',
     components: { RadiusCell, UploadImage },
-    props: {
-      curRow: {
-        type: Object
-      }
-    },
     data() {
       return {
+        WARN_TYPE,
+        CUSTOMER_TYPE,
         sopInfo: {},
+        showPopup1: false,
         formData: {
           attachmentIds: '',
           remark: ''
         },
-        tableData: [],
         rules: {
           remark: [
             {
               required: true,
               message: '请填写跟进说明',
-              type: 'error',
-              trigger: 'blur'
+              trigger: 'change'
             }
           ]
         }
       }
     },
     computed: {
+      IS_NEW_MODE() {
+        return this.$Route.query.type === 'new'
+      },
       title() {
         return `${this.sopInfo.code}`
       }
     },
+    onReady() {
+      this.$refs.formRef.setRules(this.rules)
+    },
     mounted() {
-      this.sopInfo = { ...(this.$Route.query.curRow || {}) }
+      this.sopInfo = { ...(this.$Route.query.sop || {}) }
     },
     methods: {
-      fileChange({ value }) {
+      dateFormat,
+      fileChange(value) {
         this.formData.attachmentIds = value.map((item) => item.id).join(',')
       },
-
       async submitHandle() {
         const valid = await this.$refs.formRef.validate()
         if (!valid) return
@@ -123,63 +133,3 @@
     }
   }
 </script>
-
-<style lang="scss" scoped>
-  .sop-step {
-    height: 100vh;
-    .scroll-Y {
-      height: calc(100% - 88rpx);
-      .main {
-        padding: 24rpx;
-        .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>

+ 20 - 8
src/pages/sop/delay-warning/delay-history.vue

@@ -1,28 +1,40 @@
 <template>
-  <view class="delay-history dynamic-table">
-    <view v-for="(item, index) in tableData" :key="index" class="key-body">
-      <view class="key-title">{{ item.createName | timeFormat }}</view>
-      <view class="key-info">
-        <view class="key-value"> 跟进人:{{ item.createRealName }} </view>
-        <view class="key-value"> 备注:{{ item.remark }} </view>
-        <view class="key-value"> 附件说明: </view>
-        <attachment-view :ids="item.attachmentIds"></attachment-view>
+  <view class="delay-history my-page">
+    <view class="dynamic-table">
+      <view v-for="(item, index) in tableData" :key="index" class="my-card">
+        <view class="dynamic-body-item">
+          <view class="dynamic-body-title">{{ dateFormat(item.createTime) }}</view>
+          <view class="dynamic-body-info">
+            <view class="key-value"> 跟进人:{{ item.createName }} </view>
+            <view class="key-value"> 备注:{{ item.remark }} </view>
+            <view class="key-value"> 附件说明: </view>
+            <attachment-view :ids="item.attachmentIds"></attachment-view>
+          </view>
+        </view>
       </view>
+      <u-empty v-if="!tableData.length" text="暂无数据"></u-empty>
     </view>
   </view>
 </template>
 
 <script>
   import { delayWarnDetailList } from '@/api/sop'
+  import { dateFormat } from '@/utils/utils'
+  import AttachmentView from '@/components/attachment-view.vue'
 
   export default {
     name: 'DelayHistory',
+    components: { AttachmentView },
     data() {
       return {
         tableData: []
       }
     },
+    mounted() {
+      this.initData()
+    },
     methods: {
+      dateFormat,
       async initData() {
         const res = await delayWarnDetailList(this.$Route.query.id)
         this.tableData = res

+ 27 - 134
src/pages/sop/delay-warning/index.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="my-page">
     <scroll-view
-      :scroll-top="scrollTop"
       scroll-y="true"
       refresher-enabled="true"
       class="scroll-Y"
@@ -12,43 +11,31 @@
     >
       <view class="my-card" v-for="(item, index) in list" :key="index">
         <view class="my-card-header">
-          <view>
+          <view class="my-card-header-left">
             <text class="title">{{ item.code }}</text>
-            <text class="title">{{ WARN_FLOW_STATUS[item.status] }}</text>
+            <u-tag class="title" mode="light" :text="WARN_FLOW_STATUS[item.status]" :type="statusTheme[item.status]" size="mini" />
           </view>
-          <view class="my-card-header-right">{{ item.warnTime | timeFormat }}</view>
+          <view class="my-card-header-right">{{ dateFormat(item.warnTime) }}</view>
         </view>
         <view class="my-card-body">
           <view class="key-value"> 服务单元:{{ item.service }} </view>
-          <view class="key-value"> 预警时间:{{ item.warnTime | timeFormat }} </view>
+          <view class="key-value"> 预警时间:{{ dateFormat(item.warnTime) }} </view>
           <view class="key-value"> SOP流水号:{{ item.sopNo }} </view>
           <view class="key-value"> 节点负责人:{{ item.userName }} </view>
           <view class="key-value"> 客户名称:{{ item.custom }} </view>
           <view class="key-value"> 项目单号:{{ item.crmNo }} </view>
           <view class="key-value"> 项目名称:{{ item.crmName }} </view>
-          <view class="key-value"> 预警类型:{{ WARN_FLOW_STATUS[item.type] }} </view>
+          <view class="key-value"> 预警类型:{{ WARN_TYPE[item.type] }} </view>
           <view class="key-value"> 预警字段:{{ item.fieldObj }} </view>
         </view>
         <view class="my-card-footer">
-          <u-button
-            v-if="item.status !== 'CLOSE'"
-            class="u-button"
-            type="primary"
-            size="mini"
-            style="margin-right: 10rpx"
-            @click="closeHandler(item, 'view')"
+          <u-button v-if="item.status !== 'CLOSE'" class="u-button" type="default" size="mini" plain @click="closeHandler(item, 'view')"
             >关闭</u-button
           >
-          <u-button
-            v-if="item.status === 'CLOSE'"
-            class="u-button"
-            type="primary"
-            size="mini"
-            style="margin-right: 10rpx"
-            @click="restartHandler(item, 'view')"
+          <u-button v-if="item.status === 'CLOSE'" class="u-button" type="error" size="mini" plain @click="restartHandler(item, 'view')"
             >重启</u-button
           >
-          <u-button type="primary" size="mini" class="u-button" style="margin-right: 10rpx" @click="toDetail(item, 'view')">查看</u-button>
+          <u-button type="primary" size="mini" class="u-button" plain @click="toDetail(item, 'view')">查看</u-button>
           <u-button v-if="item.status !== 'CLOSE'" class="u-button" type="primary" size="mini" @click="toDetail(item, 'audit')">跟进</u-button>
         </view>
       </view>
@@ -64,15 +51,14 @@
 </template>
 
 <script>
-  import { dictToOptionList } from '@/utils/utils'
   import { WARN_TYPE, WARN_FLOW_STATUS } from '@/utils/constants'
   import { getDelayWarnList, closeDelayWarn, restartDelayWarn } from '@/api/sop'
+  import { dateFormat } from '@/utils/utils'
 
   export default {
     name: 'ProjectChangeReport',
     data() {
       return {
-        dictToOptionList,
         WARN_TYPE,
         WARN_FLOW_STATUS,
         params: {},
@@ -88,7 +74,7 @@
         statusTheme: {
           NOT_START: 'warning',
           FOLLOW: 'success',
-          CLOSE: 'default',
+          CLOSE: 'info',
           RESTART: 'primary'
         }
       }
@@ -97,6 +83,7 @@
       this.search()
     },
     methods: {
+      dateFormat,
       toDetail(item, type) {
         console.log('iii', item, type)
         this.$Router.push({ path: `/pages/sop/delay-warning/delay-detail`, query: { sop: item, type } })
@@ -128,6 +115,21 @@
           })
         }
       },
+      async updateList() {
+        const res = await getDelayWarnList({ ...this.params, pageNumber: this.pageNumber, pageSize: this.pageSize })
+        const records = res.records || []
+
+        let data = {}
+        records.forEach((item) => {
+          data[item.id] = item
+        })
+        for (let i = 0; i < this.list.length; i++) {
+          const item = this.list[i]
+          if (data[item.id]) {
+            this.list[i] = data[item.id]
+          }
+        }
+      },
       restartHandler(row) {
         this.curRow = row
         this.modalType = 'restart'
@@ -162,6 +164,7 @@
             type: 'success'
           })
         }
+        this.updateList()
       },
       onRefresh() {
         if (this.triggered) return
@@ -171,113 +174,3 @@
     }
   }
 </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>

+ 2 - 3
src/pages/sop/project-change-report/index.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="my-page">
     <scroll-view
-      :scroll-top="scrollTop"
       scroll-y="true"
       refresher-enabled="true"
       class="scroll-Y"
@@ -21,10 +20,10 @@
           <view class="key-value"> 客户名称:{{ item.customName }} </view>
           <view class="key-value"> 项目单号:{{ item.crmNo }} </view>
           <view class="key-value"> 项目名称:{{ item.crmName }} </view>
-          <view class="key-value"> 提交时间:{{ item.flowTime | timeFormat }} </view>
+          <view class="key-value"> 提交时间:{{ dateFormat(item.flowTime) }} </view>
         </view>
         <view class="my-card-footer">
-          <u-button class="u-button" type="primary" size="mini" @click="toPlanChange(item, 'view')">查看</u-button>
+          <u-button class="u-button" type="primary" size="mini" plain @click="toPlanChange(item, 'view')">查看</u-button>
           <u-button class="u-button" type="primary" size="mini" @click="toPlanChange(item, 'audit')">处理申请</u-button>
         </view>
       </view>

+ 107 - 65
src/pages/sop/project-change-report/plan-change.vue

@@ -1,81 +1,103 @@
 <template>
   <view class="my-page">
-    <scroll-view :scroll-top="scrollTop" scroll-y="true" class="scroll-Y">
-      <view class="my-page-main">
-        <RadiusCell :title="title" @click="showPopup1 = true"></RadiusCell>
-        <RadiusCell title="SOP项目计划变更说明" @click="showPopup2 = true"></RadiusCell>
+    <view class="my-page-main">
+      <RadiusCell :title="title" @click="showPopup1 = true"></RadiusCell>
+      <RadiusCell title="SOP项目计划变更说明" @click="showPopup2 = true"></RadiusCell>
+      <u-form :model="formData" ref="formRef" :border-bottom="false" label-position="top">
         <view class="my-card">
-          <u-form :model="formData" ref="formRef" :border-bottom="false" label-position="top">
-            <template v-if="!IS_NEW_MODE">
-              <u-form-item label="报备申请人"> {{ sopInfo.createRealName }}</u-form-item>
-              <u-form-item label="报备申请时间">
-                {{ sopInfo.createTime | timeFormat }}
-              </u-form-item>
-            </template>
+          <template v-if="!IS_NEW_MODE">
+            <view class="title-info">报备申请人:{{ sopInfo.createRealName }}</view>
+            <view class="title-info">报备申请时间:{{ dateFormat(sopInfo.createTime) }}</view>
+          </template>
 
-            <u-form-item label="变更类型" requiredMark>
-              <view style="padding-top: 3px">
-                <my-select v-model="formData.type" :disabled="readonly" :list="dictToOptionList(PLAN_CHANGE_TYPE)"></my-select>
-                <div class="sub-title gray">
-                  <p>1.关键信息及计划变更:项目关键信息里填写的项目关键信息或时间计划内容调整;</p>
-                  <p>2.项目取消:若项目取消,则只需填写变更原因后提交。</p>
-                </div>
+          <u-form-item label="变更类型" prop="type" :border-bottom="false" required>
+            <view>
+              <view class="tips-info color-warning" style="margin-bottom: 16rpx">
+                <p>1.关键信息及计划变更:项目关键信息里填写的项目关键信息或时间计划内容调整;</p>
+                <p>2.项目取消:若项目取消,则只需填写变更原因后提交。</p>
               </view>
-            </u-form-item>
+              <my-select v-model="formData.type" :disabled="readonly" :list="dictToOptionList(PLAN_CHANGE_TYPE)"></my-select>
+            </view>
+          </u-form-item>
 
-            <u-form-item label="变更原因" prop="reason">
-              <u-input :disabled="readonly" v-model="formData.reason" placeholder="50字以内" :maxlength="50"></u-input>
-            </u-form-item>
+          <u-form-item label="变更原因" prop="reason" :border-bottom="false" required>
+            <u-input v-model="formData.reason" placeholder="50字以内" :maxlength="50" border :disabled="readonly"></u-input>
+          </u-form-item>
+        </view>
 
-            <u-form-item label="项目信息及计划变更明细" prop="contentJson" requiredMark>
-              <dynamic-table ref="dTable" v-model="formData.contentJson" :columns="columns" :readonly="readonly"></dynamic-table>
-            </u-form-item>
+        <view class="my-card">
+          <u-form-item prop="contentJson" :border-bottom="false">
+            <dynamic-table
+              ref="dTable"
+              v-model="formData.contentJson"
+              :columns="columns"
+              :readonly="readonly"
+              header="项目信息及计划变更明细"
+            ></dynamic-table>
+          </u-form-item>
+        </view>
 
-            <template v-if="!IS_NEW_MODE">
-              <view class="form-group-title">变更处理结果</view>
-              <u-form-item label="处理结果">
-                <my-select v-model="formData.projectExchangeApprove" :list="approveList"></my-select>
-              </u-form-item>
+        <view v-if="!IS_NEW_MODE" class="my-card">
+          <view class="title-info">变更处理结果</view>
 
-              <u-form-item label="变更备注" prop="remark">
-                <u-input type="textarea" v-model="formData.remark"></u-input>
-              </u-form-item>
-            </template>
-          </u-form>
+          <u-form-item label="处理结果" prop="projectExchangeApprove" :border-bottom="false" required>
+            <my-select v-model="formData.projectExchangeApprove" :list="approveList"></my-select>
+          </u-form-item>
+
+          <u-form-item label="变更备注" prop="remark" :border-bottom="false">
+            <view class="tips-info color-danger">(取消变更、部分变更,此项必填)</view>
+            <u-input v-model="formData.remark" type="textarea" border></u-input>
+          </u-form-item>
         </view>
-      </view>
-      <view class="my-page-footer">
-        <u-row gutter="16">
-          <u-col :span="6"> <u-button type="default" @click="cancelHandle">取消</u-button> </u-col>
-          <u-col :span="6"><u-button type="primary" @click="submitHandle">保存</u-button></u-col>
-        </u-row>
-      </view>
-    </scroll-view>
+      </u-form>
+    </view>
+    <view class="my-page-footer">
+      <u-row gutter="16">
+        <u-col :span="6"> <u-button type="default" @click="cancelHandle">取消</u-button> </u-col>
+        <u-col :span="6"><u-button type="primary" @click="submitHandle">保存</u-button></u-col>
+      </u-row>
+    </view>
 
-    <u-popup v-model="showPopup2" mode="bottom" :mask-close-able="false" :closeable="true" :border-radius="28">
-      <view class="popup-box1">
-        <view class="title">{{ title }}</view>
-        <view class="content">
-          <view
-            >SOP项目计划需要变更时,可手动发起一个或多个申请,质控专员审核后,在SOP流程中进行计划变更调整,完成变更后,申请流程结束并通知到发起申请人。</view
-          >
+    <u-popup
+      v-model="showPopup2"
+      mode="bottom"
+      :mask-close-able="false"
+      :closeable="true"
+      close-icon-color="#000000"
+      close-icon-size="22"
+      border-radius="24"
+    >
+      <view class="my-popup">
+        <view class="popup-title">{{ title }}</view>
+        <view class="popup-content">
+          <view class="color-danger">
+            SOP项目计划需要变更时,可手动发起一个或多个申请,质控专员审核后,在SOP流程中进行计划变更调整,完成变更后,申请流程结束并通知到发起申请人。
+          </view>
         </view>
       </view>
     </u-popup>
 
-    <u-popup v-model="showPopup1" mode="bottom" :mask-close-able="false" :closeable="true" :border-radius="28">
-      <view class="popup-box2">
-        <view class="title">SOP项目计划变更说明</view>
-        <view class="content">
-          <view class="item">项目单号:{{ sopInfo.crmNo }}</view>
-          <view class="item">项目名称:{{ sopInfo.crmName }}</view>
-          <view class="item">派单时间:{{ sopInfo.crmBeginTime }}</view>
-          <view class="item">客户经理:{{ sopInfo.customManagerName }}</view>
-          <view class="item">客户类型:{{ CUSTOMER_TYPE[sopInfo.customType] }}</view>
-          <view class="item">客户名称:{{ sopInfo.customName }}</view>
-          <view class="item">考试时间:{{ sopInfo.examStartTime + ' 至 ' + sopInfo.examEndTime }}</view>
-          <view class="item">实施产品:{{ sopInfo.productName }}</view>
-          <view class="item">服务单元:{{ sopInfo.serviceUnitName }}</view>
+    <u-popup
+      v-model="showPopup1"
+      mode="bottom"
+      :mask-close-able="false"
+      :closeable="true"
+      close-icon-color="#000000"
+      close-icon-size="22"
+      border-radius="24"
+    >
+      <view class="my-popup">
+        <view class="popup-title">SOP项目计划变更说明</view>
+        <view class="popup-content pad">
+          <view class="key-value">项目单号:{{ sopInfo.crmNo }}</view>
+          <view class="key-value">项目名称:{{ sopInfo.crmName }}</view>
+          <view class="key-value">派单时间:{{ sopInfo.crmBeginTime }}</view>
+          <view class="key-value">客户经理:{{ sopInfo.customManagerName }}</view>
+          <view class="key-value">客户类型:{{ CUSTOMER_TYPE[sopInfo.customType] }}</view>
+          <view class="key-value">客户名称:{{ sopInfo.customName }}</view>
+          <view class="key-value">考试时间:{{ sopInfo.examStartTime + ' 至 ' + sopInfo.examEndTime }}</view>
+          <view class="key-value">实施产品:{{ sopInfo.productName }}</view>
+          <view class="key-value">服务单元:{{ sopInfo.serviceName }}</view>
         </view>
       </view>
     </u-popup>
@@ -86,7 +108,7 @@
 
 <script>
   import { createPlanChange, approvePlanChange, planChangeDetail } from '@/api/sop'
-  import { objAssign, dictToOptionList } from '@/utils/utils'
+  import { objAssign, dictToOptionList, dateFormat } from '@/utils/utils'
   import { omit } from 'lodash'
   import { CUSTOMER_TYPE, PLAN_CHANGE_TYPE } from '@/utils/constants'
   import RadiusCell from '@/components/radius-cell.vue'
@@ -101,6 +123,8 @@
         sopInfo: {},
         CUSTOMER_TYPE,
         PLAN_CHANGE_TYPE,
+        showPopup1: false,
+        showPopup2: false,
         formData: {
           serviceId: '',
           sopNo: '',
@@ -167,11 +191,17 @@
           }
         ],
         rules: {
+          type: [
+            {
+              required: true,
+              message: '请选择变更类型',
+              trigger: 'change'
+            }
+          ],
           reason: [
             {
               required: true,
               message: '请填写变更原因',
-              type: 'error',
               trigger: 'change'
             }
           ],
@@ -184,6 +214,13 @@
               }
             }
           ],
+          projectExchangeApprove: [
+            {
+              required: true,
+              message: '请选择处理结果',
+              trigger: 'change'
+            }
+          ],
           remark: [
             {
               validator: (rule, value, callback) => {
@@ -208,12 +245,17 @@
         return `项目派单信息(${this.sopInfo.crmNo})`
       }
     },
+    onReady() {
+      this.$refs.formRef.setRules(this.rules)
+    },
     mounted() {
       this.sopInfo = { ...(this.$Route.query.sop || {}) }
       this.initData()
+      console.log(this.$Route.query)
     },
     methods: {
       dictToOptionList,
+      dateFormat,
       async initData() {
         if (this.IS_NEW_MODE) {
           this.formData.serviceId = this.sopInfo.serviceId

+ 3 - 1
src/pages/sop/sop.vue

@@ -4,6 +4,7 @@
     <officeSopList v-if="current == 0"></officeSopList>
     <ProjectChangeReport v-if="current == 2"></ProjectChangeReport>
     <DelayWarning v-if="current == 3"></DelayWarning>
+    <ViolationRegistration v-if="current == 4"></ViolationRegistration>
   </view>
 </template>
 
@@ -11,10 +12,11 @@
   import OfficeSopList from './office-sop-list.vue'
   import ProjectChangeReport from './project-change-report/index.vue'
   import DelayWarning from './delay-warning/index.vue'
+  import ViolationRegistration from './violation-registration/index.vue'
 
   export default {
     name: 'Sop',
-    components: { OfficeSopList, ProjectChangeReport, DelayWarning },
+    components: { OfficeSopList, ProjectChangeReport, DelayWarning, ViolationRegistration },
     data() {
       return {
         tabList: [{ name: '教务处SOP' }, { name: '研究生SOP' }, { name: '项目计划变更' }, { name: '延期预警' }, { name: '违规登记' }],

+ 0 - 0
src/pages/sop/violation-registration.vue


+ 178 - 0
src/pages/sop/violation-registration/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <view class="my-page">
+    <scroll-view
+      scroll-y="true"
+      refresher-enabled="true"
+      class="scroll-Y"
+      :refresher-triggered="triggered"
+      refresher-background="transparent"
+      @refresherrefresh="onRefresh"
+      @scrolltolower="scrolltolower"
+    >
+      <view class="my-card" v-for="(item, index) in list" :key="index">
+        <view class="my-card-header">
+          <view class="my-card-header-left">
+            <text class="title">{{ item.code }}</text>
+            <u-tag class="title" mode="light" :text="VIOLATION_FLOW_STATUS[item.status]" :type="statusTheme[item.status]" size="mini" />
+          </view>
+          <text class="my-card-header-right">{{ dateFormat(item.createTime) }}</text>
+        </view>
+        <view class="my-card-body">
+          <view class="key-value"> SOP流水号:{{ item.sopNo }} </view>
+          <view class="key-value"> 节点负责人:{{ item.userName }} </view>
+          <view class="key-value"> 客户名称:{{ item.custom }} </view>
+          <view class="key-value"> 项目单号:{{ item.crmNo }} </view>
+          <view class="key-value"> 项目名称:{{ item.crmName }} </view>
+          <view class="key-value"> 违规类型:{{ VIOLATION_TYPE[item.type] }} </view>
+          <view class="key-value"> 违规时间:{{ dateFormat(item.createTime) }} </view>
+        </view>
+        <view class="my-card-footer">
+          <u-button v-if="item.status !== 'CLOSE'" class="u-button" type="default" size="mini" plain @click="closeHandler(item, 'view')"
+            >关闭</u-button
+          >
+          <u-button v-if="item.status === 'CLOSE'" class="u-button" type="error" size="mini" plain @click="restartHandler(item, 'view')"
+            >重启</u-button
+          >
+          <u-button type="primary" size="mini" class="u-button" plain @click="toDetail(item, 'view')">查看</u-button>
+          <u-button v-if="item.status !== 'CLOSE'" class="u-button" type="primary" size="mini" @click="toFollow(item)">跟进</u-button>
+        </view>
+      </view>
+      <view class="my-page-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" />
+    <u-modal v-model="showModal" :content="modalContent" @confirm="modalConfirm"></u-modal>
+  </view>
+</template>
+
+<script>
+  import { VIOLATION_TYPE, VIOLATION_FLOW_STATUS } from '@/utils/constants'
+  import { getViolationList, closeViolation, restartViolation } from '@/api/sop'
+  import { dateFormat } from '@/utils/utils'
+
+  export default {
+    name: 'ViolationRegistration',
+    data() {
+      return {
+        VIOLATION_TYPE,
+        VIOLATION_FLOW_STATUS,
+        params: {},
+        pageNumber: 1,
+        pageSize: 10,
+        loadingFlag: 0,
+        list: [],
+        triggered: false,
+        curRow: {},
+        modalType: '',
+        modalContent: '',
+        showModal: false,
+        statusTheme: {
+          NOT_START: 'warning',
+          FOLLOW: 'success',
+          CLOSE: 'info',
+          RESTART: 'primary'
+        }
+      }
+    },
+    mounted() {
+      this.search()
+    },
+    methods: {
+      dateFormat,
+      toDetail(item) {
+        console.log(item)
+      },
+      toFollow(item) {
+        console.log('iii', item)
+        this.$Router.push({ path: `/pages/sop/violation-registration/violation-detail`, query: { sop: item } })
+      },
+      scrolltolower() {
+        if (this.loadingFlag > 0) {
+          return
+        }
+
+        this.loadingFlag = 1
+        this.pageNumber++
+        this.getList()
+      },
+      search(bool) {
+        this.pageNumber = 1
+        this.list = []
+        this.getList(bool)
+      },
+      async getList(bool) {
+        const res = await getViolationList({ ...this.params, pageNumber: this.pageNumber, pageSize: this.pageSize })
+        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'
+          })
+        }
+      },
+      async updateList() {
+        const res = await getViolationList({ ...this.params, pageNumber: this.pageNumber, pageSize: this.pageSize })
+        const records = res.records || []
+
+        let data = {}
+        records.forEach((item) => {
+          data[item.id] = item
+        })
+        for (let i = 0; i < this.list.length; i++) {
+          const item = this.list[i]
+          if (data[item.id]) {
+            this.list[i] = data[item.id]
+          }
+        }
+      },
+      restartHandler(row) {
+        this.curRow = row
+        this.modalType = 'restart'
+        this.modalContent = '您确定要重启当前违规登记吗?'
+        this.showModal = true
+      },
+      closeHandler(row) {
+        this.curRow = row
+        this.modalType = 'close'
+        this.modalContent = '您确定要关闭当前违规登记吗?'
+        this.showModal = true
+      },
+      async modalConfirm() {
+        if (this.modalType === 'restart') {
+          const res = await restartViolation(this.curRow.id).catch(() => {})
+          this.showModal = false
+
+          if (!res) return
+          this.$refs.uToast.show({
+            title: '操作成功',
+            type: 'success'
+          })
+        }
+
+        if (this.modalType === 'close') {
+          const res = await closeViolation(this.curRow.id).catch(() => {})
+          this.showModal = false
+
+          if (!res) return
+          this.$refs.uToast.show({
+            title: '操作成功',
+            type: 'success'
+          })
+        }
+
+        this.updateList()
+      },
+      onRefresh() {
+        if (this.triggered) return
+        this.triggered = true
+        this.search(true)
+      }
+    }
+  }
+</script>

+ 132 - 0
src/pages/sop/violation-registration/violation-detail.vue

@@ -0,0 +1,132 @@
+<template>
+  <view class="my-page">
+    <view class="my-page-main">
+      <RadiusCell title="SOP信息" @click="showPopup1 = true"></RadiusCell>
+      <view class="my-card">
+        <u-form :model="formData" ref="formRef" :border-bottom="false" label-position="top">
+          <view class="title-info"> 变更处理结果</view>
+          <view class="title-info"> 报备申请人:{{ sopInfo.createName }}</view>
+          <view class="title-info"> 报备申请时间:{{ dateFormat(sopInfo.createTime) }} </view>
+
+          <u-form-item label="跟进说明" prop="remark" :border-bottom="false" required>
+            <u-input v-model="formData.remark" type="textarea" placeholder="请输入说明" :maxlength="100" border></u-input>
+          </u-form-item>
+
+          <u-form-item label="附件说明" :border-bottom="false">
+            <upload-image :config="{ length: 3 }" @get-lists="fileChange"> </upload-image>
+          </u-form-item>
+        </u-form>
+      </view>
+      <RadiusCell title="历史明细查询" @click="toHistory"></RadiusCell>
+    </view>
+    <view class="my-page-footer">
+      <u-row gutter="16">
+        <u-col :span="6"> <u-button type="default" @click="cancelHandle">取消</u-button> </u-col>
+        <u-col :span="6"><u-button type="primary" @click="submitHandle">保存</u-button></u-col>
+      </u-row>
+    </view>
+
+    <u-popup
+      v-model="showPopup1"
+      mode="bottom"
+      :mask-close-able="false"
+      :closeable="true"
+      close-icon-color="#000000"
+      close-icon-size="22"
+      border-radius="24"
+    >
+      <view class="my-popup">
+        <view class="popup-title">SOP信息</view>
+        <view class="popup-content pad">
+          <view class="key-value"> 服务单元:{{ sopInfo.service }} </view>
+          <view class="key-value"> SOP流水号:{{ sopInfo.sopNo }} </view>
+          <view class="key-value"> 客户类型:{{ CUSTOMER_TYPE[sopInfo.customType] }} </view>
+          <view class="key-value"> 客户名称:{{ sopInfo.custom }} </view>
+          <view class="key-value"> 违规时间:{{ dateFormat(sopInfo.createTime) }} </view>
+          <view class="key-value"> 登记人:{{ sopInfo.createName }} </view>
+          <view class="key-value"> 节点负责人:{{ sopInfo.userName }} </view>
+          <view class="key-value"> 违规类型:{{ VIOLATION_TYPE[sopInfo.type] }} </view>
+          <view class="key-value"> 违规情况说明:{{ sopInfo.content }} </view>
+          <view class="key-value"> 附件说明: </view>
+          <attachment-view :ids="sopInfo.attachmentIds"></attachment-view>
+        </view>
+      </view>
+    </u-popup>
+
+    <u-toast ref="uToast" />
+  </view>
+</template>
+
+<script>
+  import RadiusCell from '@/components/radius-cell.vue'
+  import { flowViolation } from '@/api/sop'
+  import UploadImage from '@/components/low-code/UPLOAD_IMAGE.vue'
+  import { VIOLATION_TYPE, CUSTOMER_TYPE } from '@/utils/constants'
+  import { dateFormat } from '@/utils/utils'
+  import AttachmentView from '@/components/attachment-view.vue'
+
+  export default {
+    name: 'ViolationDetail',
+    components: { RadiusCell, UploadImage, AttachmentView },
+    data() {
+      return {
+        VIOLATION_TYPE,
+        CUSTOMER_TYPE,
+        sopInfo: {},
+        showPopup1: false,
+        formData: {
+          attachmentIds: '',
+          remark: ''
+        },
+        rules: {
+          remark: [
+            {
+              required: true,
+              message: '请填写跟进说明',
+              trigger: 'change'
+            }
+          ]
+        }
+      }
+    },
+    computed: {
+      title() {
+        return `${this.sopInfo.code}`
+      }
+    },
+    onReady() {
+      this.$refs.formRef.setRules(this.rules)
+    },
+    mounted() {
+      this.sopInfo = { ...(this.$Route.query.sop || {}) }
+    },
+    methods: {
+      dateFormat,
+      fileChange(value) {
+        this.formData.attachmentIds = value.map((item) => item.id).join(',')
+      },
+      async submitHandle() {
+        const valid = await this.$refs.formRef.validate()
+        if (!valid) return
+
+        const res = await flowViolation({
+          ...this.formData,
+          violationId: this.sopInfo.id
+        }).catch(() => {})
+        if (!res) return
+
+        this.$refs.uToast.show({
+          title: '提交成功',
+          type: 'success'
+        })
+        this.$Router.back()
+      },
+      cancelHandle() {
+        this.$Router.back()
+      },
+      toHistory() {
+        this.$Router.push({ path: '/pages/sop/violation-registration/violation-history', query: { id: this.sopInfo.id } })
+      }
+    }
+  }
+</script>

+ 45 - 0
src/pages/sop/violation-registration/violation-history.vue

@@ -0,0 +1,45 @@
+<template>
+  <view class="delay-history my-page">
+    <view class="dynamic-table">
+      <view v-for="(item, index) in tableData" :key="index" class="my-card">
+        <view class="dynamic-body-item">
+          <view class="dynamic-body-title">{{ dateFormat(item.createTime) }}</view>
+          <view class="dynamic-body-info">
+            <view class="key-value"> 跟进人:{{ item.createName }} </view>
+            <view class="key-value"> 备注:{{ item.remark }} </view>
+            <view class="key-value"> 附件说明: </view>
+            <attachment-view :ids="item.attachmentIds"></attachment-view>
+          </view>
+        </view>
+      </view>
+      <u-empty v-if="!tableData.length" text="暂无数据"></u-empty>
+    </view>
+  </view>
+</template>
+
+<script>
+  import { violationDetailList } from '@/api/sop'
+  import { dateFormat } from '@/utils/utils'
+  import AttachmentView from '@/components/attachment-view.vue'
+
+  export default {
+    name: 'ViolationHistory',
+    components: { AttachmentView },
+    data() {
+      return {
+        tableData: []
+      }
+    },
+    mounted() {
+      console.log('111')
+      this.initData()
+    },
+    methods: {
+      dateFormat,
+      async initData() {
+        const res = await violationDetailList(this.$Route.query.id)
+        this.tableData = res
+      }
+    }
+  }
+</script>

+ 140 - 1
src/styles/global.scss

@@ -15,6 +15,12 @@
 .color-lighter {
   color: #8c8c8c;
 }
+.color-danger {
+  color: #dd524d;
+}
+.color-warning {
+  color: #ff7d00;
+}
 
 // my-page
 .my-page {
@@ -52,6 +58,23 @@
     background-color: #fff;
     padding: 16rpx 32rpx;
     text-align: center;
+    border-top: 2rpx solid #d9d9d9;
+  }
+
+  .tips-info {
+    line-height: 40rpx;
+    font-size: 24rpx;
+  }
+
+  // u-view
+  .u-form-item {
+    line-height: 44rpx !important;
+    padding: 0 !important;
+    margin-bottom: 24rpx;
+
+    .u-form-left {
+      color: #595959;
+    }
   }
 }
 // my-card
@@ -70,6 +93,20 @@
     justify-content: space-between;
     align-items: center;
 
+    &-left {
+      font-size: 0;
+      > .title {
+        display: inline-block;
+        vertical-align: middle;
+        font-size: 32rpx;
+
+        &:not(:first-child) {
+          margin-left: 12rpx;
+          font-size: 24rpx;
+        }
+      }
+    }
+
     &-right {
       color: #8c8c8c;
       font-size: 24rpx;
@@ -88,6 +125,108 @@
   }
   .key-value {
     margin-bottom: 12rpx;
+    font-weight: 400;
+    line-height: 40rpx;
+  }
+  .title-info {
+    font-size: 28rpx;
+    margin-bottom: 12rpx;
+    font-weight: bold;
+  }
+}
+// my-popup
+.my-popup {
+  padding: 32rpx 32rpx 100rpx;
+  min-height: 400rpx;
+  position: relative;
+  max-height: 100%;
+
+  display: flex;
+  flex-direction: column;
+
+  .popup-title {
+    height: 44rpx;
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #262626;
+    line-height: 44rpx;
+    text-align: center;
+
+    flex-grow: 0;
+    flex-shrink: 0;
+  }
+  .popup-footer {
+    flex-grow: 0;
+    flex-shrink: 0;
+  }
+  .popup-content {
+    margin-top: 32rpx;
+    font-size: 24rpx;
+    font-weight: 400;
+    line-height: 40rpx;
+    color: #262626;
+    flex-grow: 2;
+
+    &.pad {
+      padding: 24rpx;
+      border-radius: 12rpx;
+      background: #f7f7f7;
+    }
+  }
+
+  .key-value {
+    margin-bottom: 12rpx;
+    font-weight: 400;
+    line-height: 40rpx;
+  }
+}
+// dynamic-table
+.dynamic-table {
+  .dynamic-header {
+    display: flex;
+    justify-content: space-between;
+    align-self: center;
+    margin-bottom: 20rpx;
+
+    > h2 {
+      line-height: 44rpx;
+      font-size: 28rpx;
+      color: #262626;
+      font-weight: 500;
+    }
+  }
+  .dynamic-body {
+    &-item {
+      padding: 30rpx 0;
+
+      &:not(:last-child) {
+        border-bottom: 2rpx solid #f0f0f0;
+      }
+    }
+    &-title {
+      font-size: 32rpx;
+      font-weight: 500;
+      color: #262626;
+      line-height: 48rpx;
+    }
+    &-info {
+      background: #f7f7f7;
+      border-radius: 12rpx;
+      padding: 24rpx;
+      margin: 12rpx 0 0;
+      color: #8c8c8c;
+      .key-value {
+        line-height: 40rpx;
+        font-size: 24rpx;
+
+        &:not(:last-child) {
+          margin-bottom: 8rpx;
+        }
+      }
+    }
+    &-foolter {
+      margin-top: 24rpx;
+    }
   }
 }
 
@@ -96,6 +235,6 @@
   margin-left: 24rpx;
 }
 
-.mr-2{
+.mr-2 {
   margin-right: 2px;
 }

+ 6 - 2
src/utils/utils.js

@@ -566,7 +566,7 @@ export const dictToOptionList = (data) => {
   })
 }
 /* 日期格式化 */
-export const dateFormat = (date, fmt = 'yyyy-MM-dd hh:mm:ss', isDefault = '-') => {
+export const dateFormat = (date, fmt = 'yyyy-MM-dd hh:mm', isDefault = '-') => {
   if (!date) {
     return '-'
   }
@@ -628,6 +628,10 @@ export function randomCode(len = 16) {
   return stepNums.join('')
 }
 
+export function fillZero(num, fullLength = 1) {
+  return ('0'.repeat(fullLength) + num).slice(-fullLength)
+}
+
 /**
  * 判断对象类型
  * @param {*} obj 对象
@@ -654,5 +658,5 @@ export function objTypeOf(obj) {
 export const timeCompare = (startStr, time, endStr) => {
   let start = new Date(startStr) //开始时间
   let end = new Date(endStr) //结束时间
-  return start.getTime() > time && time < end.getTime();
+  return start.getTime() > time && time < end.getTime()
 }