Browse Source

添加地图

lex 2 years ago
parent
commit
bc2d0e6000
34 changed files with 2931 additions and 694 deletions
  1. 87 0
      package-lock.json
  2. 2 0
      package.json
  3. 6 1
      src/components/m-image-preview/index.module.less
  4. 97 31
      src/components/m-image-preview/index.tsx
  5. 10 1
      src/components/m-search/index.module.less
  6. 15 7
      src/components/m-uploader/index.module.less
  7. 13 7
      src/components/m-uploader/index.tsx
  8. 359 0
      src/components/m-uploader/inside.tsx
  9. 53 0
      src/components/m-video/index.module.less
  10. 193 0
      src/components/m-video/index.tsx
  11. 57 0
      src/helpers/constant.ts
  12. 24 0
      src/helpers/toolsValidate.ts
  13. 8 0
      src/router/routes-common.ts
  14. 7 0
      src/styles/index.less
  15. BIN
      src/views/download/images/bg.png
  16. BIN
      src/views/download/images/btn-bg.png
  17. BIN
      src/views/download/images/center.png
  18. 21 0
      src/views/download/index.module.less
  19. 9 1
      src/views/download/index.tsx
  20. 2 2
      src/views/layout/login.tsx
  21. 505 97
      src/views/patrol-evaluation/detail-list.tsx
  22. 90 2
      src/views/patrol-evaluation/detail.module.less
  23. 164 48
      src/views/patrol-evaluation/detail.tsx
  24. 2 0
      src/views/patrol-evaluation/index.module.less
  25. 208 60
      src/views/patrol-evaluation/index.tsx
  26. 38 128
      src/views/patrol-evaluation/skeletion-detail.modal.tsx
  27. 0 1
      src/views/site-management/drop-down-modal.tsx
  28. 152 108
      src/views/site-management/index.tsx
  29. 1 0
      src/views/site-management/site-settings.module.less
  30. 115 53
      src/views/site-management/site-settings.tsx
  31. 10 0
      src/views/teacher-attendance/amap-gps.module.less
  32. 261 0
      src/views/teacher-attendance/amap-gps.tsx
  33. 245 86
      src/views/teacher-attendance/detail.tsx
  34. 177 61
      src/views/teacher-attendance/index.tsx

+ 87 - 0
package-lock.json

@@ -9,10 +9,12 @@
       "version": "0.2.0",
       "license": "MIT",
       "dependencies": {
+        "@amap/amap-jsapi-loader": "^1.0.1",
         "@vant/use": "^1.5.1",
         "clean-deep": "^3.4.0",
         "dayjs": "^1.11.7",
         "numeral": "^2.0.6",
+        "plyr": "^3.7.8",
         "query-string": "^8.1.0",
         "umi-request": "^1.4.0",
         "vant": "^4.1.2",
@@ -56,6 +58,11 @@
         "node": ">=14.20.0"
       }
     },
+    "node_modules/@amap/amap-jsapi-loader": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
+      "integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
+    },
     "node_modules/@ampproject/remapping": {
       "version": "2.2.1",
       "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -3443,6 +3450,12 @@
         "is-what": "^3.14.1"
       }
     },
+    "node_modules/core-js": {
+      "version": "3.30.2",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.30.2.tgz",
+      "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==",
+      "hasInstallScript": true
+    },
     "node_modules/core-js-compat": {
       "version": "3.30.1",
       "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.30.1.tgz",
@@ -3483,6 +3496,11 @@
       "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.21.tgz",
       "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
     },
+    "node_modules/custom-event-polyfill": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
+      "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="
+    },
     "node_modules/dayjs": {
       "version": "1.11.7",
       "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz",
@@ -5401,6 +5419,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/loadjs": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/loadjs/-/loadjs-4.2.0.tgz",
+      "integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA=="
+    },
     "node_modules/local-pkg": {
       "version": "0.4.3",
       "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz",
@@ -6433,6 +6456,18 @@
         "node": "^12.17.0 || ^14.13 || >=16.0.0"
       }
     },
+    "node_modules/plyr": {
+      "version": "3.7.8",
+      "resolved": "https://registry.npmmirror.com/plyr/-/plyr-3.7.8.tgz",
+      "integrity": "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==",
+      "dependencies": {
+        "core-js": "^3.26.1",
+        "custom-event-polyfill": "^1.0.7",
+        "loadjs": "^4.2.0",
+        "rangetouch": "^2.0.1",
+        "url-polyfill": "^1.1.12"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.23",
       "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz",
@@ -6559,6 +6594,11 @@
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
       "dev": true
     },
+    "node_modules/rangetouch": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/rangetouch/-/rangetouch-2.0.1.tgz",
+      "integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="
+    },
     "node_modules/readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -7403,6 +7443,11 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/url-polyfill": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmmirror.com/url-polyfill/-/url-polyfill-1.1.12.tgz",
+      "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -8010,6 +8055,11 @@
     }
   },
   "dependencies": {
+    "@amap/amap-jsapi-loader": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
+      "integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
+    },
     "@ampproject/remapping": {
       "version": "2.2.1",
       "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -10419,6 +10469,11 @@
         "is-what": "^3.14.1"
       }
     },
+    "core-js": {
+      "version": "3.30.2",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.30.2.tgz",
+      "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg=="
+    },
     "core-js-compat": {
       "version": "3.30.1",
       "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.30.1.tgz",
@@ -10450,6 +10505,11 @@
       "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.21.tgz",
       "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
     },
+    "custom-event-polyfill": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
+      "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="
+    },
     "dayjs": {
       "version": "1.11.7",
       "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz",
@@ -11940,6 +12000,11 @@
         }
       }
     },
+    "loadjs": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/loadjs/-/loadjs-4.2.0.tgz",
+      "integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA=="
+    },
     "local-pkg": {
       "version": "0.4.3",
       "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz",
@@ -12768,6 +12833,18 @@
         }
       }
     },
+    "plyr": {
+      "version": "3.7.8",
+      "resolved": "https://registry.npmmirror.com/plyr/-/plyr-3.7.8.tgz",
+      "integrity": "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==",
+      "requires": {
+        "core-js": "^3.26.1",
+        "custom-event-polyfill": "^1.0.7",
+        "loadjs": "^4.2.0",
+        "rangetouch": "^2.0.1",
+        "url-polyfill": "^1.1.12"
+      }
+    },
     "postcss": {
       "version": "8.4.23",
       "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz",
@@ -12864,6 +12941,11 @@
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
       "dev": true
     },
+    "rangetouch": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/rangetouch/-/rangetouch-2.0.1.tgz",
+      "integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="
+    },
     "readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -13523,6 +13605,11 @@
         "punycode": "^2.1.0"
       }
     },
+    "url-polyfill": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmmirror.com/url-polyfill/-/url-polyfill-1.1.12.tgz",
+      "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",

+ 2 - 0
package.json

@@ -22,10 +22,12 @@
     "prepare": "husky install"
   },
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@vant/use": "^1.5.1",
     "clean-deep": "^3.4.0",
     "dayjs": "^1.11.7",
     "numeral": "^2.0.6",
+    "plyr": "^3.7.8",
     "query-string": "^8.1.0",
     "umi-request": "^1.4.0",
     "vant": "^4.1.2",

+ 6 - 1
src/components/m-image-preview/index.module.less

@@ -1,3 +1,9 @@
+.overlayPreview {
+  --van-overlay-background: rgba(0, 0, 0, 0.9) !important;
+
+}
+
+
 .imagePreview {
   --van-image-preview-close-icon-size: 32px;
   --van-image-preview-index-line-height: 32px;
@@ -10,6 +16,5 @@
       left: initial;
       font-size: 32px;
     }
-
   }
 }

+ 97 - 31
src/components/m-image-preview/index.tsx

@@ -1,16 +1,27 @@
 import {
-  ImagePreview,
   Image,
   Icon,
   showLoadingToast,
   showSuccessToast,
-  showFailToast
+  showFailToast,
+  Popup,
+  Swipe,
+  SwipeItem
 } from 'vant';
-import { PropType, defineComponent, onMounted, reactive, watch } from 'vue';
+import {
+  PropType,
+  defineComponent,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue';
 import styles from './index.module.less';
 import iconPreviewClose from '@/common/images/icon-preview-close.png';
 import iconPreviewDownload from '@/common/images/icon-preview-download.png';
 import { promisefiyPostMessage } from '@/helpers/native-message';
+import { checkFile } from '@/helpers/toolsValidate';
+import MVideo from '../m-video';
 
 export default defineComponent({
   name: 'm-image-preview',
@@ -35,21 +46,21 @@ export default defineComponent({
       type: Boolean,
       default: false
     },
-    closeIconPosition: {
-      type: String as PropType<
-        'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
-      >,
-      default: 'top-left'
-    },
     showDownload: {
       type: Boolean,
       default: true
+    },
+    teleport: {
+      type: String,
+      default: ''
     }
   },
   emits: ['update:show'],
   setup(props, { emit }) {
+    console.log(props.startPosition, 'update');
     const forms = reactive({
       show: false,
+      index: props.startPosition + 1,
       saveLoading: false
     });
 
@@ -75,6 +86,15 @@ export default defineComponent({
       forms.saveLoading = false;
     };
 
+    const videoRef: any = ref([]);
+    const onPlay = (index: any) => {
+      videoRef.value.forEach((item: any, child: any) => {
+        if (child !== index) {
+          item.onStop();
+        }
+      });
+    };
+
     onMounted(() => {
       forms.show = props.show;
     });
@@ -85,30 +105,76 @@ export default defineComponent({
         forms.show = props.show;
       }
     );
+    watch(
+      () => props.startPosition,
+      () => {
+        forms.index = props.startPosition + 1;
+      }
+    );
     return () => (
-      <ImagePreview
-        class={styles.imagePreview}
+      <Popup
+        teleport={props.teleport}
         v-model:show={forms.show}
-        images={props.images}
-        showIndex={props.showIndex}
-        startPosition={props.startPosition}
-        maxZoom={3}
-        minZoom={'1/3'}
-        loop={props.loop}
-        onClose={() => emit('update:show', forms.show)}
-        closeable
-        closeIcon={iconPreviewClose}
-        closeIconPosition="top-left">
-        {{
-          cover: () =>
-            props.showDownload && (
-              <Icon name={iconPreviewDownload} onClick={onSave} />
-            ),
-          image: ({ src }: any) => (
-            <Image class="van-image-preview__image" src={src} />
-          )
-        }}
-      </ImagePreview>
+        overlay-class={styles.overlayPreview}
+        class={['van-image-preview', styles.imagePreview]}>
+        <Icon
+          name={iconPreviewClose}
+          class="van-image-preview__close-icon van-image-preview__close-icon--top-left van-haptics-feedback"
+          onClick={() => {
+            onPlay(-1);
+            emit('update:show', false);
+          }}
+        />
+        <div class={'van-image-preview__index'}>
+          {forms.index} / {props.images.length}
+        </div>
+        {props.showDownload ? (
+          <Icon
+            name={iconPreviewDownload}
+            class="van-image-preview__close-icon van-image-preview__close-icon--top-right van-haptics-feedback"
+            onClick={onSave}
+          />
+        ) : (
+          ''
+        )}
+        <Swipe
+          autoplay={0}
+          loop={false}
+          class={'van-image-preview__swipe'}
+          showIndicators={false}
+          initialSwipe={props.startPosition}
+          onChange={(index: number) => {
+            forms.index = index + 1;
+            onPlay(index);
+          }}
+          lazyRender>
+          {props.images.map((url: string, index: number) => (
+            <SwipeItem
+              class={'van-image-preview__swipe-item'}
+              onClick={() => {
+                onPlay(-1);
+                emit('update:show', false);
+              }}>
+              {checkFile(url, 'image') ? (
+                <Image class="van-image-preview__image" src={url} />
+              ) : (
+                <div
+                  class="van-image-preview__image"
+                  onClick={(e: MouseEvent) => {
+                    e.stopPropagation();
+                    e.preventDefault();
+                  }}>
+                  <MVideo
+                    ref={(el: any) => (videoRef.value[index] = el)}
+                    src={url}
+                    onPlay={() => onPlay(index)}
+                  />
+                </div>
+              )}
+            </SwipeItem>
+          ))}
+        </Swipe>
+      </Popup>
     );
   }
 });

+ 10 - 1
src/components/m-search/index.module.less

@@ -1,17 +1,22 @@
 .m-search {
   --van-cell-background-color: transparent;
+
   input::placeholder {
     color: var(--k-gray-4);
   }
+
   :global {
     .van-field__control {
       -webkit-user-select: text !important;
       user-select: text !important;
       font-size: 13px;
     }
+
     .van-search__field {
       background: transparent !important;
+      padding: 0 var(--van-padding-xs) 0 0;
     }
+
     .van-field__right-icon {
       display: flex;
       align-items: center;
@@ -19,6 +24,7 @@
       padding-right: 4px;
     }
   }
+
   &.default {
     :global {
       .van-search__content {
@@ -39,12 +45,15 @@
     :global {
       .van-search__content {
         background: rgba(255, 255, 255, 0.16);
+
         input::placeholder {
           color: #fff;
         }
+
         input {
           color: #fff;
         }
+
         .van-field__clear {
           color: #fff;
         }
@@ -61,4 +70,4 @@
     --van-button-mini-height: 28px;
     --van-font-size-xs: 14px;
   }
-}
+}

+ 15 - 7
src/components/m-uploader/index.module.less

@@ -4,6 +4,9 @@
   flex-wrap: wrap;
   box-sizing: border-box;
   position: relative;
+
+  --upload-file-size: 74px;
+
   .img-close {
     position: absolute;
     top: 5px;
@@ -20,35 +23,40 @@
     align-items: center;
     border-radius: 50%;
   }
+
   .singleImgClose {
     right: 5px;
   }
 
   .uploader {
     position: relative;
+
     &.default {
       :global {
         .van-uploader__upload {
-          width: 76px;
-          height: 76px;
+          width: var(--upload-file-size);
+          height: var(--upload-file-size);
           background-color: #fff;
         }
       }
+
       .previewImg {
-        width: 76px;
-        height: 76px;
+        width: var(--upload-file-size);
+        height: var(--upload-file-size);
         border-radius: 4px;
         overflow: hidden;
       }
 
       .uploadImg {
-        width: 76px;
-        height: 76px;
+        width: var(--upload-file-size);
+        height: var(--upload-file-size);
         border-radius: 4px;
         overflow: hidden;
       }
     }
+
     :global {
+
       .van-uploader__upload-icon,
       .van-icon__image {
         width: 100%;
@@ -56,4 +64,4 @@
       }
     }
   }
-}
+}

+ 13 - 7
src/components/m-uploader/index.tsx

@@ -68,6 +68,10 @@ export default defineComponent({
     disabled: {
       type: Boolean,
       default: false
+    },
+    position: {
+      type: String as PropType<'outside' | 'inside'>,
+      default: 'outside'
     }
   },
   emits: ['uploadChange', 'update:modelValue'],
@@ -139,7 +143,7 @@ export default defineComponent({
       // 上传文件
       try {
         // 获取签名
-        const signUrl = '/api-school/open/getUploadSign';
+        const signUrl = '/api-web/getUploadSign';
         const tempName = file.name || '';
         const fileName =
           this.path + '/' + (tempName && tempName.replace(/ /gi, '_'));
@@ -171,7 +175,7 @@ export default defineComponent({
           KSSAccessKeyId: res.data.kssAccessKeyId,
           acl: 'public-read',
           name: fileName
-        };
+        } as any;
         const formData = new FormData();
         for (const key in obj) {
           formData.append(key, obj[key]);
@@ -205,7 +209,7 @@ export default defineComponent({
         {this.modelValue.length > 0 &&
           this.maxCount > 1 &&
           this.modelValue.map((item: any) => (
-            <div class={[styles.uploader, styles[this.size]]}>
+            <div class={['van-uploader', styles.uploader, styles[this.size]]}>
               {/* 删除按钮 */}
               {this.deletable && !this.disabled && (
                 <Icon
@@ -234,7 +238,7 @@ export default defineComponent({
             // 小于长度才显示
             this.modelValue.length < this.maxCount && (
               <div
-                class={[styles.uploader, styles[this.size]]}
+                class={['van-uploader', styles.uploader, styles[this.size]]}
                 onClick={this.nativeUpload}>
                 <Icon
                   name={this.uploadIcon}
@@ -245,7 +249,7 @@ export default defineComponent({
             )
           ) : (
             <div
-              class={[styles.uploader, styles[this.size]]}
+              class={['van-uploader', styles.uploader, styles[this.size]]}
               onClick={this.nativeUpload}>
               {this.modelValue.length > 0 ? (
                 <div class={['van-uploader__upload']}>
@@ -290,7 +294,7 @@ export default defineComponent({
           // 小于长度才显示
           this.modelValue.length < this.maxCount && (
             <Uploader
-              class={[styles.uploader, styles[this.size]]}
+              class={['van-uploader', styles.uploader, styles[this.size]]}
               afterRead={this.afterRead}
               beforeRead={this.beforeRead}
               beforeDelete={this.beforeDelete}
@@ -302,7 +306,7 @@ export default defineComponent({
           )
         ) : (
           <Uploader
-            class={[styles.uploader, styles[this.size]]}
+            class={['van-uploader', styles.uploader, styles[this.size]]}
             afterRead={this.afterRead}
             beforeRead={this.beforeRead}
             beforeDelete={this.beforeDelete}
@@ -349,6 +353,8 @@ export default defineComponent({
             )}
           </Uploader>
         )}
+
+        {this.$slots.default && this.$slots.default()}
       </div>
     );
   }

+ 359 - 0
src/components/m-uploader/inside.tsx

@@ -0,0 +1,359 @@
+import {
+  closeToast,
+  Icon,
+  Image,
+  showLoadingToast,
+  showToast,
+  Uploader
+} from 'vant';
+import { defineComponent, PropType } from 'vue';
+import styles from './index.module.less';
+import { useCustomFieldValue } from '@vant/use';
+import { postMessage } from '@/helpers/native-message';
+import umiRequest from 'umi-request';
+import iconUploader from '@common/images/icon-upload.png';
+// import iconUploadClose from '@common/images/icon-upload-close.png';
+import request from '@/helpers/request';
+import { getOssUploadUrl } from '@/state';
+
+export default defineComponent({
+  name: 'col-upload',
+  props: {
+    modelValue: {
+      type: Array,
+      default: () => []
+    },
+    deletable: {
+      type: Boolean,
+      default: true
+    },
+    maxCount: {
+      type: Number,
+      default: 1
+    },
+    native: {
+      // 是否原生上传
+      type: Boolean,
+      default: false
+    },
+    uploadSize: {
+      // 上传图片大小
+      type: Number,
+      default: 5
+    },
+    uploadType: {
+      type: String as PropType<'IMAGE' | 'VIDEO'>,
+      default: 'IMAGE'
+    },
+    accept: {
+      type: String,
+      default: 'image/*'
+    },
+    bucket: {
+      type: String,
+      default: 'gyt'
+    },
+    path: {
+      type: String,
+      default: ''
+    },
+    uploadIcon: {
+      type: String,
+      default: iconUploader
+    },
+    size: {
+      type: String,
+      default: 'default'
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    position: {
+      type: String as PropType<'outside' | 'inside'>,
+      default: 'outside'
+    }
+  },
+  emits: ['uploadChange', 'update:modelValue'],
+  methods: {
+    nativeUpload() {
+      if (this.disabled) {
+        return;
+      }
+      const type = this.uploadType === 'VIDEO' ? 'video' : 'img';
+      postMessage(
+        {
+          api: 'chooseFile',
+          content: { type: type, max: 1, bucket: this.bucket, path: this.path }
+        },
+        (res: any) => {
+          console.log(res, 'fileUrl');
+          // 判断是否是多选
+          if (this.maxCount > 1) {
+            this.$emit('update:modelValue', [...this.modelValue, res.fileUrl]);
+            this.$emit('uploadChange', [...this.modelValue, res.fileUrl]);
+          } else {
+            this.$emit('update:modelValue', [res.fileUrl]);
+            this.$emit('uploadChange', [res.fileUrl]);
+          }
+        }
+      );
+    },
+    beforeRead(file: any) {
+      console.log(file, 'beforeRead');
+      const isLt2M = file.size / 1024 / 1024 < this.uploadSize;
+      if (!isLt2M) {
+        showToast(`上传文件大小不能超过 ${this.uploadSize}MB`);
+        return false;
+      }
+      return true;
+    },
+    beforeDelete() {
+      // this.dataModel.splice(detail.index, 1)
+      return true;
+    },
+    async afterRead(file: any) {
+      try {
+        file.status = 'uploading';
+        file.message = '上传中...';
+        await this.uploadFile(file.file);
+      } catch (error) {
+        closeToast();
+      }
+    },
+    onClose(e: any, item: any) {
+      const models = this.modelValue;
+      const index = models.findIndex(model => model == item);
+      if (index > -1) {
+        models.splice(index, 1);
+        this.$emit('update:modelValue', models);
+        this.$emit('uploadChange');
+      }
+
+      e.stopPropagation();
+    },
+    async getFile(file: any) {
+      try {
+        await this.uploadFile(file);
+      } catch {
+        //
+      }
+    },
+    async uploadFile(file: any) {
+      // 上传文件
+      try {
+        // 获取签名
+        const signUrl = '/api-web/getUploadSign';
+        const tempName = file.name || '';
+        const fileName =
+          this.path + '/' + (tempName && tempName.replace(/ /gi, '_'));
+        const key = new Date().getTime() + fileName;
+        console.log(file);
+
+        const res = await request.post(signUrl, {
+          data: {
+            filename: fileName,
+            bucketName: this.bucket,
+            postData: {
+              filename: fileName,
+              acl: 'public-read',
+              key: key,
+              unknowValueField: []
+            }
+          }
+        });
+        showLoadingToast({
+          message: '加载中...',
+          forbidClick: true,
+          loadingType: 'spinner',
+          duration: 0
+        });
+        const obj = {
+          policy: res.data.policy,
+          signature: res.data.signature,
+          key: key,
+          KSSAccessKeyId: res.data.kssAccessKeyId,
+          acl: 'public-read',
+          name: fileName
+        } as any;
+        const formData = new FormData();
+        for (const key in obj) {
+          formData.append(key, obj[key]);
+        }
+        formData.append('file', file, fileName);
+        await umiRequest(getOssUploadUrl(this.bucket), {
+          method: 'POST',
+          data: formData
+        });
+        console.log(getOssUploadUrl(this.bucket) + key);
+        const uploadUrl = getOssUploadUrl(this.bucket) + key;
+        closeToast();
+        // 判断是否是多选
+        if (this.maxCount > 1) {
+          this.$emit('update:modelValue', [...this.modelValue, uploadUrl]);
+          this.$emit('uploadChange', [...this.modelValue, uploadUrl]);
+        } else {
+          this.$emit('update:modelValue', [uploadUrl]);
+          this.$emit('uploadChange', [uploadUrl]);
+        }
+      } catch (error) {
+        console.log(error, 'uploadFile');
+      }
+    }
+  },
+  render() {
+    useCustomFieldValue(() => this.modelValue);
+
+    return (
+      <>
+        {this.modelValue.length > 0 &&
+          this.maxCount > 1 &&
+          this.modelValue.map((item: any) => (
+            <div class={[styles.uploader, styles[this.size]]}>
+              {/* 删除按钮 */}
+              {this.deletable && !this.disabled && (
+                <Icon
+                  name="cross"
+                  onClick={(e: any) => this.onClose(e, item)}
+                  class={styles['img-close']}
+                />
+              )}
+              <div class={['van-uploader__upload']}>
+                {this.uploadType === 'IMAGE' ? (
+                  <Image src={item} class={styles.previewImg} fit="cover" />
+                ) : (
+                  <video
+                    ref="videoUpload"
+                    style={{ backgroundColor: '#F8F8F8' }}
+                    class={styles.previewImg}
+                    src={item + '#t=1,4'}
+                  />
+                )}
+              </div>
+            </div>
+          ))}
+
+        {this.native ? (
+          this.maxCount > 1 ? (
+            // 小于长度才显示
+            this.modelValue.length < this.maxCount && (
+              <div
+                class={[styles.uploader, styles[this.size]]}
+                onClick={this.nativeUpload}>
+                <Icon
+                  name={this.uploadIcon}
+                  class={['van-uploader__upload']}
+                  size="32"
+                />
+              </div>
+            )
+          ) : (
+            <div
+              class={[styles.uploader, styles[this.size]]}
+              onClick={this.nativeUpload}>
+              {this.modelValue.length > 0 ? (
+                <div class={['van-uploader__upload']}>
+                  {this.modelValue.map((item: any) => (
+                    <>
+                      {/* 删除按钮 */}
+                      {this.deletable && !this.disabled && (
+                        <Icon
+                          name="cross"
+                          onClick={(e: any) => this.onClose(e, item)}
+                          class={[styles['img-close'], styles.singleImgClose]}
+                        />
+                      )}
+                      {this.uploadType === 'IMAGE' ? (
+                        <Image
+                          fit="cover"
+                          position="center"
+                          class={styles.uploadImg}
+                          src={item}
+                        />
+                      ) : (
+                        <video
+                          ref="videoUpload"
+                          class={styles.uploadImg}
+                          style={{ backgroundColor: '#F8F8F8' }}
+                          src={item + '#t=1,4'}
+                        />
+                      )}
+                    </>
+                  ))}
+                </div>
+              ) : (
+                <Icon
+                  name={this.uploadIcon}
+                  class={['van-uploader__upload']}
+                  size="32"
+                />
+              )}
+            </div>
+          )
+        ) : this.maxCount > 1 ? (
+          // 小于长度才显示
+          this.modelValue.length < this.maxCount && (
+            <Uploader
+              class={[styles.uploader, styles[this.size]]}
+              afterRead={this.afterRead}
+              beforeRead={this.beforeRead}
+              beforeDelete={this.beforeDelete}
+              uploadIcon={this.uploadIcon}
+              maxCount={this.maxCount}
+              disabled={this.disabled}
+              accept={this.accept}
+            />
+          )
+        ) : (
+          <Uploader
+            class={[styles.uploader, styles[this.size]]}
+            afterRead={this.afterRead}
+            beforeRead={this.beforeRead}
+            beforeDelete={this.beforeDelete}
+            uploadIcon={this.uploadIcon}
+            accept={this.accept}
+            disabled={this.disabled}>
+            {this.modelValue.length > 0 ? (
+              <div class={['van-uploader__upload']}>
+                {this.modelValue.map((item: any) => (
+                  <>
+                    {/* 删除按钮 */}
+                    {this.deletable && !this.disabled && (
+                      <Icon
+                        name="cross"
+                        onClick={(e: any) => this.onClose(e, item)}
+                        class={[styles['img-close'], styles.singleImgClose]}
+                      />
+                    )}
+
+                    {this.uploadType === 'IMAGE' ? (
+                      <Image
+                        fit="cover"
+                        position="center"
+                        class={styles.uploadImg}
+                        src={item}
+                      />
+                    ) : (
+                      <video
+                        ref="videoUpload"
+                        class={styles.uploadImg}
+                        style={{ backgroundColor: '#F8F8F8' }}
+                        src={item + '#t=1,4'}
+                      />
+                    )}
+                  </>
+                ))}
+              </div>
+            ) : (
+              <Icon
+                name={this.uploadIcon}
+                class={['van-uploader__upload']}
+                size="32"
+              />
+            )}
+          </Uploader>
+        )}
+      </>
+    );
+  }
+});

+ 53 - 0
src/components/m-video/index.module.less

@@ -0,0 +1,53 @@
+.video-container {
+  position: relative;
+  width: 100%;
+  --plyr-color-main: var(--k-primary);
+
+  video {
+    width: 100%;
+    // object-fit: cover;
+  }
+
+  :global {
+    .video-back {
+      position: absolute;
+      left: 20px;
+      top: 20px;
+      color: #fff;
+      z-index: 99;
+      font-size: 24px;
+      width: 30px;
+      height: 30px;
+      background-color: rgba(0, 0, 0, 0.5);
+      border-radius: 50%;
+      padding: 4px 5px 4px 3px;
+    }
+
+    .plyr__poster {
+      background-size: cover;
+    }
+
+    .plyr__control--overlaid {
+      border: 1px solid #fff;
+      background-color: rgba(0, 0, 0, 0.2) !important;
+    }
+
+    .plyr--video .plyr__control:hover {
+      background-color: transparent !important;
+    }
+  }
+
+  .video {
+    position: relative;
+  }
+}
+
+.loadingVideo {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.9);
+  z-index: 10;
+}

+ 193 - 0
src/components/m-video/index.tsx

@@ -0,0 +1,193 @@
+import { defineComponent, PropType } from 'vue';
+import styles from './index.module.less';
+import Plyr from 'plyr';
+import 'plyr/dist/plyr.css';
+import { Loading } from 'vant';
+import { browser } from '@/helpers/utils';
+export default defineComponent({
+  name: 'm-video',
+  props: {
+    setting: {
+      type: Object,
+      default: () => ({})
+    },
+    controls: Boolean,
+    height: String,
+    src: {
+      type: String,
+      default: ''
+    },
+    poster: {
+      type: String,
+      default: ''
+    },
+    styleValue: {
+      type: Object,
+      default: () => ({})
+    },
+    preload: {
+      type: String as PropType<'auto' | 'metadata' | 'none'>,
+      default: 'auto'
+    },
+    currentTime: {
+      type: Boolean,
+      default: true
+    },
+    playsinline: {
+      type: Boolean,
+      default: true
+    }
+    // onPlay: {
+    //   type: Function,
+    //   default: () => {}
+    // }
+  },
+  emits: ['exitfullscreen', 'play'],
+  data() {
+    return {
+      player: null as any,
+      loading: true // 首次进入加载中
+    };
+  },
+  mounted() {
+    this._init();
+  },
+  methods: {
+    _init() {
+      // controls: [
+      //   'play-large' ,  // 中间的大播放按钮
+      //   'restart' ,  // 重新开始播放
+      //   'rewind' ,  // 按寻道时间倒带(默认 10 秒)
+      //   'play' ,  // 播放/暂停播放
+      //   'fast-forward' ,  // 快进查找时间(默认 10 秒)
+      //   'progress' ,  // 播放和缓冲的进度条和滑动条
+      //   'current-time' ,  // 播放的当前时间
+      //   ' duration' ,  // 媒体的完整持续时间
+      //   'mute' ,  // 切换静音
+      //   'volume', // 音量控制
+      //   'captions' ,  // 切换字幕
+      //   'settings' ,  // 设置菜单
+      //   'pip' ,  // 画中画(当前仅 Safari)
+      //   'airplay' ,  // Airplay(当前仅 Safari)
+      //   'download ' ,  // 显示一个下载按钮,其中包含指向当前源或您在选项中指定的自定义 URL 的链接
+      //   'fullscreen' ,  // 切换全屏
+      // ] ;
+      const controls = [
+        'play-large',
+        'play',
+        'progress',
+        'captions',
+        'fullscreen'
+      ];
+      if (this.currentTime) {
+        controls.push('current-time');
+      }
+      const params: any = {
+        controls: controls,
+        ...this.setting,
+        invertTime: false
+      };
+
+      if (browser().iPhone) {
+        params.fullscreen = {
+          enabled: true,
+          fallback: 'force',
+          iosNative: true
+        };
+      }
+
+      this.player = new Plyr((this as any).$refs.video, params);
+
+      // fullscreen: {
+      //     enabled: true,
+      //     fallback: 'force',
+      //     iosNative: true
+      //   }
+      this.player.elements.container
+        ? (this.player.elements.container.style.height = this.height || '210px')
+        : null;
+
+      if (this.preload === 'none') {
+        this.loading = false;
+      }
+      this.player.on('loadedmetadata', () => {
+        this.loading = false;
+        this.domPlayVisibility(false);
+      });
+
+      this.player.on('play', () => {
+        this.$emit('play', this.player);
+      });
+
+      this.player.on('enterfullscreen', () => {
+        console.log('fullscreen');
+        const i = document.createElement('i');
+        i.id = 'fullscreen-back';
+        i.className = 'van-icon van-icon-arrow-left video-back';
+        i.addEventListener('click', () => {
+          this.player.fullscreen.exit();
+        });
+        console.log(document.getElementsByClassName('plyr'));
+        document.getElementsByClassName('plyr')[0].appendChild(i);
+      });
+
+      this.player.on('exitfullscreen', () => {
+        console.log('exitfullscreen');
+        const i = document.getElementById('fullscreen-back');
+        i && i.remove();
+        this.$emit('exitfullscreen');
+      });
+    },
+    // 操作功能
+    domPlayVisibility(hide = true) {
+      const controls = document.querySelector('.plyr__controls');
+      const controls2 = document.querySelector('.plyr__control--overlaid');
+      if (hide) {
+        controls?.setAttribute('style', 'display:none');
+        controls2?.setAttribute('style', 'display:none');
+      } else {
+        controls?.removeAttribute('style');
+        setTimeout(() => {
+          controls2?.removeAttribute('style');
+        }, 200);
+      }
+    },
+    onStop() {
+      this.player.stop();
+    }
+  },
+  unmounted() {
+    this.player?.destroy();
+  },
+  render() {
+    return (
+      <div class={styles['video-container']}>
+        <video
+          ref="video"
+          class={styles['video']}
+          src={this.src}
+          playsinline={this.playsinline}
+          poster={this.poster}
+          preload={this.preload}
+          style={{ ...this.styleValue }}></video>
+        {/* </div> */}
+        {/* 加载视频使用 */}
+        {this.loading && (
+          <div
+            class={styles.loadingVideo}
+            style={{
+              height: this.height || '210px'
+            }}>
+            <Loading
+              size={36}
+              color="#FF8057"
+              vertical
+              style={{ height: '100%', justifyContent: 'center' }}>
+              加载中...
+            </Loading>
+          </div>
+        )}
+      </div>
+    );
+  }
+});

+ 57 - 0
src/helpers/constant.ts

@@ -4,3 +4,60 @@ export const EShoolStaffType = {
   SCHOOL_LEADER: '分管领导',
   TEACHER: '负责老师'
 } as any;
+
+// 老师考勤状态
+export const teacherAttendanceStatus = {
+  NORMAL: '正常',
+  ERR: '异常',
+  NO_COURSE: '无课'
+} as any;
+
+// 课程类型
+export const coursesType = {
+  // NORMAL: '声部课',
+  SINGLE: '声部课',
+  MIX: '合奏课',
+  HIGH: '基础技能课',
+  VIP: 'VIP课',
+  DEMO: '试听课',
+  COMPREHENSIVE: '综合课',
+  ENLIGHTENMENT: '启蒙课',
+  // TRAINING: '集训课',
+  TRAINING_SINGLE: '集训声部课',
+  TRAINING_MIX: '集训合奏课',
+  CLASSROOM: '课堂课',
+  PRACTICE: '网管课',
+  COMM: '对外课',
+  // MUSIC: '乐团课',
+  HIGH_ONLINE: '线上基础技能课',
+  MUSIC_NETWORK: '乐团网管课'
+  // MAINTENANCE: '乐器保养',
+  // CLOUD_TEACHER: '云教练',
+  // CLOUD_TEACHER_PLUS: '云教练+'
+} as any;
+
+// 评价类型
+export const evaluateStatus = {
+  EXCELLENT: '优秀',
+  GOOD: '良好',
+  QUALIFIED: '合格',
+  UNQUALIFIED: '不合格'
+} as any;
+
+// 问题类型
+export const problemType = {
+  CLASSROOM_DISCIPLINE: '课堂纪律',
+  AFTER_SCHOOL_ORGANIZATION: '放学组织',
+  ENVIRONMENTAL_SANITATION: '环境卫生',
+  TEACHING_DEMEANOR: '教态仪表',
+  COMMUNICATION_EXPRESSION: '沟通表达',
+  PROFESSIONAL_ABILITY: '专业能力',
+  OTHER: '其他'
+} as any;
+
+// 课程类型
+export const coursesStatus = {
+  NOT_START: '未开始',
+  UNDERWAY: '进行中',
+  OVER: '已结束'
+} as any;

+ 24 - 0
src/helpers/toolsValidate.ts

@@ -404,3 +404,27 @@ export function verifyCarNum(val: string) {
   // true:车牌号正确
   else return true;
 }
+
+/**
+ * 检测链接类型
+ * @param fileValue 文件链接
+ * @param type 类型 image | video
+ * @returns 返回 true: 满足对应类型
+ */
+export const checkFile = (fileValue: string, type: string) => {
+  const index = fileValue.indexOf('.'); //(考虑严谨用lastIndexOf(".")得到)得到"."在第几位
+  const fileValueSuffix = fileValue.substring(index); //截断"."之前的,得到后缀
+  if (type == 'video') {
+    if (!/(.*)\.(mp4|rmvb|avi|ts)$/.test(fileValueSuffix)) {
+      //根据后缀,判断是否符合视频格式
+      return false;
+    }
+  }
+  if (type == 'image') {
+    if (!/(.*)\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(fileValueSuffix)) {
+      //根据后缀,判断是否符合图片格式
+      return false;
+    }
+  }
+  return true;
+};

+ 8 - 0
src/router/routes-common.ts

@@ -51,6 +51,14 @@ export default [
         }
       },
       {
+        path: '/amap-gps',
+        name: 'amap-gps',
+        component: () => import('@/views/teacher-attendance/amap-gps'),
+        meta: {
+          title: '考勤地点'
+        }
+      },
+      {
         path: '/teacher-attendance-detail',
         name: 'teacher-attendance-detail',
         component: () => import('@/views/teacher-attendance/detail'),

+ 7 - 0
src/styles/index.less

@@ -114,4 +114,11 @@ body {
     font-weight: 400;
     width: 48%;
   }
+}
+
+
+// 地图样式
+.amap-marker-label {
+  border: 0;
+  background: transparent;
 }

BIN
src/views/download/images/bg.png


BIN
src/views/download/images/btn-bg.png


BIN
src/views/download/images/center.png


+ 21 - 0
src/views/download/index.module.less

@@ -0,0 +1,21 @@
+.download {
+  background: url('./images/bg.png') no-repeat center top #fff;
+  background-size: contain;
+  min-height: 100vh;
+  width: 100%;
+
+  .center {
+    display: block;
+    padding-top: 25px;
+    width: 237px;
+    height: 570px;
+    margin: 0 auto;
+  }
+
+  .btn {
+    display: block;
+    margin: 35px auto 20px;
+    width: 230px;
+    height: 46px;
+  }
+}

+ 9 - 1
src/views/download/index.tsx

@@ -1,8 +1,16 @@
 import { defineComponent } from 'vue';
+import styles from './index.module.less';
+import center from './images/center.png';
+import btnBg from './images/btn-bg.png';
 
 export default defineComponent({
   name: 'download-page',
   setup() {
-    return () => <>下载管乐迷学校端</>;
+    return () => (
+      <div class={styles.download}>
+        <img src={center} class={styles.center} />
+        <img src={btnBg} class={styles.btn} />
+      </div>
+    );
   }
 });

+ 2 - 2
src/views/layout/login.tsx

@@ -55,8 +55,8 @@ export default defineComponent({
         // let res: any
         const forms: any = {
           phone: this.username,
-          clientId: 'SYSTEM',
-          clientSecret: 'SYSTEM'
+          clientId: 'EDUCATION',
+          clientSecret: 'EDUCATION'
         };
 
         if (this.loginType === 'PWD') {

+ 505 - 97
src/views/patrol-evaluation/detail-list.tsx

@@ -1,110 +1,518 @@
-import { Cell, CellGroup, Icon, Image, Tag } from 'vant';
-import { defineComponent } from 'vue';
+import {
+  Button,
+  Cell,
+  CellGroup,
+  Checkbox,
+  CheckboxGroup,
+  Field,
+  Icon,
+  Image,
+  List,
+  Radio,
+  RadioGroup,
+  Tag,
+  showToast
+} from 'vant';
+import { defineComponent, onMounted, reactive, watch } from 'vue';
 import styles from './detail.module.less';
 import iconTimer from '@/common/images/icon-timer.png';
 import iconTeacher from '@/common/images/icon-teacher-default.png';
 import iconEdit from '@/common/images/icon-edit.png';
-
 import iconFace1 from './images/icon-face-1.png';
+import iconUploadImg from './images/icon-upload-img.png';
+import iconUploadVideo from './images/icon-upload-video.png';
+import SkeletionDetailModal from './skeletion-detail.modal';
+import MUploader from '@/components/m-uploader';
+import MUploaderInside from '@/components/m-uploader/inside';
+import MFullRefresh from '@/components/m-full-refresh';
+import { coursesStatus, evaluateStatus, problemType } from '@/helpers/constant';
+import MEmpty from '@/components/m-empty';
+import request from '@/helpers/request';
+import dayjs from 'dayjs';
+import { useRoute } from 'vue-router';
+import MImagePreview from '@/components/m-image-preview';
+import { checkFile } from '@/helpers/toolsValidate';
+import deepClone from '@/helpers/deep-clone';
 
 export default defineComponent({
   name: 'detail-list',
-  setup() {
+  props: {
+    type: {
+      type: String,
+      default: ''
+    },
+    evaluateStatus: {
+      type: String,
+      default: ''
+    },
+    problemType: {
+      type: String,
+      default: ''
+    },
+    courseType: {
+      type: String,
+      default: ''
+    }
+  },
+  setup(props) {
+    const route = useRoute();
+    const forms = reactive({
+      isClick: false,
+      imageShow: false,
+      startPosition: 0,
+      imagePreview: [] as string[],
+      listState: {
+        dataShow: true, // 判断是否有数据
+        loading: true,
+        finished: false,
+        refreshing: false
+      },
+      params: {
+        evaluateFlag: props.type === 'Evaluated' ? true : false,
+        evaluateStatus: '',
+        problemType: '',
+        courseType: '',
+        startTime: route.query.date || '',
+        endTime: route.query.date || '',
+        page: 1,
+        rows: 20
+      },
+      changeType: null,
+      questionType: null,
+      evaluateList: [] as any,
+      problemTypeList: [] as any,
+      list: []
+    });
+
+    const onRefresh = () => {
+      forms.params.page = 1;
+      getList();
+    };
+
+    const getList = async () => {
+      try {
+        if (forms.isClick) return;
+        forms.isClick = true;
+        const { data } = await request.post(
+          '/api-web/coursePatrolEvaluation/page',
+          {
+            data: forms.params
+          }
+        );
+        const result = data || {};
+        // 判断是否有数据
+        if (forms.listState.refreshing) {
+          forms.list = result.rows || [];
+        } else {
+          forms.list = forms.list.concat(result.rows || []);
+        }
+
+        forms.listState.finished = result.pageNo >= result.totalPage;
+        forms.params.page = result.pageNo + 1;
+      } catch {
+        forms.listState.finished = true;
+      } finally {
+        forms.listState.dataShow = forms.list.length > 0;
+        forms.listState.refreshing = false;
+        forms.listState.loading = false;
+        forms.isClick = false;
+      }
+    };
+
+    // 提交评价
+    const onSubmit = async (item: any) => {
+      try {
+        const url = [...item.submitImgList, ...item.submitVideoList];
+        //
+        if (!item.submitEvaluateStatus) {
+          showToast('请选择评价');
+          return;
+        }
+
+        // 当选择“不合格”的时候,问题类型、问题描述、上传附件为必填项
+        if (item.submitEvaluateStatus === 'UNQUALIFIED') {
+          if (!item.submitProblemType) {
+            showToast('请选择问题类型');
+            return;
+          }
+          if (!item.submitProblemDesc) {
+            showToast('请输入问题描述');
+            return;
+          }
+          // 最大支持3-50汉字
+          if (
+            item.submitProblemDesc.length < 3 ||
+            item.submitProblemDesc.length <= 50
+          )
+            if (url.length <= 0) {
+              showToast('请上传附件');
+              return;
+            }
+        }
+
+        const params = {
+          id: item.id,
+          evaluateStatus: item.submitEvaluateStatus,
+          problemType: item.submitProblemType.join(','),
+          problemDesc: item.submitProblemDesc,
+          attachmentUrl: url.join(',')
+        };
+        if (item.evaluateFlag) {
+          // 修改
+          await request.post('/api-web/coursePatrolEvaluation/update', {
+            hideLoading: false,
+            data: params
+          });
+        } else {
+          // 修改
+          await request.post('/api-web/coursePatrolEvaluation/save', {
+            hideLoading: false,
+            data: {
+              ...params,
+              courseScheduleId: item.courseScheduleId
+            }
+          });
+        }
+
+        forms.list = [];
+        onRefresh();
+      } catch {
+        //
+      }
+    };
+
+    const onShowPreView = (attachmentUrlList: string[], index: number) => {
+      forms.imagePreview = deepClone(attachmentUrlList);
+      forms.imageShow = true;
+      forms.startPosition = index;
+    };
+
+    onMounted(() => {
+      for (const key in evaluateStatus) {
+        if (Object.prototype.hasOwnProperty.call(evaluateStatus, key)) {
+          forms.evaluateList.push({
+            text: evaluateStatus[key],
+            value: key
+          });
+        }
+      }
+      for (const key in problemType) {
+        if (Object.prototype.hasOwnProperty.call(problemType, key)) {
+          forms.problemTypeList.push({
+            text: problemType[key],
+            value: key
+          });
+        }
+      }
+
+      getList();
+    });
+
+    // 监听
+    watch(
+      () => [props.evaluateStatus, props.problemType, props.courseType],
+      () => {
+        forms.params.evaluateStatus = props.evaluateStatus;
+        forms.params.problemType = props.problemType;
+        forms.params.courseType = props.courseType;
+        forms.list = [];
+        onRefresh();
+      }
+    );
     return () => (
-      <>
-        <CellGroup inset class={styles.cellGroup}>
-          <Cell center class={styles.timerCell} border={false}>
-            {{
-              icon: () => <Icon name={iconTimer} class={styles.iconTimer} />,
-              title: () => <div class={styles.timer}>2023-03-24 14:00:32</div>,
-              value: () => (
-                <div class={styles.eStatus}>
-                  <Icon name={iconFace1} class={styles.iconFace} />
-                  <span class={[styles.sLevel, styles.success]}>优秀</span>
-                  <Icon name={iconEdit} class={styles.iconEdit} />
-                </div>
-              )
-            }}
-          </Cell>
-          <Cell center class={styles.usernameCell}>
-            {{
-              icon: () => (
-                <Image
-                  src={iconTeacher}
-                  class={styles.iconTeacher}
-                  fit="contain"
+      <div>
+        <SkeletionDetailModal v-model:show={forms.listState.loading}>
+          <MFullRefresh
+            v-model:modelValue={forms.listState.refreshing}
+            onRefresh={() => onRefresh()}
+            style={{
+              minHeight: `calc(100vh - var(--header-height) - var(--van-tabs-line-height))`
+            }}>
+            <List
+              finished={forms.listState.finished}
+              finishedText=" "
+              style={{ overflow: 'hidden' }}
+              onLoad={getList}
+              offset={100}
+              immediateCheck={false}>
+              {forms.listState.dataShow ? (
+                forms.list.map((item: any) => {
+                  const attachmentUrlList = item.attachmentUrl
+                    ? item.attachmentUrl.split(',')
+                    : [];
+                  const problemTypeList = item.problemType
+                    ? item.problemType.split(',')
+                    : [];
+
+                  // 级别
+                  item.submitEvaluateStatus = item.evaluateStatus || '';
+                  // 问题类型
+                  item.submitProblemType = problemTypeList || [];
+                  item.submitProblemDesc = item.problemDesc || '';
+                  // 图片和视屏
+                  item.submitVideoList = [];
+                  item.submitImgList = [];
+                  attachmentUrlList.forEach((url: any) => {
+                    // 判断是否是图片
+                    if (checkFile(url, 'image')) {
+                      item.submitImgList.push(url);
+                    } else {
+                      item.submitVideoList.push(url);
+                    }
+                    // 判断是否是视频
+                  });
+
+                  // 判断是否评价,如果有评价则直接显示评价内容
+                  if (!item.evaluateFlag) {
+                    item.isEdit = true;
+                  }
+                  return (
+                    <CellGroup inset class={styles.cellGroup}>
+                      <Cell center class={styles.timerCell} border={false}>
+                        {{
+                          icon: () => (
+                            <Icon name={iconTimer} class={styles.iconTimer} />
+                          ),
+                          title: () => (
+                            <div class={styles.timer}>
+                              {dayjs(item.startClassTime).format(
+                                'YYYY-MM-DD HH:mm'
+                              )}
+                              ~{dayjs(item.endClassTime).format('HH:mm')}
+                            </div>
+                          ),
+                          value: () => (
+                            <div
+                              class={styles.eStatus}
+                              onClick={() => {
+                                item.isEdit = true;
+                              }}>
+                              {/* 判断是否评价 */}
+                              {item.evaluateFlag ? (
+                                <>
+                                  <Icon
+                                    name={iconFace1}
+                                    class={styles.iconFace}
+                                  />
+                                  <span class={[styles.sLevel, styles.success]}>
+                                    {evaluateStatus[item.evaluateStatus]}
+                                  </span>
+                                  <Icon
+                                    name={iconEdit}
+                                    class={styles.iconEdit}
+                                  />
+                                </>
+                              ) : (
+                                <span
+                                  class={[
+                                    styles.sLevel,
+                                    item.courseStatus === 'UNDERWAY'
+                                      ? styles.success
+                                      : ''
+                                  ]}>
+                                  {coursesStatus[item.courseStatus]}
+                                </span>
+                              )}
+                            </div>
+                          )
+                        }}
+                      </Cell>
+                      <Cell center class={styles.usernameCell}>
+                        {{
+                          icon: () => (
+                            <Image
+                              src={item.teacherAvatar || iconTeacher}
+                              class={styles.iconTeacher}
+                              fit="contain"
+                            />
+                          ),
+                          title: () => (
+                            <div>
+                              <div class={styles.classname}>
+                                {item.courseName}
+                              </div>
+                              <div class={styles.name}>{item.teacherName}</div>
+                            </div>
+                          ),
+                          value: () => (
+                            <div class={styles.photoList}>
+                              {attachmentUrlList.map(
+                                (file: string, index: number) => (
+                                  <div
+                                    class={styles.photo}
+                                    onClick={(e: MouseEvent) => {
+                                      e.stopPropagation();
+                                      e.preventDefault();
+                                      onShowPreView(attachmentUrlList, index);
+                                    }}>
+                                    {checkFile(file, 'image') ? (
+                                      <Image src={file} fit="cover" />
+                                    ) : (
+                                      <video
+                                        src={file + '#t=1,4'}
+                                        controls={false}></video>
+                                    )}
+
+                                    {/* 判断是否大于三个 */}
+                                    {attachmentUrlList.length > 3 &&
+                                    index === 2 ? (
+                                      <div class={styles.photoMore}>+8</div>
+                                    ) : (
+                                      ''
+                                    )}
+                                  </div>
+                                )
+                              )}
+                            </div>
+                          )
+                        }}
+                      </Cell>
+
+                      {/* 展示结果 */}
+                      {(problemTypeList.length > 0 || item.problemDesc) &&
+                      !item.isEdit ? (
+                        <Cell center class={styles.resultCell}>
+                          {problemTypeList.length > 0 ? (
+                            <div class={styles.typeGroup}>
+                              {problemTypeList.map((type: string) => (
+                                <Tag type="primary" plain>
+                                  {problemType[type]}
+                                </Tag>
+                              ))}
+                            </div>
+                          ) : (
+                            ''
+                          )}
+
+                          {item.problemDesc ? (
+                            <div class={styles.result}>{item.problemDesc}</div>
+                          ) : (
+                            ''
+                          )}
+                        </Cell>
+                      ) : (
+                        ''
+                      )}
+
+                      {/* 未开始的不能进行评价 */}
+                      {item.isEdit && item.courseStatus != 'NOT_START' ? (
+                        <Cell center class={styles.operationCell}>
+                          <RadioGroup
+                            class={styles.typeGroup}
+                            v-model={item.submitEvaluateStatus}>
+                            {forms.evaluateList.map((child: any) => (
+                              <Tag
+                                type={
+                                  item.submitEvaluateStatus === child.value
+                                    ? 'primary'
+                                    : 'default'
+                                }
+                                plain>
+                                <Radio name={child.value} />
+                                {child.text}
+                              </Tag>
+                            ))}
+                          </RadioGroup>
+                          {/* 当选择“不合格”的时候,问题类型、问题描述、上传附件为必填项 当选择除不合格的其他选项时,问题类型、问题描述不展示,桑川附件为选填 */}
+                          {item.submitEvaluateStatus === 'UNQUALIFIED' ? (
+                            <>
+                              <div class={styles.operationTitle}>问题类型</div>
+                              <CheckboxGroup
+                                class={styles.typeGroup}
+                                v-model={item.submitProblemType}>
+                                {forms.problemTypeList.map((child: any) => (
+                                  <Tag
+                                    type={
+                                      item.submitProblemType.includes(
+                                        child.value
+                                      )
+                                        ? 'primary'
+                                        : 'default'
+                                    }
+                                    plain>
+                                    <Checkbox name={child.value} />
+                                    {child.text}
+                                  </Tag>
+                                ))}
+                              </CheckboxGroup>
+                              <div class={styles.operationTitle}>问题描述</div>
+                              <Field
+                                type="textarea"
+                                rows={2}
+                                v-model={item.submitProblemDesc}
+                                maxlength={50}
+                                class={styles.questionContent}
+                                placeholder="请输入问题描述"
+                                border={false}
+                              />
+                            </>
+                          ) : (
+                            ''
+                          )}
+                          <div class={styles.operationTitle}>上传附件</div>
+                          <div class={styles.uploadGroup}>
+                            <MUploader
+                              uploadIcon={iconUploadImg}
+                              maxCount={5}
+                              v-model:modelValue={item.submitImgList}>
+                              <MUploaderInside
+                                uploadIcon={iconUploadVideo}
+                                uploadType="VIDEO"
+                                accept=".mp4"
+                                maxCount={3}
+                                v-model:modelValue={item.submitVideoList}
+                              />
+                            </MUploader>
+                          </div>
+
+                          <div class={styles.btnGroup}>
+                            {/* 评价之后才能取消 */}
+                            {item.evaluateFlag ? (
+                              <Button
+                                type="default"
+                                round
+                                block
+                                onClick={() => (item.isEdit = false)}>
+                                取消
+                              </Button>
+                            ) : (
+                              ''
+                            )}
+
+                            <Button
+                              type="primary"
+                              round
+                              block
+                              onClick={() => onSubmit(item)}>
+                              确认
+                            </Button>
+                          </div>
+                        </Cell>
+                      ) : (
+                        ''
+                      )}
+                    </CellGroup>
+                  );
+                })
+              ) : (
+                <MEmpty
+                  style={{
+                    minHeight: `calc(100vh - var(--header-height))`
+                  }}
+                  description="暂无数据"
                 />
-              ),
-              title: () => (
-                <div>
-                  <div class={styles.classname}>声部课·上低音号音号</div>
-                  <div class={styles.name}>王丹丹</div>
-                </div>
-              ),
-              value: () => (
-                <div class={styles.photoList}>
-                  <div class={styles.photo}>
-                    <Image
-                      src={
-                        'https://daya.ks3-cn-beijing.ksyun.com/202201/SvE7cQl.png'
-                      }
-                      fit="cover"
-                    />
-                  </div>
-                  <div class={styles.photo}>
-                    <Image
-                      src={
-                        'https://daya.ks3-cn-beijing.ksyun.com/202201/SvE7cQl.png'
-                      }
-                      fit="cover"
-                    />
-                  </div>
-                  <div
-                    class={styles.photo}
-                    // onClick={() => {
-                    //   forms.imagePreview = item.signPhoto.split(',') || [];
-                    //   forms.imageShow = true;
-                    //   forms.startPosition = index;
-                    // }}
-                  >
-                    <Image
-                      src={
-                        'https://daya.ks3-cn-beijing.ksyun.com/202201/SvE7cQl.png'
-                      }
-                      fit="cover"
-                    />
-                    <div class={styles.photoMore}>+8</div>
-                  </div>
-                </div>
-              )
-            }}
-          </Cell>
-
-          {/* <Cell center class={styles.resultCell}>
-            <div class={styles.typeGroup}>
-              <Tag type="primary" plain>
-                课堂纪律
-              </Tag>
-              <Tag type="primary" plain>
-                课堂纪律
-              </Tag>
-              <Tag type="primary" plain>
-                课堂纪律
-              </Tag>
-              <Tag type="primary" plain>
-                课堂纪律
-              </Tag>
-            </div>
-            <div class={styles.result}>
-              没有组织学员将乐器箱包放在指定地点。
-            </div>
-          </Cell> */}
-
-          <Cell center class={styles.operationCell}>
-            <div class={styles.operation}></div>
-          </Cell>
-        </CellGroup>
-      </>
+              )}
+            </List>
+          </MFullRefresh>
+        </SkeletionDetailModal>
+
+        <MImagePreview
+          teleport="body"
+          v-model:show={forms.imageShow}
+          images={forms.imagePreview}
+          startPosition={forms.startPosition}
+        />
+      </div>
     );
   }
 });

+ 90 - 2
src/views/patrol-evaluation/detail.module.less

@@ -126,13 +126,15 @@
     align-items: center;
     width: 18px;
     height: 18px;
-    margin-left: 4px;
+    font-size: 18px;
+    margin-right: 4px;
   }
 
   .iconEdit {
     display: flex;
     align-items: center;
     margin-left: 10px;
+    font-size: 16px;
     width: 16px;
     height: 16px;
   }
@@ -180,6 +182,12 @@
     margin-left: 4px;
   }
 
+  video {
+    width: inherit;
+    height: inherit;
+    background: #f8f9fc;
+  }
+
   :global {
     .van-image {
       width: inherit;
@@ -226,9 +234,11 @@
   :global {
     .van-tag {
       margin-bottom: 8px;
-      padding: 3px 15px;
+      padding: 3px 0;
+      width: 78px;
       font-size: 12px;
       border-radius: 4px;
+      justify-content: center;
 
       &+.van-tag {
         margin-left: 4px;
@@ -251,5 +261,83 @@
     .van-tag--primary {
       background: #F2FFFC;
     }
+
+    .van-radio,
+    .van-checkbox {
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 0;
+      bottom: 0;
+      width: 100%;
+      opacity: 0;
+    }
+  }
+}
+
+.operationCell {
+  :global {
+    .van-cell__value {
+      text-align: left;
+    }
+  }
+
+  .operationTitle {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: var(--k-gray-1);
+    line-height: 20px;
+    padding-top: 12px;
+    padding-bottom: 10px;
+
+    &::before {
+      margin-right: 6px;
+      content: ' ';
+      width: 4px;
+      height: 12px;
+      background: #01C1B5;
+      border-radius: 2px;
+    }
+  }
+
+  .questionContent {
+    padding: 0;
+    font-size: 12px;
+
+    input {
+      color: var(--k-gray-3) !important;
+    }
+  }
+
+  .uploadGroup {
+    display: flex;
+    align-items: center;
+    --upload-file-size: 74px !important;
+
+    // 处理显示问题
+    :global {
+      .van-uploader:nth-child(4n + 4) {
+        .van-uploader__upload {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+
+  .btnGroup {
+    margin: 12px 20px 4px;
+    display: flex;
+    align-items: center;
+
+    :global {
+      .van-button {
+        font-size: 16px;
+
+        &+.van-button {
+          margin-left: 10px;
+        }
+      }
+    }
   }
 }

+ 164 - 48
src/views/patrol-evaluation/detail.tsx

@@ -1,9 +1,10 @@
-import { defineComponent, reactive, ref } from 'vue';
+import { defineComponent, onMounted, reactive, ref } from 'vue';
 import styles from './detail.module.less';
 import MHeader from '@/components/m-header';
 import { Button, DropdownItem, DropdownMenu, Tab, Tabs } from 'vant';
 import MSticky from '@/components/m-sticky';
 import DetailList from './detail-list';
+import { coursesType, evaluateStatus, problemType } from '@/helpers/constant';
 
 export default defineComponent({
   name: 'patrol-evaluation-detail',
@@ -16,35 +17,75 @@ export default defineComponent({
         loading: true
       },
       dropDownValue: {
-        evaluation: '',
-        question: ''
+        evaluateStatus: '',
+        problemType: '',
+        evaluateStatus1: '',
+        courseType: ''
       },
-      eveluationType: [
-        { text: '全部评价', value: '' },
-        { text: '优秀', value: '1' },
-        { text: '良好', value: '2' },
-        { text: '合格', value: '3' },
-        { text: '不合格', value: '4' }
-      ],
-      questionType: [
-        { text: '全部问题', value: '' },
-        { text: '优秀', value: '1' },
-        { text: '良好', value: '2' },
-        { text: '合格', value: '3' },
-        { text: '不合格', value: '4' }
-      ]
+      dropDownValueSelect: {
+        evaluateStatus: '',
+        problemType: '',
+        evaluateStatus1: '',
+        courseType: ''
+      },
+
+      eveluationType: [{ text: '全部评价', value: '' }],
+      questionType: [{ text: '全部问题', value: '' }],
+      courseType: [{ text: '全部类型', value: '' }]
     });
 
     const onDropDownClose = (item: any) => {
       item.value && item.value.toggle();
     };
+
+    const onSubmit = () => {
+      if (forms.active === 'Evaluated') {
+        //
+        forms.dropDownValue.evaluateStatus =
+          forms.dropDownValueSelect.evaluateStatus;
+        forms.dropDownValue.problemType = forms.dropDownValueSelect.problemType;
+      } else if (forms.active === 'NotEvaluated') {
+        //
+        forms.dropDownValue.evaluateStatus1 =
+          forms.dropDownValueSelect.evaluateStatus1;
+        forms.dropDownValue.courseType = forms.dropDownValueSelect.courseType;
+      }
+
+      onDropDownClose(dropDownRef);
+    };
+
+    onMounted(() => {
+      for (const key in evaluateStatus) {
+        if (Object.prototype.hasOwnProperty.call(evaluateStatus, key)) {
+          forms.eveluationType.push({
+            text: evaluateStatus[key],
+            value: key
+          });
+        }
+      }
+      for (const key in problemType) {
+        if (Object.prototype.hasOwnProperty.call(problemType, key)) {
+          forms.questionType.push({
+            text: problemType[key],
+            value: key
+          });
+        }
+      }
+      for (const key in coursesType) {
+        if (Object.prototype.hasOwnProperty.call(coursesType, key)) {
+          forms.courseType.push({
+            text: coursesType[key],
+            value: key
+          });
+        }
+      }
+    });
     return () => (
       <div class={styles['patrol-evaluation-detail']}>
         <MSticky
           position="top"
           onBarHeight={(height: number) => {
             forms.heightV = height;
-            console.log(height, 'height');
           }}>
           <MHeader>
             {{
@@ -55,37 +96,106 @@ export default defineComponent({
                   <DropdownItem
                     title="筛选"
                     ref={dropDownRef}
-                    teleport={'body'}>
+                    teleport={'body'}
+                    onOpen={() => {
+                      forms.dropDownValueSelect = {
+                        ...forms.dropDownValue
+                      };
+                    }}>
                     <div class={styles.searchContainer}>
-                      <div class={styles.searchTitle}>评价类型</div>
-                      <div
-                        class={[styles.searchTypeGroup, styles.searchTypeFlex]}>
-                        {forms.eveluationType.map((item: any) => (
+                      {forms.active === 'Evaluated' ? (
+                        <>
+                          <div class={styles.searchTitle}>评价类型</div>
+                          <div
+                            class={[
+                              styles.searchTypeGroup,
+                              styles.searchTypeFlex
+                            ]}>
+                            {forms.eveluationType.map((item: any) => (
+                              <div
+                                class={[
+                                  styles.searchTypeItem,
+                                  forms.dropDownValueSelect.evaluateStatus ===
+                                    item.value && styles['is-active']
+                                ]}
+                                onClick={() => {
+                                  forms.dropDownValueSelect.evaluateStatus =
+                                    item.value;
+                                }}>
+                                {item.text}
+                              </div>
+                            ))}
+                          </div>
+                          <div class={styles.searchTitle}>问题类型</div>
                           <div
                             class={[
-                              styles.searchTypeItem,
-                              forms.dropDownValue.evaluation === item.value &&
-                                styles['is-active']
+                              styles.searchTypeGroup,
+                              styles.searchTypeFlex
                             ]}>
-                            {item.text}
+                            {forms.questionType.map((item: any) => (
+                              <div
+                                class={[
+                                  styles.searchTypeItem,
+                                  forms.dropDownValueSelect.problemType ===
+                                    item.value && styles['is-active']
+                                ]}
+                                onClick={() => {
+                                  forms.dropDownValueSelect.problemType =
+                                    item.value;
+                                }}>
+                                {item.text}
+                              </div>
+                            ))}
                           </div>
-                        ))}
-                      </div>
+                        </>
+                      ) : null}
 
-                      <div class={styles.searchTitle}>问题类型</div>
-                      <div
-                        class={[styles.searchTypeGroup, styles.searchTypeFlex]}>
-                        {forms.questionType.map((item: any) => (
+                      {forms.active === 'NotEvaluated' ? (
+                        <>
+                          <div class={styles.searchTitle}>评价类型</div>
                           <div
                             class={[
-                              styles.searchTypeItem,
-                              forms.dropDownValue.evaluation === item.value &&
-                                styles['is-active']
+                              styles.searchTypeGroup,
+                              styles.searchTypeFlex
                             ]}>
-                            {item.text}
+                            {forms.eveluationType.map((item: any) => (
+                              <div
+                                class={[
+                                  styles.searchTypeItem,
+                                  forms.dropDownValueSelect.evaluateStatus1 ===
+                                    item.value && styles['is-active']
+                                ]}
+                                onClick={() => {
+                                  forms.dropDownValueSelect.evaluateStatus1 =
+                                    item.value;
+                                }}>
+                                {item.text}
+                              </div>
+                            ))}
                           </div>
-                        ))}
-                      </div>
+                          <div class={styles.searchTitle}>课程类型</div>
+                          <div
+                            class={[
+                              styles.searchTypeGroup,
+                              styles.searchTypeFlex
+                            ]}>
+                            {forms.courseType.map((item: any) => (
+                              <div
+                                class={[
+                                  styles.searchTypeItem,
+                                  forms.dropDownValueSelect.courseType ===
+                                    item.value && styles['is-active']
+                                ]}
+                                onClick={() => {
+                                  forms.dropDownValueSelect.courseType =
+                                    item.value;
+                                }}>
+                                {item.text}
+                              </div>
+                            ))}
+                          </div>
+                        </>
+                      ) : null}
                     </div>
                     <div class={['btnGroupPopup', 'van-hairline--top']}>
                       <Button
@@ -93,12 +203,7 @@ export default defineComponent({
                         onClick={() => onDropDownClose(dropDownRef)}>
                         取消
                       </Button>
-                      <Button
-                        type="primary"
-                        round
-                        onClick={() => {
-                          onDropDownClose(dropDownRef);
-                        }}>
+                      <Button type="primary" round onClick={onSubmit}>
                         确定
                       </Button>
                     </div>
@@ -110,14 +215,25 @@ export default defineComponent({
         </MSticky>
 
         <Tabs
-          v-mdoel:active={forms.active}
+          v-model:active={forms.active}
           offsetTop={forms.heightV}
+          sticky
           lazyRender
           swipeable>
           <Tab name={'Evaluated'} title="已评价">
-            <DetailList />
+            <DetailList
+              type="Evaluated"
+              evaluateStatus={forms.dropDownValue.evaluateStatus}
+              problemType={forms.dropDownValue.problemType}
+            />
+          </Tab>
+          <Tab name={'NotEvaluated'} title="未评价">
+            <DetailList
+              type="NotEvaluated"
+              evaluateStatus={forms.dropDownValue.evaluateStatus1}
+              courseType={forms.dropDownValue.courseType}
+            />
           </Tab>
-          <Tab name={'NotEvaluated'} title="未评价"></Tab>
         </Tabs>
       </div>
     );

+ 2 - 0
src/views/patrol-evaluation/index.module.less

@@ -1,6 +1,8 @@
 .patrolDropDown {
   --van-dropdown-menu-background: transparent;
   display: inline-flex;
+  background-color: #f8f9fc;
+  width: 100%;
 
   :global {
     .van-dropdown-menu__title {

+ 208 - 60
src/views/patrol-evaluation/index.tsx

@@ -10,32 +10,140 @@ import {
   DropdownMenu,
   Grid,
   GridItem,
-  Icon
+  Icon,
+  List
 } from 'vant';
 import iconTimer from '@/common/images/icon-timer.png';
 import SkeletionIndexModal from './skeletion-index.modal';
 import { useRouter } from 'vue-router';
+import request from '@/helpers/request';
+import MEmpty from '@/components/m-empty';
+import MFullRefresh from '@/components/m-full-refresh';
+import dayjs from 'dayjs';
 
 export default defineComponent({
   name: 'patrol-evaluation',
   setup() {
     const router = useRouter();
     const forms = reactive({
+      isClick: false,
       listState: {
-        loading: true
+        dataShow: true, // 判断是否有数据
+        loading: true,
+        finished: false,
+        refreshing: false
       },
-      statusValue: 'week',
+      params: {
+        startTime: '',
+        endTime: '',
+        page: 1,
+        rows: 20
+      },
+      statusValue: 'term',
       statusColumns: [
         { text: '本周', value: 'week' },
         { text: '本月', value: 'month' },
         { text: '本学期', value: 'term' }
-      ]
+      ],
+      list: []
     });
 
-    onMounted(() => {
-      setTimeout(() => {
+    const getList = async () => {
+      try {
+        if (forms.isClick) return;
+        forms.isClick = true;
+        const { data } = await request.post(
+          '/api-web/coursePatrolEvaluation/pageStat',
+          {
+            data: forms.params
+          }
+        );
+        const result = data || {};
+        // 处理重复请求数据
+        if (forms.list.length > 0 && result.pageNo === 1) {
+          return;
+        }
+        // 判断是否有数据
+        if (forms.listState.refreshing) {
+          forms.list = result.rows || [];
+        } else {
+          forms.list = forms.list.concat(result.rows || []);
+        }
+
+        forms.listState.finished = result.pageNo >= result.totalPage;
+        forms.params.page = result.pageNo + 1;
+      } catch {
+        forms.listState.finished = true;
+      } finally {
+        forms.listState.dataShow = forms.list.length > 0;
+        forms.listState.refreshing = false;
         forms.listState.loading = false;
-      }, 1000);
+        forms.isClick = false;
+      }
+    };
+
+    const onRefresh = () => {
+      forms.params.page = 1;
+      getList();
+    };
+
+    const formatTimer = () => {
+      if (forms.statusValue == 'week') {
+        const startDay = dayjs()
+          .startOf('week')
+          .add(1, 'day')
+          .format('YYYY-MM-DD');
+        const endDay = dayjs().endOf('week').add(1, 'day').format('YYYY-MM-DD');
+        forms.params.startTime = startDay;
+        forms.params.endTime = endDay;
+      } else if (forms.statusValue == 'month') {
+        const startDay = dayjs().startOf('month').format('YYYY-MM-DD');
+        const endDay = dayjs().endOf('month').format('YYYY-MM-DD');
+        forms.params.startTime = startDay;
+        forms.params.endTime = endDay;
+      } else if (forms.statusValue == 'term') {
+        const year = dayjs().year();
+        const startTime = '09-01';
+        const endTime = '03-01';
+        const sysStartTime = dayjs(year + startTime).format('YYYY-MM-DD');
+        const sysEndTime = dayjs(year + endTime).format('YYYY-MM-DD');
+        const nowTime = dayjs().format('YYYY-MM-DD');
+        const before = dayjs(nowTime).isBefore(dayjs(sysStartTime));
+        const after = dayjs(nowTime).isBefore(dayjs(sysEndTime));
+
+        console.log(before, after, 'before');
+        if (before && after) {
+          forms.params.startTime = dayjs(year - 1 + startTime).format(
+            'YYYY-MM-DD'
+          );
+          forms.params.endTime = dayjs(dayjs().year() + endTime)
+            .subtract(1, 'day')
+            .format('YYYY-MM-DD');
+        }
+
+        if (!before && !after) {
+          forms.params.startTime = dayjs(dayjs().year() + startTime).format(
+            'YYYY-MM-DD'
+          );
+          forms.params.endTime = dayjs(dayjs().year() + endTime)
+            .add(1, 'year')
+            .subtract(1, 'day')
+            .format('YYYY-MM-DD');
+          // 下一年的上学期
+        }
+
+        if (before && !after) {
+          forms.params.startTime = dayjs(year + endTime).format('YYYY-MM-DD');
+          forms.params.endTime = dayjs(year + startTime)
+            .subtract(1, 'day')
+            .format('YYYY-MM-DD');
+        }
+      }
+    };
+
+    onMounted(() => {
+      formatTimer();
+      getList();
     });
     return () => (
       <div class={styles['patrol-evaluation']}>
@@ -45,63 +153,103 @@ export default defineComponent({
           <DropdownMenu class={styles.patrolDropDown}>
             <DropdownItem
               v-model={forms.statusValue}
-              options={forms.statusColumns as any}></DropdownItem>
+              options={forms.statusColumns as any}
+              onChange={() => {
+                formatTimer();
+                forms.listState.dataShow = true;
+                forms.params.page = 1;
+                getList();
+              }}></DropdownItem>
           </DropdownMenu>
         </MSticky>
 
         <SkeletionIndexModal v-model:show={forms.listState.loading}>
-          <CellGroup inset class={styles.cellGroup}>
-            <Cell center>
-              {{
-                title: () => (
-                  <div class={styles.timer}>
-                    <Icon name={iconTimer} class={styles.iconTimer} />
-                    2023-03-24
-                  </div>
-                ),
-                label: () => (
-                  <div class={styles.patrolContainer}>
-                    <Grid columnNum={2} border={false}>
-                      <GridItem>
-                        {{
-                          icon: () => (
-                            <div class={styles.num}>
-                              <span>8</span>节
-                            </div>
-                          ),
-                          text: () => (
-                            <div class="van-grid-item__text">已评价</div>
-                          )
-                        }}
-                      </GridItem>
-                      <GridItem>
-                        {{
-                          icon: () => (
-                            <div class={styles.num}>
-                              <span>8</span>节
-                            </div>
-                          ),
-                          text: () => (
-                            <div class="van-grid-item__text">全部课程</div>
-                          )
-                        }}
-                      </GridItem>
-                    </Grid>
-                    <Button
-                      type="primary"
-                      class={styles.btn}
-                      size="small"
-                      round
-                      onClick={() => {
-                        router.push('/patrol-evaluation-detail');
-                      }}>
-                      查看详情
-                    </Button>
-                  </div>
-                )
-              }}
-            </Cell>
-          </CellGroup>
+          <MFullRefresh
+            v-model:modelValue={forms.listState.refreshing}
+            onRefresh={() => onRefresh()}
+            style={{
+              minHeight: `calc(100vh - var(--header-height))`
+            }}>
+            <List
+              finished={forms.listState.finished}
+              finishedText=" "
+              style={{ overflow: 'hidden' }}
+              onLoad={getList}
+              immediateCheck={false}>
+              {forms.listState.dataShow ? (
+                forms.list.map((item: any) => (
+                  <CellGroup inset class={styles.cellGroup}>
+                    <Cell center>
+                      {{
+                        title: () => (
+                          <div class={styles.timer}>
+                            <Icon name={iconTimer} class={styles.iconTimer} />
+                            {item.date}
+                          </div>
+                        ),
+                        label: () => (
+                          <div class={styles.patrolContainer}>
+                            <Grid columnNum={2} border={false}>
+                              <GridItem>
+                                {{
+                                  icon: () => (
+                                    <div class={styles.num}>
+                                      <span>{item.patrolCount}</span>节
+                                    </div>
+                                  ),
+                                  text: () => (
+                                    <div class="van-grid-item__text">
+                                      已评价
+                                    </div>
+                                  )
+                                }}
+                              </GridItem>
+                              <GridItem>
+                                {{
+                                  icon: () => (
+                                    <div class={styles.num}>
+                                      <span>{item.totalCount}</span>节
+                                    </div>
+                                  ),
+                                  text: () => (
+                                    <div class="van-grid-item__text">
+                                      全部课程
+                                    </div>
+                                  )
+                                }}
+                              </GridItem>
+                            </Grid>
+                            <Button
+                              type="primary"
+                              class={styles.btn}
+                              size="small"
+                              round
+                              onClick={() => {
+                                router.push({
+                                  path: '/patrol-evaluation-detail',
+                                  query: {
+                                    date: item.date
+                                  }
+                                });
+                              }}>
+                              查看详情
+                            </Button>
+                          </div>
+                        )
+                      }}
+                    </Cell>
+                  </CellGroup>
+                ))
+              ) : (
+                <MEmpty
+                  style={{
+                    minHeight: `calc(100vh - var(--header-height))`
+                  }}
+                  description="暂无数据"
+                />
+              )}
+            </List>
+          </MFullRefresh>
         </SkeletionIndexModal>
       </div>
     );

+ 38 - 128
src/views/patrol-evaluation/skeletion-detail.modal.tsx

@@ -1,15 +1,13 @@
 import {
   Cell,
   CellGroup,
-  Grid,
-  GridItem,
   Skeleton,
   SkeletonAvatar,
   SkeletonImage,
   SkeletonParagraph
 } from 'vant';
 import { defineComponent, onMounted, reactive, watch } from 'vue';
-import styles from './index.module.less';
+import styles from './detail.module.less';
 
 export default defineComponent({
   name: 'skeleton-modal',
@@ -48,138 +46,50 @@ export default defineComponent({
           template: () => (
             <div
               style={{
-                height: props.isLink
-                  ? `calc(100vh - var(--header-height))`
-                  : 'auto',
+                height: `calc(100vh - var(--header-height) - var(--van-tabs-line-height))`,
                 overflow: 'hidden',
                 width: '100%'
               }}>
               {props.showCount.map(() => (
-                // <CellGroup inset class={styles.cellGroup}>
-                //   <Cell center>
-                //     {{
-                //       title: () => (
-                //         <div class={styles.timer}>
-                //           <SkeletonAvatar class={styles.iconTimer} />
-                //           <SkeletonParagraph
-                //             rowWidth={'40%'}
-                //             style={{ marginTop: '0' }}
-                //           />
-                //         </div>
-                //       ),
-                //       label: () => (
-                //         <div class={styles.patrolContainer}>
-                //           <Grid columnNum={2} border={false}>
-                //             <GridItem>
-                //               {{
-                //                 icon: () => (
-                //                   <SkeletonParagraph
-                //                     rowWidth={'40%'}
-                //                     style={{ width: '50px' }}
-                //                     class={styles.num}
-                //                   />
-                //                 ),
-                //                 text: () => (
-                //                   <div class="van-grid-item__text">
-                //                     <SkeletonParagraph
-                //                       rowWidth={'40%'}
-                //                       style={{ width: '50px' }}
-                //                       class={styles.num}
-                //                     />
-                //                   </div>
-                //                 )
-                //               }}
-                //             </GridItem>
-                //             <GridItem>
-                //               {{
-                //                 icon: () => (
-                //                   <SkeletonParagraph
-                //                     rowWidth={'40%'}
-                //                     style={{ width: '50px' }}
-                //                     class={styles.num}
-                //                   />
-                //                 ),
-                //                 text: () => (
-                //                   <div class="van-grid-item__text">
-                //                     <SkeletonParagraph
-                //                       rowWidth={'40%'}
-                //                       style={{ width: '50px' }}
-                //                       class={styles.num}
-                //                     />
-                //                   </div>
-                //                 )
-                //               }}
-                //             </GridItem>
-                //           </Grid>
-
-                //           <SkeletonParagraph
-                //             class={styles.btn}
-                //             rowWidth={'20%'}
-                //           />
-                //         </div>
-                //       )
-                //     }}
-                //   </Cell>
-                // </CellGroup>
-                <div>
-                  <CellGroup inset class={styles.cellGroup}>
-                    <Cell center class={styles.timerCell} border={false}>
-                      {{
-                        icon: () => <SkeletonAvatar class={styles.iconTimer} />,
-                        title: () => (
-                          <div class={styles.timer}>2023-03-24 14:00:32</div>
-                        ),
-                        value: () => (
-                          <div class={styles.eStatus}>
+                <CellGroup inset class={styles.cellGroup}>
+                  <Cell center class={styles.timerCell} border={false}>
+                    {{
+                      icon: () => <SkeletonAvatar class={styles.iconTimer} />,
+                      title: () => (
+                        <div class={styles.timer} style={{ width: '120px' }}>
+                          <SkeletonParagraph />
+                        </div>
+                      ),
+                      value: () => (
+                        <div class={styles.eStatus}>
+                          <SkeletonParagraph rowWidth={'50%'} />
+                        </div>
+                      )
+                    }}
+                  </Cell>
+                  <Cell center class={styles.usernameCell}>
+                    {{
+                      icon: () => <SkeletonImage class={styles.iconTeacher} />,
+                      title: () => (
+                        <div>
+                          <div class={styles.classname}>
                             <SkeletonParagraph rowWidth={'50%'} />
                           </div>
-                        )
-                      }}
-                    </Cell>
-                    <Cell center class={styles.usernameCell}>
-                      {{
-                        icon: () => (
-                          <SkeletonImage class={styles.iconTeacher} />
-                        ),
-                        title: () => (
-                          <div>
-                            <div class={styles.classname}>
-                              声部课·上低音号音号
-                            </div>
-                            <div class={styles.name}>王丹丹</div>
-                          </div>
-                        ),
-                        value: () => (
-                          <div class={styles.photoList}>
-                            <SkeletonImage class={styles.photo} />
-                            <SkeletonImage class={styles.photo} />
-                            <SkeletonImage class={styles.photo} />
+                          <div class={styles.name} style={{ marginTop: '4px' }}>
+                            <SkeletonParagraph rowWidth={'50%'} />
                           </div>
-                        )
-                      }}
-                    </Cell>
-
-                    {/* <Cell center class={styles.resultCell}>
-                      <div class={styles.typeGroup}>
-                        <Tag type="primary" plain>
-                          课堂纪律
-                        </Tag>
-                        <Tag type="primary" plain>
-                          课堂纪律
-                        </Tag>
-                        <Tag type="primary" plain>
-                          课堂纪律
-                        </Tag>
-                        <Tag type="primary" plain>
-                          课堂纪律
-                        </Tag>
-                      </div>
-                      <div class={styles.result}>
-                        没有组织学员将乐器箱包放在指定地点。
-                      </div>
-                    </Cell> */}
-                  </CellGroup>
-                </div>
+                        </div>
+                      ),
+                      value: () => (
+                        <div class={styles.photoList}>
+                          <SkeletonImage class={styles.photo} />
+                          <SkeletonImage class={styles.photo} />
+                          <SkeletonImage class={styles.photo} />
+                        </div>
+                      )
+                    }}
+                  </Cell>
+                </CellGroup>
               ))}
             </div>
           ),

+ 0 - 1
src/views/site-management/drop-down-modal.tsx

@@ -57,7 +57,6 @@ export default defineComponent({
             type="primary"
             round
             onClick={() => {
-              console.log(forms.values);
               emit('dropDownConfirm', forms.values);
             }}>
             确定

+ 152 - 108
src/views/site-management/index.tsx

@@ -45,7 +45,7 @@ export default defineComponent({
       params: {
         startTime: null,
         endTime: null,
-        musicGroupId: null,
+        musicGroupId: '',
         page: 1,
         rows: 20
       },
@@ -72,7 +72,7 @@ export default defineComponent({
           value: [startDay.format('YYYY-MM-DD'), endDay.format('YYYY-MM-DD')]
         });
       }
-      console.log(tempTimer, 'tempTimer');
+      // console.log(tempTimer, 'tempTimer');
       forms.timeColumns = tempTimer;
       forms.titleTimeValue = tempTimer[0].value;
     };
@@ -108,10 +108,7 @@ export default defineComponent({
           }
         );
         const result = data || {};
-        // 处理重复请求数据
-        if (forms.list.length > 0 && result.pageNo === 1) {
-          return;
-        }
+
         // 判断是否有数据
         if (forms.listState.refreshing) {
           forms.list = result.rows || [];
@@ -119,7 +116,7 @@ export default defineComponent({
           forms.list = forms.list.concat(result.rows || []);
         }
 
-        forms.listState.finished = result.pageNo >= result.pages;
+        forms.listState.finished = result.pageNo >= result.totalPage;
         forms.params.page = result.pageNo + 1;
       } catch {
         forms.listState.finished = true;
@@ -143,12 +140,30 @@ export default defineComponent({
       }
     };
 
+    // 乐团列表
+    const musicGroupPage = async () => {
+      try {
+        const { data } = await request.get(
+          '/api-web/cooperationOrgan/musicGroupPage'
+        );
+        (data || []).forEach((item: any) => {
+          forms.orchestraColumns.push({
+            text: item.name,
+            value: item.id
+          });
+        });
+      } catch {
+        //
+      }
+    };
+
     const onRefresh = () => {
       forms.params.page = 1;
       getList();
     };
 
     onMounted(async () => {
+      musicGroupPage();
       getInitTimer();
       await getList();
       teachingPointCourse();
@@ -175,7 +190,12 @@ export default defineComponent({
             <DropdownItem
               ref={dropDownItemRef}
               v-model={forms.titleTimeValue}
-              options={forms.timeColumns as any}></DropdownItem>
+              options={forms.timeColumns as any}
+              onChange={() => {
+                forms.list = [];
+                forms.listState.dataShow = true;
+                onRefresh();
+              }}></DropdownItem>
             <DropdownItem
               ref={dropDownItemRef1}
               title={formatName('orchestra')}>
@@ -186,8 +206,11 @@ export default defineComponent({
                 onDropDownClose={() => onDropDownClose(dropDownItemRef1)}
                 onDropDownConfirm={(values: any) => {
                   forms.titleOrchestraValue = values[0];
-                  console.log(values, 'orchestra');
                   onDropDownClose(dropDownItemRef1);
+                  forms.params.musicGroupId = forms.titleOrchestraValue || '';
+                  forms.list = [];
+                  forms.listState.dataShow = true;
+                  onRefresh();
                 }}
               />
             </DropdownItem>
@@ -208,108 +231,129 @@ export default defineComponent({
               onLoad={getList}
               immediateCheck={false}>
               {forms.listState.dataShow ? (
-                forms.list.map((item: any) => (
-                  <div class={styles.siteItem}>
-                    <CellGroup class={styles.cellGroup} border={false}>
-                      <Cell border={false} center>
-                        {{
-                          title: () => (
-                            <div class={styles.orchestraName}>
-                              <img src={iconMusic} class={styles.iconMusic} />
-                              <p class={styles.overhide}>
-                                {item.musicGroupName}
+                forms.list.map((item: any) => {
+                  const signPhotoList = item.signPhoto
+                    ? item.signPhoto.split(',')
+                    : [];
+                  const signOutPhotoList = item.signOutPhoto
+                    ? item.signOutPhoto.split(',')
+                    : [];
+
+                  return (
+                    <div class={styles.siteItem}>
+                      <CellGroup class={styles.cellGroup} border={false}>
+                        <Cell border={false} center>
+                          {{
+                            title: () => (
+                              <div class={styles.orchestraName}>
+                                <img src={iconMusic} class={styles.iconMusic} />
+                                <p class={styles.overhide}>
+                                  {item.musicGroupName}
+                                </p>
+                              </div>
+                            ),
+                            default: () => (
+                              <p class={[styles.address, styles.overhide]}>
+                                {item.teachingPoint}
                               </p>
-                            </div>
-                          ),
-                          default: () => (
-                            <p class={[styles.address, styles.overhide]}>
-                              1年级1班1年级1班1年级1班
-                            </p>
-                          )
-                        }}
-                      </Cell>
-                      <Cell center class={styles.username}>
-                        {{
-                          icon: () => (
-                            <Image
-                              src={iconTeacher}
-                              class={styles.iconTeacher}
-                              fit="contain"
-                            />
-                          ),
-                          title: () => (
-                            <div>
-                              <div class={styles.classname}>
-                                {item.courseName}
+                            )
+                          }}
+                        </Cell>
+                        <Cell center class={styles.username}>
+                          {{
+                            icon: () => (
+                              <Image
+                                src={item.teacherAvatar || iconTeacher}
+                                class={styles.iconTeacher}
+                                fit="contain"
+                              />
+                            ),
+                            title: () => (
+                              <div>
+                                <div class={styles.classname}>
+                                  {item.courseName}
+                                </div>
+                                <div class={styles.name}>
+                                  {item.teacherName}
+                                </div>
                               </div>
-                              <div class={styles.name}>{item.teacherName}</div>
-                            </div>
-                          )
-                        }}
-                      </Cell>
-                    </CellGroup>
-                    <CellGroup class={styles.cellGroup}>
-                      <div class={[styles.photoGroup]}>
-                        <div class={styles.photoUp}>
-                          <h3>
-                            <span class={styles.photoTitle}>课前照片</span>
-                          </h3>
-                          {item.signPhoto ? (
-                            <div class={styles.photoList}>
-                              {(item.signPhoto.split(',') || []).map(
-                                (img: string, index: number) => (
-                                  <div
-                                    class={styles.photo}
-                                    onClick={() => {
-                                      forms.imagePreview =
-                                        item.signPhoto.split(',') || [];
-                                      forms.imageShow = true;
-                                      forms.startPosition = index;
-                                    }}>
-                                    <Image src={img} />
-                                  </div>
-                                )
-                              )}
-                            </div>
-                          ) : (
-                            <div class={styles.photoEmpty}>
-                              <img src={iconEmpty} class={styles.iconEmpty} />
-                              <p>老师未上传照片~</p>
-                            </div>
-                          )}
-                        </div>
-                        <div class={styles.photoDown}>
-                          <h3>
-                            <span class={styles.photoTitle}>课后照片</span>
-                          </h3>
-                          {item.signOutPhoto ? (
-                            <div class={styles.photoList}>
-                              {(item.signOutPhoto.split(',') || []).map(
-                                (img: string, index: number) => (
-                                  <div
-                                    class={styles.photo}
-                                    onClick={() => {
-                                      forms.imagePreview =
-                                        item.signOutPhoto.split(',') || [];
-                                      forms.imageShow = true;
-                                      forms.startPosition = index;
-                                    }}>
-                                    <Image src={img} />
-                                  </div>
-                                )
-                              )}
-                            </div>
-                          ) : (
-                            <div class={styles.photoEmpty}>
-                              <img src={iconEmpty} class={styles.iconEmpty} />
-                              <p>老师未上传照片~</p>
-                            </div>
-                          )}
+                            )
+                          }}
+                        </Cell>
+                      </CellGroup>
+                      <CellGroup class={styles.cellGroup}>
+                        <div class={[styles.photoGroup]}>
+                          <div class={styles.photoUp}>
+                            <h3>
+                              <span class={styles.photoTitle}>课前照片</span>
+                            </h3>
+                            {item.signPhoto ? (
+                              <div class={styles.photoList}>
+                                {signPhotoList.map(
+                                  (img: string, index: number) => (
+                                    <div
+                                      class={styles.photo}
+                                      onClick={() => {
+                                        forms.imagePreview = signPhotoList;
+                                        forms.imageShow = true;
+                                        forms.startPosition = index;
+                                      }}>
+                                      <Image src={img} />
+                                      {signPhotoList.length > 3 &&
+                                      index === 2 ? (
+                                        <div class={styles.photoMore}>+8</div>
+                                      ) : (
+                                        ''
+                                      )}
+                                    </div>
+                                  )
+                                )}
+                              </div>
+                            ) : (
+                              <div class={styles.photoEmpty}>
+                                <img src={iconEmpty} class={styles.iconEmpty} />
+                                <p>老师未上传照片~</p>
+                              </div>
+                            )}
+                          </div>
+                          <div class={styles.photoDown}>
+                            <h3>
+                              <span class={styles.photoTitle}>课后照片</span>
+                            </h3>
+                            {item.signOutPhoto ? (
+                              <div class={styles.photoList}>
+                                {signOutPhotoList.map(
+                                  (img: string, index: number) => (
+                                    <div
+                                      class={styles.photo}
+                                      onClick={() => {
+                                        forms.imagePreview = signOutPhotoList;
+                                        forms.imageShow = true;
+                                        forms.startPosition = index;
+                                      }}>
+                                      <Image src={img} />
+                                      {signOutPhotoList.length > 3 &&
+                                      index === 2 ? (
+                                        <div class={styles.photoMore}>+8</div>
+                                      ) : (
+                                        ''
+                                      )}
+                                    </div>
+                                  )
+                                )}
+                              </div>
+                            ) : (
+                              <div class={styles.photoEmpty}>
+                                <img src={iconEmpty} class={styles.iconEmpty} />
+                                <p>老师未上传照片~</p>
+                              </div>
+                            )}
+                          </div>
                         </div>
-                      </div>
-                    </CellGroup>
-                  </div>
-                ))
+                      </CellGroup>
+                    </div>
+                  );
+                })
               ) : (
                 <MEmpty
                   style={{

+ 1 - 0
src/views/site-management/site-settings.module.less

@@ -1,5 +1,6 @@
 .searchGroup {
   padding: 10px 13px;
+  background: #f8f9fc;
 }
 
 .searchItem {

+ 115 - 53
src/views/site-management/site-settings.tsx

@@ -11,7 +11,8 @@ import {
   Image,
   List,
   Picker,
-  Popup
+  Popup,
+  showToast
 } from 'vant';
 import iconMusic from '@/common/images/icon-music.png';
 import iconTeacher from '@/common/images/icon-teacher-default.png';
@@ -28,6 +29,8 @@ export default defineComponent({
       showPopup: false,
       showOrchestraPopup: false,
       siteName: '',
+      orchestraName: '全部乐团',
+      orchestraColumns: [{ text: '全部乐团', value: '' }], //
       listState: {
         dataShow: true, // 判断是否有数据
         loading: true,
@@ -40,36 +43,51 @@ export default defineComponent({
         rows: 20
       },
       isClick: false,
-      list: []
+      list: [],
+      selectSite: {} as any //
     });
 
-    const onEdit = () => {
+    const onEdit = (item: any) => {
+      forms.selectSite = item;
+      forms.siteName = item.teachingPoint;
       forms.showPopup = true;
     };
 
+    const onSubmitUpdateSite = async () => {
+      try {
+        await request.post('/api-web/classGroup/teachingPoint', {
+          hideLoading: false,
+          data: {
+            classGroupId: forms.selectSite.classGroupId,
+            teachingPoint: forms.siteName
+          }
+        });
+        forms.showPopup = false;
+        forms.list = [];
+        onRefresh();
+      } catch {
+        //
+      }
+    };
+
     const getList = async () => {
       try {
         if (forms.isClick) return;
         forms.isClick = true;
         const { data } = await request.post(
-          '/api-web/classGroup/teachingPointCourse',
+          '/api-web/classGroup/teachingPointClass',
           {
             data: forms.params
           }
         );
         const result = data || {};
-        // 处理重复请求数据
-        if (forms.list.length > 0 && result.pageNo === 1) {
-          return;
-        }
         // 判断是否有数据
         if (forms.listState.refreshing) {
           forms.list = result.rows || [];
         } else {
           forms.list = forms.list.concat(result.rows || []);
         }
-
-        forms.listState.finished = result.pageNo >= result.pages;
+        forms.listState.finished = result.pageNo >= result.totalPage;
         forms.params.page = result.pageNo + 1;
       } catch {
         forms.listState.finished = true;
@@ -86,7 +104,25 @@ export default defineComponent({
       getList();
     };
 
+    // 乐团列表
+    const musicGroupPage = async () => {
+      try {
+        const { data } = await request.get(
+          '/api-web/cooperationOrgan/musicGroupPage'
+        );
+        (data || []).forEach((item: any) => {
+          forms.orchestraColumns.push({
+            text: item.name,
+            value: item.id
+          });
+        });
+      } catch {
+        //
+      }
+    };
+
     onMounted(() => {
+      musicGroupPage();
       getList();
     });
     return () => (
@@ -98,7 +134,7 @@ export default defineComponent({
             <div
               class={styles.searchItem}
               onClick={() => (forms.showOrchestraPopup = true)}>
-              <span>全部乐团</span>
+              <span>{forms.orchestraName}</span>
             </div>
           </div>
         </MSticky>
@@ -117,43 +153,53 @@ export default defineComponent({
               onLoad={getList}
               immediateCheck={false}>
               {forms.listState.dataShow ? (
-                <CellGroup class={styles.cellGroup} border={false}>
-                  <Cell border={false} center>
-                    {{
-                      title: () => (
-                        <div class={styles.orchestraName}>
-                          <img src={iconMusic} class={styles.iconMusic} />
-                          <p class={styles.overhide}>
-                            武汉小学乐团武汉小学乐团武汉小学乐团武汉小学乐团
-                          </p>
-                        </div>
-                      ),
-                      default: () => (
-                        <div class={styles.address} onClick={() => onEdit()}>
-                          <p class={[styles.overhide]}>1年级1班</p>
-                          <Icon name={iconEdit} class={styles.iconEdit} />
-                        </div>
-                      )
-                    }}
-                  </Cell>
-                  <Cell center class={styles.username}>
-                    {{
-                      icon: () => (
-                        <Image
-                          src={iconTeacher}
-                          class={styles.iconTeacher}
-                          fit="contain"
-                        />
-                      ),
-                      title: () => (
-                        <div>
-                          <div class={styles.classname}>声部课·上低音号</div>
-                          <div class={styles.name}>王丹丹</div>
-                        </div>
-                      )
-                    }}
-                  </Cell>
-                </CellGroup>
+                forms.list.map((item: any) => (
+                  <CellGroup class={styles.cellGroup} border={false}>
+                    <Cell border={false} center>
+                      {{
+                        title: () => (
+                          <div class={styles.orchestraName}>
+                            <img src={iconMusic} class={styles.iconMusic} />
+                            <p class={styles.overhide}>{item.musicGroupName}</p>
+                          </div>
+                        ),
+                        default: () => (
+                          <div
+                            class={styles.address}
+                            onClick={() => onEdit(item)}>
+                            <p
+                              class={[
+                                styles.overhide,
+                                item.teachingPoint ? '' : styles.red
+                              ]}>
+                              {item.teachingPoint || '未设置场地'}
+                            </p>
+                            <Icon name={iconEdit} class={styles.iconEdit} />
+                          </div>
+                        )
+                      }}
+                    </Cell>
+                    <Cell center class={styles.username}>
+                      {{
+                        icon: () => (
+                          <Image
+                            src={item.teacherAvatar || iconTeacher}
+                            class={styles.iconTeacher}
+                            fit="contain"
+                          />
+                        ),
+                        title: () => (
+                          <div>
+                            <div class={styles.classname}>
+                              {item.courseName}
+                            </div>
+                            <div class={styles.name}>{item.teacherName}</div>
+                          </div>
+                        )
+                      }}
+                    </Cell>
+                  </CellGroup>
+                ))
               ) : (
                 <MEmpty
                   style={{
@@ -166,7 +212,13 @@ export default defineComponent({
           </MFullRefresh>
         </SkeletonSettingsModal>
 
-        <Popup v-model:show={forms.showPopup} round>
+        <Popup
+          v-model:show={forms.showPopup}
+          round
+          onClose={() => {
+            forms.siteName = '';
+            forms.selectSite = {};
+          }}>
           <div class={styles.popupContainer}>
             <h2>设置场地</h2>
             <Field
@@ -177,10 +229,14 @@ export default defineComponent({
               maxlength={15}
             />
             <div class={['btnGroupPopup']}>
-              <Button round onClick={() => (forms.showPopup = false)}>
+              <Button
+                round
+                onClick={() => {
+                  forms.showPopup = false;
+                }}>
                 取消
               </Button>
-              <Button type="primary" round>
+              <Button type="primary" round onClick={onSubmitUpdateSite}>
                 确定
               </Button>
             </div>
@@ -189,10 +245,16 @@ export default defineComponent({
 
         <Popup v-model:show={forms.showOrchestraPopup} round position="bottom">
           <Picker
-            columns={[]}
+            columns={forms.orchestraColumns}
             onCancel={() => (forms.showOrchestraPopup = false)}
-            onConfirm={() => {
+            onConfirm={({ selectedOptions }: any) => {
               forms.showOrchestraPopup = false;
+              const option = selectedOptions[0];
+              forms.orchestraName = option.text;
+              forms.params.musicGroupId = option.value;
+              forms.list = [];
+              forms.listState.dataShow = true;
+              onRefresh();
             }}
           />
         </Popup>

+ 10 - 0
src/views/teacher-attendance/amap-gps.module.less

@@ -0,0 +1,10 @@
+.info {
+  padding: 0;
+  background: #00B2A7;
+  border: 0;
+  color: #fff;
+  line-height: 18px;
+  font-size: 12px;
+  padding: 2px 4px;
+  border-radius: 4px;
+}

+ 261 - 0
src/views/teacher-attendance/amap-gps.tsx

@@ -0,0 +1,261 @@
+import { defineComponent, onMounted, reactive, ref, shallowRef } from 'vue';
+import styles from './amap-gps.module.less';
+import AMapLoader from '@amap/amap-jsapi-loader';
+import MHeader from '@/components/m-header';
+import MSticky from '@/components/m-sticky';
+export default defineComponent({
+  name: 'amap-gps',
+  setup() {
+    // 选中地图详情地址
+    const selectMapAddress = reactive({
+      attendance_range: 1000,
+      lnglat: [114.343011, 30.55499] as any, // 教学点 A座
+      address: '',
+      signInLongitudeLatitude: [114.341116, 30.552401], // 签到  B座
+      signOutLongitudeLatitude: [114.34424, 30.556584], // 签退
+      signInMark: null,
+      signOutMark: null,
+      addressMark: null
+    });
+
+    const map = shallowRef<any>(null);
+    const ininMap = () => {
+      AMapLoader.load({
+        key: '501e5bfaf7cfc5d3d53a9a4d9466d8b9', //api服务key--另外需要在public中使用安全密钥!!!
+        version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
+        plugins: [
+          // 'AMap.AutoComplete', // 输入提示插件
+          // 'AMap.PlaceSearch',
+          'AMap.ToolBar', // 缩放工具条
+          'AMap.Driving'
+        ], // 需要使用的的插件列表
+        AMapUI: {
+          version: '1.1',
+          plugins: []
+        },
+        Loca: {
+          version: '2.0.0'
+        }
+      })
+        .then(AMap => {
+          let marker = null as any;
+          map.value = new AMap.Map('gpsContainer', {
+            resizeEnable: true,
+            zoom: 16,
+            zooms: [2, 22],
+            center: [114.34371, 30.55939]
+          });
+          AMap.plugin(
+            [
+              // 'AMap.AutoComplete',
+              // 'AMap.PlaceSearch',
+              'AMap.Geocoder',
+              'AMap.GeometryUtil'
+            ],
+            function () {
+              // 判断是否有数据 加载教学点
+              if (
+                selectMapAddress.lnglat &&
+                selectMapAddress.lnglat.length > 0
+              ) {
+                map.value.setCenter(selectMapAddress.lnglat, true);
+                clearMarker();
+                addMarker(selectMapAddress.lnglat, '教学点', 'addressMark');
+
+                const circle = new AMap.Circle({
+                  center: new AMap.LngLat(...selectMapAddress.lnglat), // 圆心位置
+                  radius: selectMapAddress.attendance_range, //半径
+                  strokeColor: '#00B2A7', //线颜色
+                  strokeOpacity: 1, //线透明度
+                  strokeWeight: 2, //线粗细度
+                  fillColor: '#A3FFF9', //填充颜色
+                  fillOpacity: 0.14 //填充透明度
+                });
+                map.value.add(circle);
+                map.value.setFitView();
+              }
+
+              // 加载签到点
+              // if (
+              //   selectMapAddress.signInLongitudeLatitude &&
+              //   selectMapAddress.signInLongitudeLatitude.length > 0
+              // ) {
+              //   console.log(
+              //     'signInLongitudeLatitude',
+              //     selectMapAddress.signInLongitudeLatitude
+              //   );
+              //   addMarker(
+              //     selectMapAddress.signInLongitudeLatitude,
+              //     '签到点',
+              //     'signInMark'
+              //   );
+              // }
+              // 加载签退点
+              if (
+                selectMapAddress.signOutLongitudeLatitude &&
+                selectMapAddress.signOutLongitudeLatitude.length > 0
+              ) {
+                console.log(
+                  'signOutLongitudeLatitude',
+                  selectMapAddress.signOutLongitudeLatitude
+                );
+                addMarker(
+                  selectMapAddress.signOutLongitudeLatitude,
+                  '签退点',
+                  'signOutMark'
+                );
+              }
+              console.log(selectMapAddress, 'selectMapAddress');
+              if (selectMapAddress.signInMark && selectMapAddress.addressMark) {
+                // 连线 设置签到点与教学点的距离
+                computeDis(
+                  selectMapAddress.addressMark,
+                  selectMapAddress.signInMark,
+                  'top'
+                );
+                console.log('连线 设置签到点与教学点的距离');
+              }
+              if (
+                selectMapAddress.signOutMark &&
+                selectMapAddress.addressMark
+              ) {
+                // 连线 设置签退点与教学点的距离
+                computeDis(
+                  selectMapAddress.addressMark,
+                  selectMapAddress.signOutMark,
+                  'bottom'
+                );
+                console.log('连线 设置签退点与教学点的距离');
+              }
+            }
+          );
+
+          // 添加标记点
+          function addMarker(
+            lnglat: any,
+            title: string,
+            type: 'signInMark' | 'signOutMark' | 'addressMark'
+          ) {
+            const Icon = new AMap.Icon({
+              size: [25, 34],
+              image: 'https://daya.ks3-cn-beijing.ksyun.com/202211/TO13VtW.png',
+              imageSize: [25, 34]
+            });
+            // const Icon = new AMap.Icon({
+            //   size: new AMap.Size([25, 34]), // 图标尺寸
+            //   image: 'https://daya.ks3-cn-beijing.ksyun.com/202211/TO13VtW.png', // Icon的图像
+            //   // imageOffset: new AMap.Pixel(0, -60), // 图像相对展示区域的偏移量,适于雪碧图等
+            //   imageSize: new AMap.Size([25, 34]) // 根据所设置的大小拉伸或压缩图片
+            // });
+            marker = new AMap.Marker({
+              // icon: Icon,
+              // offset: new AMap.Pixel(-25, -14),
+              position: new AMap.LngLat(...lnglat),
+              title
+            });
+
+            marker.setLabel({
+              content: `<div class='${styles.info}' >${title}</div>`, //设置文本标注内容
+              direction: 'top'
+            });
+
+            selectMapAddress[type] = marker;
+            marker.setMap(map);
+            map.value.add(marker);
+          }
+          // 清除标记点
+          function clearMarker() {
+            if (marker) {
+              marker.setMap(null);
+              marker = null;
+            }
+          }
+          // 连线 写标记
+          function computeDis(m1: any, m2: any, type: string) {
+            const line = new AMap.Polyline({
+              strokeColor: '#01C1B5',
+              strokeWeight: 3,
+              isOutline: false,
+              outlineColor: '#01C1B5'
+            });
+            line.setMap(map.value);
+
+            const p1 = m1.getPosition();
+            const p2 = m2.getPosition();
+            const textPos = p1.divideBy(2).add(p2.divideBy(2));
+            const distance = Math.round(p1.distance(p2));
+            const path = [p1, p2];
+            line.setPath(path);
+
+            const a = calcAngle([p1.lng, p2.lng], [p2.lng, p1.lng]);
+            console.log(a, 'aaaaaaa');
+
+            if (type == 'top') {
+              console.log(p1, p2, textPos, distance, 'computed', type);
+              const text = new AMap.Text({
+                // text: '签到点距离学校' + distance + '米',
+                text: distance + 'M',
+                // angle: a,
+                style: {
+                  'font-size': '12px'
+                }
+                //设置文本标注方位
+              });
+
+              // text.setOffset(new AMap.Pixel(-40, -10));
+              text.setPosition(textPos);
+              text.setMap(map.value);
+            } else {
+              const text = new AMap.Text({
+                // text: '签退点距离学校' + distance + '米',
+                text: distance + 'M',
+                angle: a,
+                style: {
+                  'font-size': '12px'
+                }
+                //设置文本标注方位
+              });
+              // text.setOffset(new AMap.Pixel(-40, -50));
+              text.setPosition(textPos);
+              text.setMap(map.value);
+              // line.setPath(path);
+            }
+          }
+
+          // 计算两点间的角度
+          function calcAngle(start: any, end: any) {
+            const p_start = map.value.lngLatToContainer(start),
+              p_end = map.value.lngLatToContainer(end);
+
+            const diff_x = p_end.x - p_start.x,
+              diff_y = p_end.y - p_start.y;
+            console.log((360 * Math.atan2(diff_y, diff_x)) / (2 * Math.PI));
+            return (360 * Math.atan2(diff_y, diff_x)) / (2 * Math.PI) - 45;
+          }
+        })
+        .catch(() => {
+          //
+        });
+    };
+
+    onMounted(() => {
+      ininMap();
+    });
+
+    return () => (
+      <div class={styles['amap-gps']}>
+        <MSticky position="top">
+          <MHeader />
+        </MSticky>
+
+        <div
+          id="gpsContainer"
+          style={{
+            width: '100%',
+            height: 'calc(100vh - var(--header-height))',
+            position: 'relative'
+          }}></div>
+      </div>
+    );
+  }
+});

+ 245 - 86
src/views/teacher-attendance/detail.tsx

@@ -7,26 +7,92 @@ import iconSuccess from '@/common/images/icon-check-active.png';
 import iconWarn from '@/common/images/icon-warn.png';
 import SkeletionDetailModal from './skeletion-detail-modal';
 import MHeader from '@/components/m-header';
+import request from '@/helpers/request';
+import { useRoute } from 'vue-router';
+import { coursesType, teacherAttendanceStatus } from '@/helpers/constant';
+import dayjs from 'dayjs';
+import MEmpty from '@/components/m-empty';
 
 export default defineComponent({
   name: 'teacher-attendance-detail',
   setup() {
+    const route = useRoute();
     const forms = reactive({
+      teacherId: route.query.teacherId,
+      classGroupId: route.query.classGroupId,
       listState: {
         loading: true,
-        loadingList: true
+        loadingList: true,
+        dataShow: true,
+        refreshing: false
       },
-      classValue: '',
-      classColumns: [{ text: '全部状态', value: '' }],
-      typeValue: '',
-      typeColumns: [{ text: '全部状态', value: '' }]
+      classColumns: [
+        { text: '全部状态', value: '' },
+        { text: '正常', value: '1' },
+        { text: '异常', value: '0' }
+      ],
+      typeColumns: [
+        { text: '全部类型', value: '' },
+        { text: '声部课', value: 'TRAINING' },
+        { text: '集训声部课', value: 'TRAINING_SINGLE' }
+      ],
+      params: {
+        courseScheduleType: '',
+        signInStatus: ''
+      },
+      teacherInfo: {} as any,
+      list: []
     });
 
-    onMounted(() => {
-      setTimeout(() => {
-        forms.listState.loading = false;
+    // 老师详情
+    const getTeacherInfo = async () => {
+      try {
+        const { data } = await request.post(
+          '/api-web/schoolTeacherAttendance/queryTeacherAttendance',
+          {
+            data: {
+              teacherId: forms.teacherId,
+              classGroupId: forms.classGroupId
+            }
+          }
+        );
+        const result = data || [];
+        forms.teacherInfo = result[0] || {};
+      } catch {
+        //
+      }
+      forms.listState.loading = false;
+    };
+
+    const getList = async () => {
+      try {
+        const { data } = await request.post(
+          '/api-web/schoolTeacherAttendance/queryTeacherAttendance',
+          {
+            data: {
+              ...forms.params
+            }
+          }
+        );
+        const result = data || [];
+        forms.list = result || [];
+      } catch {
+        //
+      } finally {
+        forms.listState.dataShow = forms.list.length > 0;
+        forms.listState.refreshing = false;
         forms.listState.loadingList = false;
-      }, 1000);
+      }
+    };
+
+    onMounted(() => {
+      // setTimeout(() => {
+      //   forms.listState.loading = false;
+      //   forms.listState.loadingList = false;
+      // }, 1000);
+
+      getTeacherInfo();
+      getList();
     });
     return () => (
       <div class={styles.teacherAttendanceDetail}>
@@ -40,7 +106,7 @@ export default defineComponent({
               {{
                 icon: () => (
                   <Image
-                    src={iconTeacher}
+                    src={forms.teacherInfo.teacherAvatar || iconTeacher}
                     fit="contain"
                     class={styles.iconTeacher}
                   />
@@ -48,21 +114,44 @@ export default defineComponent({
 
                 title: () => (
                   <div class={styles.username}>
-                    <p class={styles.name}>孙忆枫</p>
-                    <p class={styles.class}>长笛声部班</p>
+                    <p class={styles.name}>{forms.teacherInfo.teacherName}</p>
+                    <p class={styles.class}>
+                      {forms.teacherInfo.classGroupName}
+                    </p>
                   </div>
                 ),
                 value: () => (
-                  <div class={[styles.attendance, styles.noLink]}>
+                  <div class={styles.attendance}>
                     <div class={[styles.attendanceItem, styles.weekAttendance]}>
-                      <p class={[styles.value, styles.error]}>正常</p>
+                      <p
+                        class={[
+                          styles.value,
+                          forms.teacherInfo.teacherAttendanceStatus === 'ERR'
+                            ? styles.error
+                            : '',
+                          forms.teacherInfo.teacherAttendanceStatus === 'NORMAL'
+                            ? styles.success
+                            : ''
+                        ]}>
+                        {teacherAttendanceStatus[
+                          forms.teacherInfo.teacherAttendanceStatus
+                        ] || '--'}
+                      </p>
                       <p class={styles.title}>本周考勤</p>
                     </div>
                     <div
                       class={[styles.attendanceItem, styles.classAttendance]}>
-                      <p class={[styles.value, styles.success]}>
-                        2<span>课时</span>
+                      <p
+                        class={[
+                          styles.value,
+                          forms.teacherInfo.errCourseNum > 0
+                            ? styles.error
+                            : styles.success
+                        ]}>
+                        {forms.teacherInfo.errCourseNum}
+                        <span>课时</span>
                       </p>
+
                       <p class={styles.title}>学期异常</p>
                     </div>
                   </div>
@@ -74,85 +163,155 @@ export default defineComponent({
 
         <DropdownMenu>
           <DropdownItem
-            v-model={forms.classValue}
-            options={forms.classColumns as any}></DropdownItem>
+            v-model={forms.params.signInStatus}
+            options={forms.classColumns as any}
+            onChange={() => {
+              forms.listState.dataShow = true;
+              forms.list = [];
+              getList();
+            }}></DropdownItem>
           <DropdownItem
-            v-model={forms.typeValue}
-            options={forms.typeColumns as any}></DropdownItem>
+            v-model={forms.params.courseScheduleType}
+            options={forms.typeColumns as any}
+            onChange={() => {
+              forms.listState.dataShow = true;
+              forms.list = [];
+              getList();
+            }}></DropdownItem>
         </DropdownMenu>
 
         <SkeletionDetailModal v-model:show={forms.listState.loadingList}>
-          {[1, 2, 3, 4].map(() => (
-            <CellGroup inset class={styles.detailCellGroup}>
-              <Cell center border={false} class={styles.className}>
-                {{
-                  title: () => <div class={styles.class}>集训声部课</div>,
-                  value: () => (
-                    <div class={styles.timer}>2023-03-14 13:00~14:00</div>
-                  )
-                }}
-              </Cell>
-              <Cell center>
-                <div class={styles.detailGroup}>
-                  <div class={styles.detailItem}>
-                    <div class={styles.detailStatus}>
-                      <span class={styles.statusName}>正常</span>
-                      <img src={iconSuccess} class={styles.img} />
+          {forms.listState.dataShow ? (
+            forms.list.map((item: any) => (
+              <CellGroup inset class={styles.detailCellGroup}>
+                <Cell center border={false} class={styles.className}>
+                  {{
+                    title: () => (
+                      <div class={styles.class}>
+                        {coursesType[item.courseScheduleType]}
+                      </div>
+                    ),
+                    value: () => (
+                      <div class={styles.timer}>
+                        {dayjs(item.classDate).format('YYYY-MM-DD HH:mm:ss')}
+                      </div>
+                    )
+                  }}
+                </Cell>
+                <Cell center>
+                  <div class={styles.detailGroup}>
+                    <div class={styles.detailItem}>
+                      <div class={styles.detailStatus}>
+                        <span
+                          class={[
+                            styles.statusName,
+                            item.signInStatus === 1 ? '' : styles.error
+                          ]}>
+                          {item.signInStatus === 1 ? '正常' : '异常'}
+                        </span>
+                        <img
+                          src={item.signInStatus === 1 ? iconSuccess : iconWarn}
+                          class={styles.img}
+                        />
+                      </div>
+                      <div class={styles.sign}>
+                        <span class={styles.signTime}>
+                          签到时间 {dayjs(item.signInTime).format('HH:mm:ss')}
+                        </span>
+                      </div>
                     </div>
-                    <div class={styles.sign}>
-                      <span class={styles.signTime}>签到地点</span>
-                      <span class={styles.locate}>
-                        查看定位 <Icon name="arrow" class={styles.iconArrow} />
-                      </span>
+                    <div class={styles.detailItem}>
+                      <div class={styles.detailStatus}>
+                        <span
+                          class={[
+                            styles.statusName,
+                            item.signInAddressStatus === 'YES'
+                              ? ''
+                              : styles.error
+                          ]}>
+                          {item.signInAddressStatus === 'YES' ? '正常' : '异常'}
+                        </span>
+                        <img
+                          src={
+                            item.signInAddressStatus === 'YES'
+                              ? iconSuccess
+                              : iconWarn
+                          }
+                          class={styles.img}
+                        />
+                      </div>
+                      <div class={styles.sign}>
+                        <span class={styles.signTime}>签到地点</span>
+                        <span class={styles.locate}>
+                          查看定位
+                          <Icon name="arrow" class={styles.iconArrow} />
+                        </span>
+                      </div>
                     </div>
-                  </div>
-                  <div class={styles.detailItem}>
-                    <div class={styles.detailStatus}>
-                      <span class={styles.statusName}>正常</span>
-                      <img src={iconSuccess} class={styles.img} />
-                    </div>
-                    <div class={styles.sign}>
-                      <span class={styles.signTime}>签到地点</span>
-                      <span class={styles.locate}>
-                        查看定位
-                        <Icon name="arrow" class={styles.iconArrow} />
-                      </span>
-                    </div>
-                  </div>
-                  <div class={styles.detailItem}>
-                    <div class={styles.detailStatus}>
-                      <span class={[styles.statusName, styles.error]}>
-                        异常签退
-                      </span>
-                      <img src={iconWarn} class={styles.img} />
+                    <div class={styles.detailItem}>
+                      <div class={styles.detailStatus}>
+                        <span
+                          class={[
+                            styles.statusName,
+                            item.signOutStatus === 1 ? '' : styles.error
+                          ]}>
+                          {item.signOutStatus === 1 ? '正常' : '异常'}
+                        </span>
+                        <img
+                          src={
+                            item.signOutStatus === 1 ? iconSuccess : iconWarn
+                          }
+                          class={styles.img}
+                        />
+                      </div>
+                      <div class={styles.sign}>
+                        <span class={styles.signTime}>
+                          签退时间 {dayjs(item.signOutTime).format('HH:mm:ss')}
+                        </span>
+                      </div>
                     </div>
-                    <div class={styles.sign}>
-                      <span class={styles.signTime}>签到地点</span>
-                      <span class={styles.locate}>
-                        查看定位
-                        <Icon name="arrow" class={styles.iconArrow} />
-                      </span>
+                    <div class={styles.detailItem}>
+                      <div class={styles.detailStatus}>
+                        <span
+                          class={[
+                            styles.statusName,
+                            item.signOutAddressStatus === 'YES'
+                              ? ''
+                              : styles.error
+                          ]}>
+                          {item.signOutAddressStatus === 'YES'
+                            ? '正常'
+                            : '异常'}
+                        </span>
+                        <img
+                          src={
+                            item.signOutAddressStatus === 'YES'
+                              ? iconSuccess
+                              : iconWarn
+                          }
+                          class={styles.img}
+                        />
+                      </div>
+                      <div class={styles.sign}>
+                        <span class={styles.signTime}>签到地点</span>
+                        <span class={styles.locate}>
+                          查看定位
+                          <Icon name="arrow" class={styles.iconArrow} />
+                        </span>
+                      </div>
                     </div>
                   </div>
-                  <div class={styles.detailItem}>
-                    <div class={styles.detailStatus}>
-                      <span class={[styles.statusName, styles.error]}>
-                        异常
-                      </span>
-                      <img src={iconWarn} class={styles.img} />
-                    </div>
-                    <div class={styles.sign}>
-                      <span class={styles.signTime}>签到地点</span>
-                      <span class={styles.locate}>
-                        查看定位
-                        <Icon name="arrow" class={styles.iconArrow} />
-                      </span>
-                    </div>
-                  </div>
-                </div>
-              </Cell>
-            </CellGroup>
-          ))}
+                </Cell>
+              </CellGroup>
+            ))
+          ) : (
+            <MEmpty
+              style={{
+                minHeight: `calc(60vh)`
+              }}
+              description="暂无数据"
+            />
+          )}
         </SkeletionDetailModal>
       </div>
     );

+ 177 - 61
src/views/teacher-attendance/index.tsx

@@ -8,6 +8,10 @@ import DropDownModal from '../site-management/drop-down-modal';
 import iconTeacher from '@/common/images/icon-teacher-default.png';
 import SkeletionIndexModal from './skeletion-index-modal';
 import { useRouter } from 'vue-router';
+import request from '@/helpers/request';
+import MFullRefresh from '@/components/m-full-refresh';
+import { teacherAttendanceStatus } from '@/helpers/constant';
+import MEmpty from '@/components/m-empty';
 
 export default defineComponent({
   name: 'teacher-attendance',
@@ -17,17 +21,21 @@ export default defineComponent({
     const router = useRouter();
     const forms = reactive({
       listState: {
-        loading: true
+        loading: true,
+        dataShow: true,
+        refreshing: false
       },
-      statusValue: '',
-      statusColumns: [
-        { text: '全部状态', value: '' },
-        { text: '正常', value: 'normal' },
-        { text: '异常', value: 'error' },
-        { text: '无课', value: 'noClass' }
-      ],
-      classValue: '',
-      classColumns: [{ text: '全部班级', value: '' }]
+      statusColumns: [{ text: '全部状态', value: '' }],
+      classColumns: [{ text: '全部班级', value: '' }],
+      isClick: false,
+      params: {
+        teacherAttendanceStatus: '',
+        classType: '',
+        search: ''
+        // page: 1,
+        // rows: 20
+      },
+      list: []
     });
 
     const onDropDownClose = (item: any) => {
@@ -38,7 +46,7 @@ export default defineComponent({
       if (type === 'orchestra') {
         let name = '';
         forms.classColumns.forEach((item: any) => {
-          if (forms.classValue === item.value) {
+          if (forms.params.classType === item.value) {
             name = item.text;
           }
         });
@@ -46,33 +54,90 @@ export default defineComponent({
       }
     };
 
-    onMounted(() => {
-      setTimeout(() => {
+    //
+    const getList = async () => {
+      try {
+        if (forms.isClick) return;
+        forms.isClick = true;
+        const { data } = await request.post(
+          '/api-web/schoolTeacherAttendance/queryTeacherAttendance',
+          {
+            data: {
+              ...forms.params
+            }
+          }
+        );
+        const result = data || [];
+        // 处理重复请求数据
+        if (forms.list.length > 0 && result.pageNo === 1) {
+          return;
+        }
+        forms.list = result || [];
+
+        // forms.listState.finished = result.pageNo >= result.totalPage;
+        // forms.params.page = result.pageNo + 1;
+      } catch {
+        // forms.listState.finished = true;
+      } finally {
+        forms.listState.dataShow = forms.list.length > 0;
+        forms.listState.refreshing = false;
         forms.listState.loading = false;
-      }, 2000);
+        forms.isClick = false;
+      }
+    };
+
+    onMounted(() => {
+      // setTimeout(() => {
+      //   forms.listState.loading = false;
+      // }, 2000);
+      for (const key in teacherAttendanceStatus) {
+        if (
+          Object.prototype.hasOwnProperty.call(teacherAttendanceStatus, key)
+        ) {
+          const text = teacherAttendanceStatus[key];
+          forms.statusColumns.push({
+            text,
+            value: key
+          });
+        }
+      }
+
+      getList();
     });
     return () => (
       <div class={styles.teacherAttendance}>
         <MSticky position="top">
           <MHeader />
-          <MSearch placeholder="请输入老师姓名" />
+          <MSearch
+            placeholder="请输入老师姓名"
+            onSearch={(val: string) => {
+              forms.params.search = val;
+              forms.listState.dataShow = true;
+              getList();
+            }}
+          />
           <DropdownMenu>
             <DropdownItem
               ref={dropDownItemRef}
-              v-model={forms.statusValue}
-              options={forms.statusColumns as any}></DropdownItem>
+              v-model={forms.params.teacherAttendanceStatus}
+              options={forms.statusColumns as any}
+              onChange={() => {
+                forms.listState.dataShow = true;
+                getList();
+              }}></DropdownItem>
             <DropdownItem
               ref={dropDownItemRef1}
               title={formatName('orchestra')}>
               <DropDownModal
-                selectValues={forms.classValue}
+                selectValues={forms.params.classType}
                 columns={forms.classColumns}
                 open={dropDownItemRef1.value.state.showPopup}
                 onDropDownClose={() => onDropDownClose(dropDownItemRef1)}
                 onDropDownConfirm={(values: any) => {
-                  forms.classValue = values[0];
-                  console.log(values, 'orchestra');
+                  forms.params.classType = values[0];
                   onDropDownClose(dropDownItemRef1);
+                  forms.listState.dataShow = true;
+                  getList();
                 }}
               />
             </DropdownItem>
@@ -80,48 +145,99 @@ export default defineComponent({
         </MSticky>
 
         <SkeletionIndexModal v-model:show={forms.listState.loading}>
-          <List class={styles.cellGroup}>
-            <Cell
-              center
-              isLink
-              clickable={false}
-              class={styles.cell}
-              onClick={() => {
-                router.push('/teacher-attendance-detail');
-              }}>
-              {{
-                icon: () => (
-                  <Image
-                    src={iconTeacher}
-                    fit="contain"
-                    class={styles.iconTeacher}
-                  />
-                ),
+          <MFullRefresh
+            v-model:modelValue={forms.listState.refreshing}
+            onRefresh={() => getList()}
+            style={{
+              minHeight: `calc(100vh - var(--header-height))`
+            }}>
+            <div class={styles.cellGroup}>
+              {forms.listState.dataShow ? (
+                forms.list.map((item: any) => (
+                  <Cell
+                    center
+                    isLink
+                    clickable={false}
+                    class={styles.cell}
+                    onClick={() => {
+                      router.push({
+                        path: '/teacher-attendance-detail',
+                        query: {
+                          teacherId: item.teacherId,
+                          classGroupId: item.classGroupId
+                        }
+                      });
+                    }}>
+                    {{
+                      icon: () => (
+                        <Image
+                          src={item.teacherAvatar || iconTeacher}
+                          fit="contain"
+                          class={styles.iconTeacher}
+                        />
+                      ),
 
-                title: () => (
-                  <div class={styles.username}>
-                    <p class={styles.name}>孙忆枫</p>
-                    <p class={styles.class}>长笛声部班</p>
-                  </div>
-                ),
-                value: () => (
-                  <div class={styles.attendance}>
-                    <div class={[styles.attendanceItem, styles.weekAttendance]}>
-                      <p class={[styles.value, styles.error]}>正常</p>
-                      <p class={styles.title}>本周考勤</p>
-                    </div>
-                    <div
-                      class={[styles.attendanceItem, styles.classAttendance]}>
-                      <p class={[styles.value, styles.success]}>
-                        2<span>课时</span>
-                      </p>
-                      <p class={styles.title}>学期异常</p>
-                    </div>
-                  </div>
-                )
-              }}
-            </Cell>
-          </List>
+                      title: () => (
+                        <div class={styles.username}>
+                          <p class={styles.name}>{item.teacherName}</p>
+                          <p class={styles.class}>{item.classGroupName}</p>
+                        </div>
+                      ),
+                      value: () => (
+                        <div class={styles.attendance}>
+                          <div
+                            class={[
+                              styles.attendanceItem,
+                              styles.weekAttendance
+                            ]}>
+                            <p
+                              class={[
+                                styles.value,
+                                item.teacherAttendanceStatus === 'ERR'
+                                  ? styles.error
+                                  : '',
+                                item.teacherAttendanceStatus === 'NORMAL'
+                                  ? styles.success
+                                  : ''
+                              ]}>
+                              {teacherAttendanceStatus[
+                                item.teacherAttendanceStatus
+                              ] || '--'}
+                            </p>
+                            <p class={styles.title}>本周考勤</p>
+                          </div>
+                          <div
+                            class={[
+                              styles.attendanceItem,
+                              styles.classAttendance
+                            ]}>
+                            <p
+                              class={[
+                                styles.value,
+                                item.errCourseNum > 0
+                                  ? styles.error
+                                  : styles.success
+                              ]}>
+                              {item.errCourseNum}
+                              <span>课时</span>
+                            </p>
+                            <p class={styles.title}>学期异常</p>
+                          </div>
+                        </div>
+                      )
+                    }}
+                  </Cell>
+                ))
+              ) : (
+                <MEmpty
+                  style={{
+                    minHeight: `calc(100vh - var(--header-height))`
+                  }}
+                  description="暂无老师考勤"
+                />
+              )}
+            </div>
+          </MFullRefresh>
         </SkeletionIndexModal>
       </div>
     );