mo 2 年之前
父节点
当前提交
777665693b
共有 38 个文件被更改,包括 2455 次插入10 次删除
  1. 131 0
      package-lock.json
  2. 3 0
      package.json
  3. 二进制
      src/components/CDatePicker/images/dateIcon.png
  4. 26 0
      src/components/CDatePicker/index.module.less
  5. 55 0
      src/components/CDatePicker/index.tsx
  6. 1 2
      src/components/layout/index.module.less
  7. 4 0
      src/components/pagination/index.module.less
  8. 170 0
      src/components/pagination/index.tsx
  9. 28 0
      src/enums/breakpointEnum.ts
  10. 47 0
      src/hooks/core/useTimeout.ts
  11. 91 0
      src/hooks/event/useBreakpoint.ts
  12. 66 0
      src/hooks/event/useEventListener.ts
  13. 36 0
      src/hooks/event/useWindowSizeFn.ts
  14. 3 0
      src/hooks/index.ts
  15. 34 0
      src/hooks/setting/index.ts
  16. 18 0
      src/hooks/setting/useDesignSetting.ts
  17. 42 0
      src/hooks/setting/useProjectSetting.ts
  18. 93 0
      src/hooks/useBattery.ts
  19. 23 0
      src/hooks/useDomWidth.ts
  20. 30 0
      src/hooks/useOnline.ts
  21. 55 0
      src/hooks/useTime.ts
  22. 124 0
      src/hooks/web/useECharts.ts
  23. 63 0
      src/hooks/web/usePage.ts
  24. 52 0
      src/hooks/web/usePermission.ts
  25. 40 0
      src/store/modules/designSetting.ts
  26. 55 0
      src/utils/lib/echarts.ts
  27. 105 0
      src/utils/searchs.ts
  28. 42 0
      src/views/home/components/study.tsx
  29. 71 0
      src/views/home/components/teachList.tsx
  30. 299 0
      src/views/home/components/trainData.tsx
  31. 二进制
      src/views/home/images/boxIcon.png
  32. 二进制
      src/views/home/images/cloundIcon.png
  33. 二进制
      src/views/home/images/goClass.png
  34. 二进制
      src/views/home/images/headerD.png
  35. 439 5
      src/views/home/index.module.less
  36. 157 3
      src/views/home/index.tsx
  37. 22 0
      src/views/home/modals/teachGroup.tsx
  38. 30 0
      src/views/home/modals/teachItem.tsx

+ 131 - 0
package-lock.json

@@ -10,8 +10,10 @@
       "license": "MIT",
       "dependencies": {
         "@vant/use": "^1.5.1",
+        "@vueuse/core": "^10.2.0",
         "clean-deep": "^3.4.0",
         "dayjs": "^1.11.7",
+        "echarts": "^5.4.2",
         "numeral": "^2.0.6",
         "pinia": "^2.1.4",
         "umi-request": "^1.4.0",
@@ -24,6 +26,7 @@
         "@babel/preset-env": "^7.21.4",
         "@types/crypto-js": "^4.1.1",
         "@types/node": "^16.18.23",
+        "@types/numeral": "^2.0.2",
         "@typescript-eslint/eslint-plugin": "^5.57.1",
         "@typescript-eslint/parser": "^5.57.1",
         "@vitejs/plugin-vue": "^4.1.0",
@@ -2567,6 +2570,12 @@
       "integrity": "sha512-zvSN2Esek1aeLdKDYuntKAYjti9Z2oT4I8bfkLLhIxHlv3dwZ5vvATxOc31820iYm4hQRCwjUgDpwSMFjfTUnw==",
       "dev": true
     },
+    "node_modules/@types/numeral": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/@types/numeral/-/numeral-2.0.2.tgz",
+      "integrity": "sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA==",
+      "dev": true
+    },
     "node_modules/@types/semver": {
       "version": "7.3.13",
       "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.13.tgz",
@@ -2582,6 +2591,11 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.17",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
+      "integrity": "sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA=="
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "5.59.1",
       "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz",
@@ -3125,6 +3139,30 @@
       "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz",
       "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
     },
+    "node_modules/@vueuse/core": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.2.0.tgz",
+      "integrity": "sha512-aHBnoCteIS3hFu7ZZkVB93SanVDY6t4TIb7XDLxJT/HQdAZz+2RdIEJ8rj5LUoEJr7Damb5+sJmtpCwGez5ozQ==",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.17",
+        "@vueuse/metadata": "10.2.0",
+        "@vueuse/shared": "10.2.0",
+        "vue-demi": ">=0.14.5"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.2.0.tgz",
+      "integrity": "sha512-IR7Mkq6QSgZ38q/2ZzOt+Zz1OpcEsnwE64WBumDQ+RGKrosFCtUA2zgRrOqDEzPBXrVB+4HhFkwDjQMu0fDBKw=="
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.2.0.tgz",
+      "integrity": "sha512-dIeA8+g9Av3H5iF4NXR/sft4V6vys76CpZ6hxwj8eMXybXk2WRl3scSsOVi+kQ9SX38COR7AH7WwY83UcuxbSg==",
+      "dependencies": {
+        "vue-demi": ">=0.14.5"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.8.2",
       "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz",
@@ -3950,6 +3988,20 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
       "dev": true
     },
+    "node_modules/echarts": {
+      "version": "5.4.2",
+      "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.2.tgz",
+      "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.4.3"
+      }
+    },
+    "node_modules/echarts/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.4.371",
       "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz",
@@ -8540,6 +8592,19 @@
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
       "dev": true
+    },
+    "node_modules/zrender": {
+      "version": "5.4.3",
+      "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.3.tgz",
+      "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    },
+    "node_modules/zrender/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
     }
   },
   "dependencies": {
@@ -10252,6 +10317,12 @@
       "integrity": "sha512-zvSN2Esek1aeLdKDYuntKAYjti9Z2oT4I8bfkLLhIxHlv3dwZ5vvATxOc31820iYm4hQRCwjUgDpwSMFjfTUnw==",
       "dev": true
     },
+    "@types/numeral": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/@types/numeral/-/numeral-2.0.2.tgz",
+      "integrity": "sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA==",
+      "dev": true
+    },
     "@types/semver": {
       "version": "7.3.13",
       "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.13.tgz",
@@ -10267,6 +10338,11 @@
         "@types/node": "*"
       }
     },
+    "@types/web-bluetooth": {
+      "version": "0.0.17",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
+      "integrity": "sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA=="
+    },
     "@typescript-eslint/eslint-plugin": {
       "version": "5.59.1",
       "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz",
@@ -10688,6 +10764,30 @@
       "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz",
       "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
     },
+    "@vueuse/core": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.2.0.tgz",
+      "integrity": "sha512-aHBnoCteIS3hFu7ZZkVB93SanVDY6t4TIb7XDLxJT/HQdAZz+2RdIEJ8rj5LUoEJr7Damb5+sJmtpCwGez5ozQ==",
+      "requires": {
+        "@types/web-bluetooth": "^0.0.17",
+        "@vueuse/metadata": "10.2.0",
+        "@vueuse/shared": "10.2.0",
+        "vue-demi": ">=0.14.5"
+      }
+    },
+    "@vueuse/metadata": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.2.0.tgz",
+      "integrity": "sha512-IR7Mkq6QSgZ38q/2ZzOt+Zz1OpcEsnwE64WBumDQ+RGKrosFCtUA2zgRrOqDEzPBXrVB+4HhFkwDjQMu0fDBKw=="
+    },
+    "@vueuse/shared": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.2.0.tgz",
+      "integrity": "sha512-dIeA8+g9Av3H5iF4NXR/sft4V6vys76CpZ6hxwj8eMXybXk2WRl3scSsOVi+kQ9SX38COR7AH7WwY83UcuxbSg==",
+      "requires": {
+        "vue-demi": ">=0.14.5"
+      }
+    },
     "acorn": {
       "version": "8.8.2",
       "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz",
@@ -11366,6 +11466,22 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
       "dev": true
     },
+    "echarts": {
+      "version": "5.4.2",
+      "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.2.tgz",
+      "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==",
+      "requires": {
+        "tslib": "2.3.0",
+        "zrender": "5.4.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+          "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+        }
+      }
+    },
     "electron-to-chromium": {
       "version": "1.4.371",
       "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz",
@@ -14923,6 +15039,21 @@
           "dev": true
         }
       }
+    },
+    "zrender": {
+      "version": "5.4.3",
+      "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.3.tgz",
+      "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==",
+      "requires": {
+        "tslib": "2.3.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+          "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+        }
+      }
     }
   }
 }

+ 3 - 0
package.json

@@ -23,8 +23,10 @@
   },
   "dependencies": {
     "@vant/use": "^1.5.1",
+    "@vueuse/core": "^10.2.0",
     "clean-deep": "^3.4.0",
     "dayjs": "^1.11.7",
+    "echarts": "^5.4.2",
     "numeral": "^2.0.6",
     "pinia": "^2.1.4",
     "umi-request": "^1.4.0",
@@ -37,6 +39,7 @@
     "@babel/preset-env": "^7.21.4",
     "@types/crypto-js": "^4.1.1",
     "@types/node": "^16.18.23",
+    "@types/numeral": "^2.0.2",
     "@typescript-eslint/eslint-plugin": "^5.57.1",
     "@typescript-eslint/parser": "^5.57.1",
     "@vitejs/plugin-vue": "^4.1.0",

二进制
src/components/CDatePicker/images/dateIcon.png


+ 26 - 0
src/components/CDatePicker/index.module.less

@@ -0,0 +1,26 @@
+.CdataWrap {
+  position: relative;
+  :global {
+    .n-input {
+      width: 353px;
+      height: 43px;
+      line-height: 43px;
+      border-radius: 8px;
+      font-size: 15px;
+      font-weight: 400;
+      color: #333333;
+
+      &:nth-last-of-type(1) {
+        padding-left: 25px;
+      }
+    }
+  }
+  .dateIcons {
+    width: 20px;
+    height: 19px;
+    position: absolute;
+    z-index: 500;
+    left: 15px;
+    top: 11px;
+  }
+}

+ 55 - 0
src/components/CDatePicker/index.tsx

@@ -0,0 +1,55 @@
+import { defineComponent, ref } from 'vue';
+import styles from './index.module.less';
+import { NIcon, NImage, NDatePicker } from 'naive-ui';
+import dateIcons from './images/dateIcon.png';
+export default defineComponent({
+  name: 'CDatePicker',
+  props: {
+    type: {
+      type: String,
+      default: 'daterange'
+    },
+    separator: {
+      type: String,
+      default: '-'
+    }
+  },
+  setup(props, { emit }) {
+    const timer = ref(null);
+    const updateTimer = () => {
+      console.log('更新日期', timer.value);
+      emit('update:value', timer.value);
+    };
+    return () => (
+      <>
+        <div class={styles.CdataWrap}>
+          <NImage
+            previewDisabled
+            class={styles.dateIcons}
+            src={dateIcons}></NImage>
+          <NDatePicker
+            class={styles.CDatePicker}
+            clearable
+            v-model:value={timer.value}
+            separator={props.separator}
+            type={props.type as any}
+            onUpdate:value={updateTimer}
+            v-slots={{
+              'date-icon': () => (
+                <>
+                  <span></span>
+                </>
+              )
+            }}></NDatePicker>
+        </div>
+      </>
+    );
+  }
+});
+/**
+ *  <NImage
+                  previewDisabled
+                  class={styles.dateIcons}
+                  src={dateIcons}></NImage>
+ *
+ */

+ 1 - 2
src/components/layout/index.module.less

@@ -6,8 +6,6 @@
   .WrapcoreView {
     margin: 32px;
   }
-  height: 100vh;
-  overflow-y: scroll;
 }
 .silder {
   width: 100px;
@@ -70,6 +68,7 @@
   }
 }
 .Wrapcore {
+  height: 100%;
   flex: 1;
   .layoutTop {
     height: 64px;

+ 4 - 0
src/components/pagination/index.module.less

@@ -0,0 +1,4 @@
+.pagination {
+  margin-top: 12px;
+  justify-content: flex-end;
+}

+ 170 - 0
src/components/pagination/index.tsx

@@ -0,0 +1,170 @@
+import { Searchs } from '@/utils/searchs';
+import { NDataTable, NPagination } from 'naive-ui';
+import {
+  computed,
+  defineComponent,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch
+} from 'vue';
+import { useRoute } from 'vue-router';
+import styles from './index.module.less';
+
+export default defineComponent({
+  name: 'table-container',
+  props: {
+    page: {
+      type: Number,
+      default: 1,
+      required: true
+    },
+    pageSize: {
+      type: Number,
+      default: 10
+    },
+    pageTotal: {
+      type: Number,
+      default: 0
+    },
+    saveKey: {
+      type: String,
+      default: ''
+    },
+    sync: {
+      type: Boolean,
+      default: false
+    },
+    checkedRowKeysRef: {
+      type: Object
+    }
+  },
+  emits: ['update:page', 'update:pageSize', 'list'],
+  setup(props, { slots, attrs, emit }) {
+    const route = useRoute();
+    const state = reactive({
+      pageInformation: null as any
+    });
+
+    const pageCount = ref(0);
+
+    pageCount.value = Math.ceil(props.pageTotal / props.pageSize);
+
+    // 当前页发生改变时的回调函数
+    const onUpdatePage = (page: number) => {
+      emit('update:page', page);
+      emit('list');
+
+      syncStore();
+    };
+    // 当前分页大小发生改变时的回调函数
+    const onUpdatePageSize = (pageSize: number) => {
+      emit('update:pageSize', pageSize);
+      emit('list');
+
+      syncStore();
+    };
+
+    onMounted(() => {
+      if (props.sync) {
+        const searchs = new Searchs(props.saveKey || route.path);
+        const active = searchs.get(props.saveKey || route.path);
+        state.pageInformation = active;
+        if (active && active.page) {
+          for (const key in active.page) {
+            if (
+              // eslint-disable-next-line no-prototype-builtins
+              active.page.hasOwnProperty(key) &&
+              ['page', 'pageSize'].includes(key)
+            ) {
+              const item = active.page[key];
+              const tempKey = `update:${key}` as any;
+              emit(tempKey, item);
+            }
+          }
+        }
+        if (props.saveKey) {
+          searchs.update(route.path, undefined, 'bind');
+        }
+      }
+      window.addEventListener('watchStorage', watchStorage);
+    });
+
+    onUnmounted(() => {
+      window.removeEventListener('watchStorage', watchStorage);
+    });
+
+    watch(
+      () => props.pageSize,
+      () => {
+        pageCount.value = Math.ceil(props.pageTotal / props.pageSize);
+        syncStore();
+      }
+    );
+    watch(
+      () => props.page,
+      () => {
+        syncStore();
+      }
+    );
+    watch(
+      () => props.pageTotal,
+      () => {
+        pageCount.value = Math.ceil(props.pageTotal / props.pageSize);
+        syncStore();
+      }
+    );
+
+    const currentPage = computed({
+      get() {
+        return props.page;
+      },
+      set(val) {
+        emit('update:page', val);
+      }
+    });
+
+    const syncStore = () => {
+      if (props.sync) {
+        const searchs = new Searchs(props.saveKey || route.path);
+        searchs.update(
+          {
+            page: props.page,
+            pageCount: pageCount.value,
+            pageSize: props.pageSize,
+            saveKey: props.saveKey
+          },
+          undefined,
+          'page'
+        );
+      }
+    };
+
+    const watchStorage = () => {
+      const page =
+        state.pageInformation && state.pageInformation.page
+          ? state.pageInformation.page
+          : null;
+      currentPage.value = page && page.page ? page.page : 1;
+    };
+
+    return () => (
+      <NPagination
+        style={{
+          marginTop: '12px',
+          justifyContent: 'flex-end'
+        }}
+        v-model:page={props.page}
+        displayOrder={['quick-jumper', 'pages', 'size-picker']}
+        pageCount={pageCount.value}
+        showQuickJumper
+        showSizePicker
+        pageSize={props.pageSize}
+        prefix={() => `共 ${props.pageTotal} 条`}
+        pageSizes={[10, 20, 30, 40]}
+        onUpdatePage={onUpdatePage}
+        onUpdatePageSize={onUpdatePageSize}></NPagination>
+    );
+  }
+});

+ 28 - 0
src/enums/breakpointEnum.ts

@@ -0,0 +1,28 @@
+export enum sizeEnum {
+  XS = 'XS',
+  SM = 'SM',
+  MD = 'MD',
+  LG = 'LG',
+  XL = 'XL',
+  XXL = 'XXL'
+}
+
+export enum screenEnum {
+  XS = 480,
+  SM = 576,
+  MD = 768,
+  LG = 992,
+  XL = 1200,
+  XXL = 1600
+}
+
+const screenMap = new Map<sizeEnum, number>();
+
+screenMap.set(sizeEnum.XS, screenEnum.XS);
+screenMap.set(sizeEnum.SM, screenEnum.SM);
+screenMap.set(sizeEnum.MD, screenEnum.MD);
+screenMap.set(sizeEnum.LG, screenEnum.LG);
+screenMap.set(sizeEnum.XL, screenEnum.XL);
+screenMap.set(sizeEnum.XXL, screenEnum.XXL);
+
+export { screenMap };

+ 47 - 0
src/hooks/core/useTimeout.ts

@@ -0,0 +1,47 @@
+import { ref, watch } from 'vue';
+import { Fn, tryOnUnmounted } from '@vueuse/core';
+import { isFunction } from '@/utils/is';
+
+export function useTimeoutFn(handle: any, wait: number, native = false) {
+  if (!isFunction(handle)) {
+    throw new Error('handle is not Function!');
+  }
+
+  const { readyRef, stop, start } = useTimeoutRef(wait);
+  if (native) {
+    handle();
+  } else {
+    watch(
+      readyRef,
+      (maturity) => {
+        maturity && handle();
+      },
+      { immediate: false }
+    );
+  }
+  return { readyRef, stop, start };
+}
+
+export function useTimeoutRef(wait: number) {
+  const readyRef = ref(false);
+
+  let timer: any;
+
+  function stop(): void {
+    readyRef.value = false;
+    timer && window.clearTimeout(timer);
+  }
+
+  function start(): void {
+    stop();
+    timer = setTimeout(() => {
+      readyRef.value = true;
+    }, wait);
+  }
+
+  start();
+
+  tryOnUnmounted(stop);
+
+  return { readyRef, stop, start };
+}

+ 91 - 0
src/hooks/event/useBreakpoint.ts

@@ -0,0 +1,91 @@
+import { ref, computed, ComputedRef, unref } from 'vue';
+import { useEventListener } from '@/hooks/event/useEventListener';
+import { screenMap, sizeEnum, screenEnum } from '@/enums/breakpointEnum';
+
+let globalScreenRef: ComputedRef<sizeEnum | undefined>;
+let globalWidthRef: ComputedRef<number>;
+let globalRealWidthRef: ComputedRef<number>;
+
+export interface CreateCallbackParams {
+  screen: ComputedRef<sizeEnum | undefined>;
+  width: ComputedRef<number>;
+  realWidth: ComputedRef<number>;
+  screenEnum: typeof screenEnum;
+  screenMap: Map<sizeEnum, number>;
+  sizeEnum: typeof sizeEnum;
+}
+
+export function useBreakpoint() {
+  return {
+    screenRef: computed(() => unref(globalScreenRef)),
+    widthRef: globalWidthRef,
+    screenEnum,
+    realWidthRef: globalRealWidthRef
+  };
+}
+
+// Just call it once
+export function createBreakpointListen(
+  fn?: (opt: CreateCallbackParams) => void
+) {
+  const screenRef = ref<sizeEnum>(sizeEnum.XL);
+  const realWidthRef = ref(window.innerWidth);
+
+  function getWindowWidth() {
+    const width = document.body.clientWidth;
+    const xs = screenMap.get(sizeEnum.XS)!;
+    const sm = screenMap.get(sizeEnum.SM)!;
+    const md = screenMap.get(sizeEnum.MD)!;
+    const lg = screenMap.get(sizeEnum.LG)!;
+    const xl = screenMap.get(sizeEnum.XL)!;
+    if (width < xs) {
+      screenRef.value = sizeEnum.XS;
+    } else if (width < sm) {
+      screenRef.value = sizeEnum.SM;
+    } else if (width < md) {
+      screenRef.value = sizeEnum.MD;
+    } else if (width < lg) {
+      screenRef.value = sizeEnum.LG;
+    } else if (width < xl) {
+      screenRef.value = sizeEnum.XL;
+    } else {
+      screenRef.value = sizeEnum.XXL;
+    }
+    realWidthRef.value = width;
+  }
+
+  useEventListener({
+    el: window,
+    name: 'resize',
+
+    listener: () => {
+      getWindowWidth();
+      resizeFn();
+    }
+    // wait: 100,
+  });
+
+  getWindowWidth();
+  globalScreenRef = computed(() => unref(screenRef));
+  globalWidthRef = computed((): number => screenMap.get(unref(screenRef)!)!);
+  globalRealWidthRef = computed((): number => unref(realWidthRef));
+
+  function resizeFn() {
+    fn?.({
+      screen: globalScreenRef,
+      width: globalWidthRef,
+      realWidth: globalRealWidthRef,
+      screenEnum,
+      screenMap,
+      sizeEnum
+    });
+  }
+
+  resizeFn();
+  return {
+    screenRef: globalScreenRef,
+    screenEnum,
+    widthRef: globalWidthRef,
+    realWidthRef: globalRealWidthRef
+  };
+}

+ 66 - 0
src/hooks/event/useEventListener.ts

@@ -0,0 +1,66 @@
+import type { Ref } from 'vue';
+
+import { ref, watch, unref } from 'vue';
+import { useThrottleFn, useDebounceFn } from '@vueuse/core';
+
+export type RemoveEventFn = () => void;
+
+export interface UseEventParams {
+  el?: Element | Ref<Element | undefined> | Window | any;
+  name: string;
+  listener: EventListener;
+  options?: boolean | AddEventListenerOptions;
+  autoRemove?: boolean;
+  isDebounce?: boolean;
+  wait?: number;
+}
+
+export function useEventListener({
+  el = window,
+  name,
+  listener,
+  options,
+  autoRemove = true,
+  isDebounce = true,
+  wait = 80
+}: UseEventParams): { removeEvent: RemoveEventFn } {
+  /* eslint-disable-next-line */
+  let remove: RemoveEventFn = () => {};
+  const isAddRef = ref(false);
+
+  if (el) {
+    const element: Ref<Element> = ref(el as Element);
+
+    // eslint-disable-next-line prettier/prettier
+    const handler = isDebounce
+      ? useDebounceFn(listener, wait)
+      : useThrottleFn(listener, wait);
+    const realHandler = wait ? handler : listener;
+    const removeEventListener = (e: Element) => {
+      isAddRef.value = true;
+      e.removeEventListener(name, realHandler, options);
+    };
+    // eslint-disable-next-line prettier/prettier
+    const addEventListener = (e: Element) =>
+      e.addEventListener(name, realHandler, options);
+
+    const removeWatch = watch(
+      element,
+      (v, _ov, cleanUp) => {
+        if (v) {
+          !unref(isAddRef) && addEventListener(v);
+          cleanUp(() => {
+            autoRemove && removeEventListener(v);
+          });
+        }
+      },
+      { immediate: true }
+    );
+
+    remove = () => {
+      removeEventListener(element.value);
+      removeWatch();
+    };
+  }
+  return { removeEvent: remove };
+}

+ 36 - 0
src/hooks/event/useWindowSizeFn.ts

@@ -0,0 +1,36 @@
+import { Fn, tryOnMounted, tryOnUnmounted } from '@vueuse/core';
+import { useDebounceFn } from '@vueuse/core';
+
+interface WindowSizeOptions {
+  once?: boolean;
+  immediate?: boolean;
+  listenerOptions?: AddEventListenerOptions | boolean;
+}
+
+export function useWindowSizeFn<T>(fn: any, wait = 150, options?: WindowSizeOptions) {
+  let handler = () => {
+    fn();
+  };
+  const handleSize = useDebounceFn(handler, wait);
+  handler = handleSize;
+
+  const start = () => {
+    if (options && options.immediate) {
+      handler();
+    }
+    window.addEventListener('resize', handler);
+  };
+
+  const stop = () => {
+    window.removeEventListener('resize', handler);
+  };
+
+  tryOnMounted(() => {
+    start();
+  });
+
+  tryOnUnmounted(() => {
+    stop();
+  });
+  return [start, stop];
+}

+ 3 - 0
src/hooks/index.ts

@@ -0,0 +1,3 @@
+import { useAsync } from './use-async';
+
+export { useAsync };

+ 34 - 0
src/hooks/setting/index.ts

@@ -0,0 +1,34 @@
+// import type { GlobConfig } from '/#/config';
+
+import { warn } from '@/utils/log';
+import { getAppEnvConfig } from '@/utils/env';
+
+export const useGlobSetting = (): Readonly<any> => {
+  const {
+    VITE_GLOB_APP_TITLE,
+    VITE_GLOB_API_URL,
+    VITE_GLOB_APP_SHORT_NAME,
+    VITE_GLOB_API_URL_PREFIX,
+    VITE_GLOB_UPLOAD_URL,
+    VITE_GLOB_PROD_MOCK,
+    VITE_GLOB_IMG_URL,
+  } = getAppEnvConfig();
+
+  if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
+    warn(
+      `VITE_GLOB_APP_SHORT_NAME Variables can only be characters/underscores, please modify in the environment variables and re-running.`
+    );
+  }
+
+  // Take global configuration
+  const glob: Readonly<any> = {
+    title: VITE_GLOB_APP_TITLE,
+    apiUrl: VITE_GLOB_API_URL,
+    shortName: VITE_GLOB_APP_SHORT_NAME,
+    urlPrefix: VITE_GLOB_API_URL_PREFIX,
+    uploadUrl: VITE_GLOB_UPLOAD_URL,
+    prodMock: VITE_GLOB_PROD_MOCK,
+    imgUrl: VITE_GLOB_IMG_URL,
+  };
+  return glob as Readonly<any>;
+};

+ 18 - 0
src/hooks/setting/useDesignSetting.ts

@@ -0,0 +1,18 @@
+import { computed } from 'vue';
+import { useDesignSettingStore } from '@/store/modules/designSetting';
+
+export function useDesignSetting() {
+  const designStore = useDesignSettingStore();
+
+  const getDarkTheme = computed(() => designStore.darkTheme);
+
+  const getAppTheme = computed(() => designStore.appTheme);
+
+  const getAppThemeList = computed(() => designStore.appThemeList);
+
+  return {
+    getDarkTheme,
+    getAppTheme,
+    getAppThemeList,
+  };
+}

+ 42 - 0
src/hooks/setting/useProjectSetting.ts

@@ -0,0 +1,42 @@
+import { computed } from 'vue';
+import { useProjectSettingStore } from '@/store/modules/projectSetting';
+
+export function useProjectSetting() {
+  const projectStore = useProjectSettingStore();
+
+  const getNavMode = computed(() => projectStore.navMode);
+
+  const getNavTheme = computed(() => projectStore.navTheme);
+
+  const getIsMobile = computed(() => projectStore.isMobile);
+
+  const getHeaderSetting = computed(() => projectStore.headerSetting);
+
+  const getMultiTabsSetting = computed(() => projectStore.multiTabsSetting);
+
+  const getMenuSetting = computed(() => projectStore.menuSetting);
+
+  const getCrumbsSetting = computed(() => projectStore.crumbsSetting);
+
+  const getPermissionMode = computed(() => projectStore.permissionMode);
+
+  const getShowFooter = computed(() => projectStore.showFooter);
+
+  const getIsPageAnimate = computed(() => projectStore.isPageAnimate);
+
+  const getPageAnimateType = computed(() => projectStore.pageAnimateType);
+
+  return {
+    getNavMode,
+    getNavTheme,
+    getIsMobile,
+    getHeaderSetting,
+    getMultiTabsSetting,
+    getMenuSetting,
+    getCrumbsSetting,
+    getPermissionMode,
+    getShowFooter,
+    getIsPageAnimate,
+    getPageAnimateType,
+  };
+}

+ 93 - 0
src/hooks/useBattery.ts

@@ -0,0 +1,93 @@
+import { computed, onMounted, reactive, toRefs } from 'vue';
+
+interface Battery {
+  charging: boolean; // 当前电池是否正在充电
+  chargingTime: number; // 距离充电完毕还需多少秒,如果为0则充电完毕
+  dischargingTime: number; // 代表距离电池耗电至空且挂起需要多少秒
+  level: number; // 代表电量的放大等级,这个值在 0.0 至 1.0 之间
+  [key: string]: any;
+}
+
+export const useBattery = () => {
+  const state = reactive({
+    battery: {
+      charging: false,
+      chargingTime: 0,
+      dischargingTime: 0,
+      level: 100,
+    } as any,
+  });
+
+  // 更新电池使用状态
+  const updateBattery = (target: any) => {
+    for (const key in state.battery) {
+      state.battery[key] = target[key];
+    }
+    state.battery.level = state.battery.level * 100;
+  };
+
+  // 计算电池剩余可用时间
+  const calcDischargingTime = computed(() => {
+    const hour = state.battery.dischargingTime / 3600;
+    const minute = (state.battery.dischargingTime / 60) % 60;
+    return `${~~hour}小时${~~minute}分钟`;
+  });
+
+  // 计算电池充满剩余时间
+  const calcChargingTime = computed(() => {
+    console.log(state.battery);
+    const hour = state.battery.chargingTime / 3600;
+    const minute = (state.battery.chargingTime / 60) % 60;
+    return `${~~hour}小时${~~minute}分钟`;
+  });
+
+  // 电池状态
+  const batteryStatus = computed(() => {
+    if (state.battery.charging && state.battery.level >= 100) {
+      return '已充满';
+    } else if (state.battery.charging) {
+      return '充电中';
+    } else {
+      return '已断开电源';
+    }
+  });
+
+  onMounted(async () => {
+    const BatteryManager: Battery = await (window.navigator as any).getBattery();
+    updateBattery(BatteryManager);
+
+    // 电池充电状态更新时被调用
+    BatteryManager.onchargingchange = ({ target }: any) => {
+      updateBattery(target);
+    };
+    // 电池充电时间更新时被调用
+    BatteryManager.onchargingtimechange = ({ target }: any) => {
+      updateBattery(target);
+    };
+    // 电池断开充电时间更新时被调用
+    BatteryManager.ondischargingtimechange = ({ target }: any) => {
+      updateBattery(target);
+    };
+    // 电池电量更新时被调用
+    BatteryManager.onlevelchange = ({ target }: any) => {
+      updateBattery(target);
+    };
+
+    // new Intl.DateTimeFormat('zh', {
+    //   year: 'numeric',
+    //   month: '2-digit',
+    //   day: '2-digit',
+    //   hour: '2-digit',
+    //   minute: '2-digit',
+    //   second: '2-digit',
+    //   hour12: false
+    // }).format(new Date())
+  });
+
+  return {
+    ...toRefs(state),
+    batteryStatus,
+    calcDischargingTime,
+    calcChargingTime,
+  };
+};

+ 23 - 0
src/hooks/useDomWidth.ts

@@ -0,0 +1,23 @@
+import { ref, onMounted, onUnmounted } from 'vue';
+import { debounce } from 'lodash';
+
+/**
+ * description: 获取页面宽度
+ */
+
+export function useDomWidth() {
+  const domWidth = ref(window.innerWidth);
+
+  function resize() {
+    domWidth.value = document.body.clientWidth;
+  }
+
+  onMounted(() => {
+    window.addEventListener('resize', debounce(resize, 80));
+  });
+  onUnmounted(() => {
+    window.removeEventListener('resize', resize);
+  });
+
+  return domWidth;
+}

+ 30 - 0
src/hooks/useOnline.ts

@@ -0,0 +1,30 @@
+import { ref, onMounted, onUnmounted } from 'vue';
+
+/**
+ * @description 用户网络是否可用
+ * */
+export function useOnline() {
+  const online = ref(true);
+
+  const showStatus = (val: any) => {
+    online.value = typeof val == 'boolean' ? val : val.target.online;
+  };
+
+  // 在页面加载后,设置正确的网络状态
+  navigator.onLine ? showStatus(true) : showStatus(false);
+
+  onMounted(() => {
+    // 开始监听网络状态的变化
+    window.addEventListener('online', showStatus);
+
+    window.addEventListener('offline', showStatus);
+  });
+  onUnmounted(() => {
+    // 移除监听网络状态的变化
+    window.removeEventListener('online', showStatus);
+
+    window.removeEventListener('offline', showStatus);
+  });
+
+  return { online };
+}

+ 55 - 0
src/hooks/useTime.ts

@@ -0,0 +1,55 @@
+import { ref, onMounted, onUnmounted } from 'vue'
+
+/**
+ * @description 获取本地时间
+ */
+export function useTime() {
+  let timer: any // 定时器
+  const year = ref(0) // 年份
+  const month = ref(0) // 月份
+  const week = ref('') // 星期几
+  const day = ref(0) // 天数
+  const hour = ref<number | string>(0) // 小时
+  const minute = ref<number | string>(0) // 分钟
+  const second = ref(0) // 秒
+
+  // 更新时间
+  const updateTime = () => {
+    const date = new Date()
+    year.value = date.getFullYear()
+    month.value = date.getMonth() + 1
+    week.value = '日一二三四五六'.charAt(date.getDay())
+    day.value = date.getDate()
+    hour.value =
+      (date.getHours() + '')?.padStart(2, '0') ||
+      new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getHours())
+    minute.value =
+      (date.getMinutes() + '')?.padStart(2, '0') ||
+      new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getMinutes())
+    second.value = date.getSeconds()
+  }
+
+  // 原生时间格式化
+  // new Intl.DateTimeFormat('zh', {
+  //     year: 'numeric',
+  //     month: '2-digit',
+  //     day: '2-digit',
+  //     hour: '2-digit',
+  //     minute: '2-digit',
+  //     second: '2-digit',
+  //     hour12: false
+  // }).format(new Date())
+
+  updateTime()
+
+  onMounted(() => {
+    clearInterval(timer)
+    timer = setInterval(() => updateTime(), 1000)
+  })
+
+  onUnmounted(() => {
+    clearInterval(timer)
+  })
+
+  return { month, day, hour, minute, second, week }
+}

+ 124 - 0
src/hooks/web/useECharts.ts

@@ -0,0 +1,124 @@
+import type { EChartsOption } from 'echarts';
+import type { Ref } from 'vue';
+
+import { useTimeoutFn } from '@/hooks/core/useTimeout';
+import { Fn, tryOnUnmounted } from '@vueuse/core';
+import { unref, nextTick, watch, computed, ref } from 'vue';
+import { useDebounceFn } from '@vueuse/core';
+import { useEventListener } from '@/hooks/event/useEventListener';
+
+import echarts from '@/utils/lib/echarts';
+
+import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
+import { useBreakpoint } from '@/hooks/event/useBreakpoint';
+export function useECharts(
+  elRef: Ref<HTMLDivElement>,
+  theme: 'light' | 'dark' | 'default' = 'default'
+) {
+  const { getDarkTheme: getSysDarkTheme } = useDesignSetting();
+
+  const getDarkTheme = computed(() => {
+    const sysTheme: string = getSysDarkTheme.value ? 'dark' : 'light';
+    return theme === 'default' ? sysTheme : theme;
+  });
+
+  let chartInstance: echarts.ECharts | null = null;
+  let resizeFn: Fn = resize;
+  const cacheOptions = ref({});
+
+  // eslint-disable-next-line @typescript-eslint/no-empty-function
+  let removeResizeFn: Fn = () => {};
+  resizeFn = useDebounceFn(resize, 200);
+
+  const getOptions = computed((): EChartsOption => {
+    if (getDarkTheme.value !== 'dark') {
+      return cacheOptions.value;
+    }
+    return {
+      backgroundColor: 'transparent',
+      ...cacheOptions.value
+    };
+  });
+
+  function initCharts(t = theme) {
+    const el = unref(elRef);
+    if (!el || !unref(el)) {
+      return;
+    }
+
+    chartInstance = echarts.init(el, t);
+    const { removeEvent } = useEventListener({
+      el: window,
+      name: 'resize',
+      listener: resizeFn
+    });
+    removeResizeFn = removeEvent;
+    const { widthRef, screenEnum } = useBreakpoint();
+    if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
+      useTimeoutFn(() => {
+        resizeFn();
+      }, 30);
+    }
+  }
+
+  function setOptions(options: EChartsOption, clear = true) {
+    cacheOptions.value = options;
+    if (unref(elRef)?.offsetHeight === 0) {
+      useTimeoutFn(() => {
+        setOptions(unref(getOptions));
+      }, 30);
+      return;
+    }
+    nextTick(() => {
+      useTimeoutFn(() => {
+        if (!chartInstance) {
+          initCharts(getDarkTheme.value as 'default');
+
+          if (!chartInstance) return;
+        }
+        clear && chartInstance?.clear();
+
+        chartInstance?.setOption(unref(getOptions));
+      }, 30);
+    });
+  }
+
+  function resize() {
+    chartInstance?.resize();
+  }
+
+  watch(
+    () => getDarkTheme.value,
+    theme => {
+      if (chartInstance) {
+        chartInstance.dispose();
+        initCharts(theme as 'default');
+        setOptions(cacheOptions.value);
+      }
+    }
+  );
+
+  tryOnUnmounted(disposeInstance);
+
+  function getInstance(): echarts.ECharts | null {
+    if (!chartInstance) {
+      initCharts(getDarkTheme.value as 'default');
+    }
+    return chartInstance;
+  }
+
+  function disposeInstance() {
+    if (!chartInstance) return;
+    removeResizeFn();
+    chartInstance.dispose();
+    chartInstance = null;
+  }
+
+  return {
+    setOptions,
+    resize,
+    echarts,
+    getInstance,
+    disposeInstance
+  };
+}

+ 63 - 0
src/hooks/web/usePage.ts

@@ -0,0 +1,63 @@
+import type { RouteLocationRaw, Router } from 'vue-router'
+
+import { PageEnum } from '@/enums/pageEnum'
+import { RedirectName } from '@/router/constant'
+
+import { useRouter } from 'vue-router'
+import { isString } from '@/utils/is'
+import { unref } from 'vue'
+
+export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum }
+
+function handleError(e: Error) {
+  console.error(e)
+}
+
+/**
+ * 页面切换
+ */
+export function useGo(_router?: Router) {
+  let router
+  if (!_router) {
+    router = useRouter()
+  }
+  const { push, replace }: any = _router || router
+
+  function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
+    if (!opt) {
+      return
+    }
+    if (isString(opt)) {
+      isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError)
+    } else {
+      const o = opt as RouteLocationRaw
+      isReplace ? replace(o).catch(handleError) : push(o).catch(handleError)
+    }
+  }
+  return go
+}
+
+/**
+ * 重做当前页面
+ */
+export const useRedo = (_router?: Router) => {
+  const { push, currentRoute } = _router || useRouter()
+  const { query, params = {}, name, fullPath } = unref(currentRoute.value)
+  function redo(): Promise<boolean> {
+    return new Promise((resolve) => {
+      if (name === RedirectName) {
+        resolve(false)
+        return
+      }
+      if (name && Object.keys(params).length > 0) {
+        params['_redirect_type'] = 'name'
+        params['path'] = String(name)
+      } else {
+        params['_redirect_type'] = 'path'
+        params['path'] = fullPath
+      }
+      push({ name: RedirectName, params, query }).then(() => resolve(true))
+    })
+  }
+  return redo
+}

+ 52 - 0
src/hooks/web/usePermission.ts

@@ -0,0 +1,52 @@
+import { useUserStore } from '@/store/modules/user';
+
+export function usePermission() {
+  const userStore = useUserStore();
+
+  /**
+   * 检查权限
+   * @param accesses
+   */
+  function _somePermissions(accesses: string[]) {
+    return userStore.getPermissions.some((item) => {
+      const { value }: any = item;
+      return accesses.includes(value);
+    });
+  }
+
+  /**
+   * 判断是否存在权限
+   * 可用于 v-if 显示逻辑
+   * */
+  function hasPermission(accesses: string[]): boolean {
+    if (!accesses || !accesses.length) return true;
+    return _somePermissions(accesses);
+  }
+
+  /**
+   * 是否包含指定的所有权限
+   * @param accesses
+   */
+  function hasEveryPermission(accesses: string[]): boolean {
+    const permissionsList = userStore.getPermissions;
+    if (Array.isArray(accesses)) {
+      return permissionsList.every((access: any) => accesses.includes(access.value));
+    }
+    throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`);
+  }
+
+  /**
+   * 是否包含其中某个权限
+   * @param accesses
+   * @param accessMap
+   */
+  function hasSomePermission(accesses: string[]): boolean {
+    const permissionsList = userStore.getPermissions;
+    if (Array.isArray(accesses)) {
+      return permissionsList.some((access: any) => accesses.includes(access.value));
+    }
+    throw new Error(`[hasSomePermission]: ${accesses} should be a array !`);
+  }
+
+  return { hasPermission, hasEveryPermission, hasSomePermission };
+}

+ 40 - 0
src/store/modules/designSetting.ts

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia';
+import { store } from '@/store';
+import designSetting from '@/settings/designSetting';
+
+const { darkTheme, appTheme, appThemeList } = designSetting;
+
+interface DesignSettingState {
+  //深色主题
+  darkTheme: boolean;
+  //系统风格
+  appTheme: string;
+  //系统内置风格
+  appThemeList: string[];
+}
+
+export const useDesignSettingStore = defineStore({
+  id: 'app-design-setting',
+  state: (): DesignSettingState => ({
+    darkTheme,
+    appTheme,
+    appThemeList
+  }),
+  getters: {
+    getDarkTheme(): boolean {
+      return this.darkTheme;
+    },
+    getAppTheme(): string {
+      return this.appTheme;
+    },
+    getAppThemeList(): string[] {
+      return this.appThemeList;
+    }
+  },
+  actions: {}
+});
+
+// Need to be used outside the setup
+export function useDesignSettingWithOut() {
+  return useDesignSettingStore(store);
+}

+ 55 - 0
src/utils/lib/echarts.ts

@@ -0,0 +1,55 @@
+import * as echarts from 'echarts/core';
+
+import {
+  BarChart,
+  LineChart,
+  PieChart,
+  MapChart,
+  PictorialBarChart,
+  RadarChart,
+} from 'echarts/charts';
+
+import {
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  PolarComponent,
+  AriaComponent,
+  ParallelComponent,
+  LegendComponent,
+  RadarComponent,
+  ToolboxComponent,
+  DataZoomComponent,
+  VisualMapComponent,
+  TimelineComponent,
+  CalendarComponent,
+  GraphicComponent
+} from 'echarts/components';
+
+import { SVGRenderer } from 'echarts/renderers';
+
+echarts.use([
+  LegendComponent,
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  PolarComponent,
+  AriaComponent,
+  ParallelComponent,
+  BarChart,
+  LineChart,
+  PieChart,
+  MapChart,
+  RadarChart,
+  SVGRenderer,
+  PictorialBarChart,
+  RadarComponent,
+  ToolboxComponent,
+  DataZoomComponent,
+  VisualMapComponent,
+  TimelineComponent,
+  CalendarComponent,
+  GraphicComponent
+]);
+
+export default echarts;

+ 105 - 0
src/utils/searchs.ts

@@ -0,0 +1,105 @@
+/* eslint-disable no-empty */
+export class Searchs {
+  saveKey = 'searchs';
+
+  initSearch = {
+    form: {},
+    page: {}
+  };
+
+  searchs = {} as any;
+  key = '' as string;
+
+  constructor(key: string) {
+    this.key = key;
+    this.searchs = this.parse();
+  }
+
+  save() {
+    localStorage.setItem(this.saveKey, JSON.stringify(this.searchs));
+  }
+
+  parse() {
+    let json = { ...initSearch };
+    try {
+      const val = localStorage.getItem(this.saveKey) as string;
+      json = JSON.parse(val) || json;
+    } catch (error) {}
+    return json;
+  }
+
+  get(key: string) {
+    const k = key || this.key;
+    if (!this.searchs[k]) {
+      this.searchs[k] = { ...initSearch };
+    }
+    return this.searchs[k];
+  }
+
+  remove(type: 'page' | 'form') {
+    if (this.searchs && this.searchs[this.key]) {
+      type
+        ? delete this.searchs[this.key][type]
+        : delete this.searchs[this.key];
+      this.save();
+    }
+    return this.searchs;
+  }
+
+  getSearchs() {
+    return this.searchs;
+  }
+
+  removeByKey(key: string) {
+    console.log('真正的删', key);
+    delete this.searchs[key];
+    this.save();
+    return this.searchs;
+  }
+  removeAll() {
+    this.searchs = {};
+    localStorage.setItem(this.saveKey, JSON.stringify(this.searchs));
+    return this.searchs;
+  }
+  removeByRouter(path: string) {
+    this.searchs = this.parse();
+    for (const key in this.searchs) {
+      // console.log('清除的循环', key, this.searchs[key]?.bind)
+      if (path === key || path === this.searchs[key]?.bind) {
+        console.log('清除的页面', key);
+        this.removeByKey(key);
+      }
+    }
+  }
+
+  removeByOtherRouter(path: string) {
+    this.searchs = this.parse();
+    for (const key in this.searchs) {
+      if (path === key || path === this.searchs[key]?.bind) {
+      } else {
+        this.removeByKey(key);
+      }
+    }
+  }
+
+  update(data: any, key: string | any, type: 'page' | 'form' | 'bind' | '') {
+    this.searchs = this.parse();
+    const k = key || this.key;
+    if (!this.searchs[k]) {
+      this.searchs[k] = { ...initSearch };
+    }
+
+    if (type) {
+      this.searchs[k][type] = data;
+    } else {
+      this.searchs[k] = data;
+    }
+    this.save();
+    return this.searchs;
+  }
+}
+
+const initSearch = {
+  form: {},
+  page: {}
+};

+ 42 - 0
src/views/home/components/study.tsx

@@ -0,0 +1,42 @@
+import { defineComponent } from 'vue';
+import styles from '../index.module.less';
+import bannerPerson from './images/bannerPerson.png';
+import { NIcon, NImage, NDatePicker, NSpace, NButton } from 'naive-ui';
+import CDatePicker from '@/components/CDatePicker';
+import TrainData from './trainData';
+export default defineComponent({
+  name: 'home-study',
+  setup() {
+    return () => (
+      <>
+        <div class={styles.homeStudy}>
+          <div class={styles.homeStudyTitle}>
+            <div class={styles.homeStudyTitleDot}></div>
+            学情
+          </div>
+          <div class={styles.homeStudyInfoList}>
+            <div class={styles.homeStudyInfoTabs}>
+              <div class={[styles.homeStudyInfoTabItem, styles.active]}>
+                训练数据
+              </div>
+              <div class={styles.homeStudyInfoTabItem}>练习数据</div>
+              <div class={styles.homeStudyInfoTabItem}>练习排行</div>
+            </div>
+            <div class={styles.homeStudyInfoDate}>
+              <NSpace>
+                <CDatePicker separator={'-'} type="daterange"></CDatePicker>
+                <NButton type="primary" class={styles.searchBtn}>
+                  搜索
+                </NButton>
+                <NButton type="primary" ghost class={styles.resetBtn}>
+                  重置
+                </NButton>
+              </NSpace>
+            </div>
+          </div>
+          <TrainData></TrainData>
+        </div>
+      </>
+    );
+  }
+});

+ 71 - 0
src/views/home/components/teachList.tsx

@@ -0,0 +1,71 @@
+import { defineComponent } from 'vue';
+import styles from '../index.module.less';
+import TeachGroup from '../modals/teachGroup';
+export default defineComponent({
+  name: 'home-teachList',
+  setup() {
+    const teachList = {
+      '06-18': [
+        {
+          classGroup: '3年级2班',
+          teacherName: '孙忆枫',
+          conent: '人教版二年级上册 | 第七单元 |【歌表演】大雁',
+          image: ''
+        },
+        {
+          classGroup: '3年级2班',
+          teacherName: '孙忆枫',
+          conent: '人教版二年级上册 | 第六单元 |【歌表演】大雁',
+          image: ''
+        }
+      ],
+      '06-17': [
+        {
+          classGroup: '3年级2班',
+          teacherName: '孙忆枫',
+          conent: '人教版二年级上册 | 第五单元 |【歌表演】大雁',
+          image: ''
+        },
+        {
+          classGroup: '3年级2班',
+          teacherName: '孙忆枫',
+          conent: '人教版二年级上册 | 第四单元 |【歌表演】大雁',
+          image: ''
+        }
+      ],
+      '06-16': [
+        {
+          classGroup: '3年级2班',
+          teacherName: '孙忆枫',
+          conent: '人教版二年级上册 | 第三单元 |【歌表演】大雁',
+          image: ''
+        }
+      ]
+      // '06-15': [
+      //   {
+      //     classGroup: '3年级2班',
+      //     teacherName: '孙忆枫',
+      //     conent: '人教版二年级上册 | 第二单元 |【歌表演】大雁',
+      //     image: ''
+      //   }
+      // ],
+      // '06-14': [
+      //   {
+      //     classGroup: '3年级2班',
+      //     teacherName: '孙忆枫',
+      //     conent: '人教版二年级上册 | 第一单元 |【歌表演】大雁',
+      //     image: ''
+      //   }
+      // ]
+    } as any;
+    return () => (
+      <>
+        <div class={styles.teachListWrap}>
+          {Object.keys(teachList).map(key => (
+            <TeachGroup list={teachList[key]} keys={key}></TeachGroup>
+          ))}
+        </div>
+      </>
+    );
+  }
+});

+ 299 - 0
src/views/home/components/trainData.tsx

@@ -0,0 +1,299 @@
+import { Ref, defineComponent, reactive, ref } from 'vue';
+import styles from '../index.module.less';
+import { NNumberAnimation } from 'naive-ui';
+import numeral from 'numeral';
+import { useECharts } from '@/hooks/web/useECharts';
+export default defineComponent({
+  name: 'home-trainData',
+  setup() {
+    const chartRef = ref<HTMLDivElement | null>(null);
+    const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+    const qualifiedFlag = ref(true);
+    const unqualifiedFlag = ref(true);
+    const payForm = reactive({
+      height: '360px',
+      width: '100%',
+      studentNum: 0,
+      paymentAmount: 0,
+      dateList: [
+        '2022-10-10',
+        '2022-10-11',
+        '2022-10-12',
+        '2022-10-13',
+        '2022-10-14',
+        '2022-10-15',
+        '2022-10-16'
+      ],
+      studentList: [22, 23, 25, 26, 27, 6, 7],
+      payInfoList: [100, 200, 300, 450, 330, 200, 10]
+    });
+
+    const setChart = () => {
+      setOptions({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            lineStyle: {
+              width: 2,
+              color: '#A9C7FF'
+            }
+          }
+        },
+        legend: {
+          show: false,
+          selected: {
+            //在这里设置默认展示就ok了
+            合格人数: qualifiedFlag.value,
+            不合格人数: unqualifiedFlag.value
+          }
+        },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          axisLabel: {
+            show: true,
+            interval: 0
+          },
+          data: payForm.dateList
+          // splitLine: {
+          //   show: true,
+          //   lineStyle: {
+          //     width: 2,
+          //     type: 'solid',
+          //     color: 'rgba(226,226,226,0.5)'
+          //   }
+          // }
+          // axisTick: {
+          //   show: false
+          // }
+        },
+        yAxis: [
+          {
+            type: 'value',
+            axisTick: {
+              show: false
+            },
+            splitArea: {
+              show: false,
+              areaStyle: {
+                color: ['rgba(255,255,255,0.2)']
+              }
+            }
+          }
+        ],
+        grid: {
+          left: '1%',
+          right: '1%',
+          top: '2  %',
+          bottom: 0,
+          containLabel: true
+        },
+        series: [
+          {
+            data: payForm.payInfoList,
+            type: 'line',
+            name: '合格人数',
+            symbolSize: 10,
+            symbol: 'circle',
+            smooth: true,
+            itemStyle: {
+              color: '#198CFE',
+              borderColor: '#fff',
+              borderWidth: 3
+            },
+            lineStyle: {
+              width: 2 //设置线条粗细
+            },
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: 'rgba(212, 231, 255, 1)'
+                    // 0% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: 'rgba(221, 235, 254, 0)' // 100% 处的颜色
+                  }
+                ]
+              }
+            },
+            emphasis: {
+              disabled: true
+            }
+          },
+          {
+            // smooth: true,
+            data: payForm.studentList,
+            symbolSize: 10,
+            type: 'line',
+            name: '不合格人数',
+            symbol: 'circle',
+            smooth: true,
+            itemStyle: {
+              color: '#FF7AA7',
+              borderColor: '#fff',
+              borderWidth: 3
+            },
+            lineStyle: {
+              width: 3 //设置线条粗细
+            },
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: 'rgba(255, 243, 246, 1)'
+                    // 0% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    // 100% 处的颜色
+                    color: 'rgba(255, 246, 248, 0)'
+                  }
+                ]
+              }
+            },
+            emphasis: {
+              disabled: true
+            }
+          }
+        ],
+
+        formatter: (item: any) => {
+          if (Array.isArray(item)) {
+            return [
+              item[0].axisValueLabel,
+              ...item.map(
+                (d: any) =>
+                  `<br/>${
+                    d.marker
+                  }<span style="margin-top:10px;margin-left:5px;font-size: 13px;font-weight: 500;
+                  color: #333333;
+                  line-height: 18px;">${d.seriesName}: ${
+                    d.value
+                  }${'人'} </span>`
+              )
+            ].join('');
+          } else {
+            return item;
+          }
+        }
+        // dataZoom: [
+        //   {
+        //     type: 'slider',
+        //     start: 5,
+        //     end: 100,
+        //     filterMode: 'empty'
+        //   }
+        // ]
+      });
+    };
+    setChart();
+    return () => (
+      <>
+        <div class={styles.homeTrainData}>
+          <div class={styles.TrainDataTop}>
+            <div class={styles.TrainDataTopLeft}>
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <span>
+                    <NNumberAnimation from={0} to={6}></NNumberAnimation>
+                  </span>
+                  次
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>训练次数</p>
+              </div>
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <span>
+                    <NNumberAnimation from={0} to={100}></NNumberAnimation>
+                  </span>
+                  人次
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>应交总人次</p>
+              </div>
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <span>
+                    <NNumberAnimation from={0} to={40}></NNumberAnimation>
+                  </span>
+                  人次
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>提交总人次</p>
+              </div>
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <span>
+                    {' '}
+                    <NNumberAnimation from={0} to={30}></NNumberAnimation>
+                  </span>
+                  人次
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>合格总人次</p>
+              </div>
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <span>
+                    <NNumberAnimation from={0} to={40}></NNumberAnimation>%
+                  </span>
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>训练提交率</p>
+              </div>
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <span>
+                    <NNumberAnimation from={0} to={30}></NNumberAnimation>%
+                  </span>
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>训练合格率</p>
+              </div>
+            </div>
+            <div class={styles.TrainDataTopRight}>
+              <div
+                onClick={() => {
+                  qualifiedFlag.value = !qualifiedFlag.value;
+                  setChart();
+                }}
+                class={[
+                  styles.DataTopRightItem,
+                  qualifiedFlag.value ? '' : styles.DataTopRightItemDis
+                ]}>
+                <div class={styles.DataTopRightDot}></div>
+                <p>合格人数</p>
+              </div>
+              <div
+                onClick={() => {
+                  unqualifiedFlag.value = !unqualifiedFlag.value;
+                  setChart();
+                }}
+                class={[
+                  styles.DataTopRightItem,
+                  unqualifiedFlag.value ? '' : styles.DataTopRightItemDis
+                ]}>
+                <div class={[styles.DataTopRightDot, styles.red]}></div>
+                <p>不合格人数</p>
+              </div>
+            </div>
+          </div>
+          <div class={styles.chatrs}>
+            <div
+              ref={chartRef}
+              style={{ height: payForm.height, width: payForm.width }}></div>
+          </div>
+        </div>
+      </>
+    );
+  }
+});

二进制
src/views/home/images/boxIcon.png


二进制
src/views/home/images/cloundIcon.png


二进制
src/views/home/images/goClass.png


二进制
src/views/home/images/headerD.png


+ 439 - 5
src/views/home/index.module.less

@@ -1,16 +1,19 @@
 .homeWrap {
   display: flex;
   flex-direction: row;
-  align-items: center;
+  // align-items: center;
   justify-content: space-between;
+  align-content: stretch;
   .homeInfoLeft {
-    height: 500px;
+    display: flex;
+    flex-direction: column;
     width: 1286px;
     .homeBanner {
       height: 246px;
       border-radius: 20px;
       background-color: #d5e9ff;
       position: relative;
+      margin-bottom: 20px;
       .bannerPerson {
         width: 327px;
         height: 278px;
@@ -26,7 +29,6 @@
         bottom: 45px;
         right: 195px;
         font-size: 18px;
-
         font-weight: 400;
         color: rgba(0, 0, 0, 0.65);
         line-height: 25px;
@@ -44,12 +46,444 @@
         .bannerRed {
           color: #f24a6c;
         }
+        .bannerBtn {
+          width: 154px;
+          height: 43px;
+          background: #3583fa;
+          border-radius: 22px;
+          font-size: 18px;
+          line-height: 43px;
+          font-weight: 600;
+          color: #ffffff;
+          letter-spacing: 1px;
+          display: flex;
+          flex-direction: row;
+          align-items: center;
+          justify-content: center;
+          margin-top: 25px;
+          cursor: pointer;
+          &:hover {
+            opacity: 0.8;
+          }
+          .rotueRight {
+            font-size: 14px;
+            margin-left: 4px;
+          }
+        }
+      }
+      .leftDot {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 141px;
+        height: 141px;
+      }
+      .rightDot {
+        position: absolute;
+        right: 0;
+        bottom: 0;
+        width: 158px;
+        height: 137px;
+      }
+      .lineIcon {
+        position: absolute;
+        z-index: 500;
+        width: 48px;
+        height: 48px;
+        left: 96px;
+        top: 89px;
+      }
+      .musicIcon {
+        position: absolute;
+        width: 51px;
+        height: 46px;
+        left: 400px;
+        top: 36px;
       }
     }
   }
   .homeInfoRight {
-    height: 500px;
-
     width: 450px;
   }
 }
+
+// 学情
+.homeStudy {
+  background-color: #fff;
+
+  .homeStudyTitle {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    font-size: 20px;
+
+    font-weight: 600;
+    color: #131415;
+    line-height: 28px;
+    .homeStudyTitleDot {
+      width: 5px;
+      height: 16px;
+      background: #198cfe;
+      margin-right: 8px;
+    }
+  }
+  .homeStudyInfoList {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 21px;
+    .homeStudyInfoTabs {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      .homeStudyInfoTabItem {
+        cursor: pointer;
+        width: 112px;
+        height: 39px;
+        border-radius: 20px;
+        font-size: 18px;
+        font-weight: 600;
+        line-height: 39px;
+        text-align: center;
+        margin-right: 24px;
+        background: #f5f6fa;
+        color: rgba(0, 0, 0, 0.5);
+        &:hover {
+          background: #198cfe;
+          color: #ffffff;
+          opacity: 0.8;
+        }
+      }
+      .homeStudyInfoTabItem.active {
+        background: #198cfe;
+        color: #ffffff;
+      }
+    }
+    .homeStudyInfoDate {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: flex-end;
+    }
+  }
+  .searchBtn {
+    width: 90px;
+    height: 43px;
+    background: #198cfe;
+    border-radius: 8px;
+    line-height: 43px;
+    font-weight: 600 !important;
+    font-size: 18px;
+  }
+  .resetBtn {
+    width: 90px;
+    height: 43px;
+    border-radius: 8px;
+    line-height: 43px;
+    font-weight: 600 !important;
+    font-size: 18px;
+  }
+}
+
+.homeTrainData {
+  margin-top: 40px;
+  .TrainDataTop {
+    margin-bottom: 40px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    .TrainDataTopLeft {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      .TrainDataItem {
+        margin-right: 40px;
+        .TrainDataItemTitle {
+          text-align: center;
+          font-size: 13px;
+          font-weight: 400;
+          color: #777777;
+          line-height: 18px;
+          span {
+            font-size: 24px;
+            font-weight: bold;
+            color: #131415;
+            line-height: 28px;
+          }
+        }
+        .TrainDataItemsubTitle {
+          margin-top: 4px;
+          text-align: center;
+          font-size: 13px;
+          font-family: PingFangSC-Regular, PingFang SC;
+          font-weight: 400;
+          color: #777777;
+          line-height: 18px;
+        }
+      }
+    }
+    .TrainDataTopRight {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      .DataTopRightItem {
+        cursor: pointer;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        margin-left: 30px;
+        &:hover {
+          opacity: 0.8;
+        }
+
+        .DataTopRightDot {
+          width: 16px;
+          height: 16px;
+          background: #3583fa;
+          border-radius: 4px;
+          margin-right: 6px;
+        }
+        .DataTopRightDot.red {
+          background: #ff7aa7;
+        }
+      }
+      .DataTopRightItem.DataTopRightItemDis {
+        .DataTopRightDot {
+          background: #f5f6fa;
+        }
+      }
+    }
+  }
+}
+.leftBottomWrap {
+  padding: 20px 32px;
+  border-radius: 20px;
+  background-color: #fff;
+  flex: 1;
+  .tableWrap {
+    margin-top: 40px;
+    :global {
+      .n-data-table {
+        border-radius: 10px 10px 0 0;
+        overflow: hidden;
+      }
+      .n-data-table-thead {
+        height: 54px;
+        line-height: 54px;
+      }
+      .n-data-table-th {
+        padding: 0 20px;
+        background-color: #f7f7f8;
+        color: rgba(0, 0, 0, 0.88);
+      }
+      .n-data-table-th__title-wrapper {
+        &::after {
+          content: '';
+          width: 1px;
+          height: 22px;
+          background: #ebebeb;
+          &:nth-last-child(1) {
+            display: none;
+          }
+        }
+      }
+      .n-data-table-th--last {
+        .n-data-table-th__title-wrapper {
+          &::after {
+            content: '';
+            width: 0px;
+            height: 22px;
+            background: #ebebeb;
+          }
+        }
+      }
+    }
+  }
+}
+
+.homeInfoRight {
+  display: flex;
+  flex-direction: column;
+  .homeInfoRightTop {
+    border-radius: 20px;
+    background-color: #fff;
+    padding: 30px 20px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    .HeaderWrap {
+      position: relative;
+      .headerD {
+        width: 237px;
+        height: 142px;
+      }
+      .defultHeade {
+        width: 116px;
+        height: 116px;
+        overflow: hidden;
+        border-radius: 50%;
+        position: absolute;
+        top: 13px;
+        left: 61px;
+      }
+    }
+    .headerInfo {
+      .headerTitle {
+        font-size: 20px;
+        font-weight: 600;
+        color: #131415;
+        line-height: 28px;
+        letter-spacing: 1px;
+        margin: 18px 0 8px;
+        text-align: center;
+      }
+      .headerSubTitle {
+        font-size: 14px;
+        font-weight: 400;
+        color: #707a92;
+        line-height: 20px;
+      }
+    }
+    .quickEnter {
+      width: 100%;
+      margin-top: 30px;
+      .quickList {
+        margin-top: 20px;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-around;
+        .quickItem {
+          cursor: pointer;
+          &:hover {
+            opacity: 0.8;
+          }
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          .quickItemImg {
+            img {
+              width: 48px;
+              height: 48px;
+            }
+          }
+          p {
+            font-size: 14px;
+            font-weight: 600;
+            color: #333333;
+          }
+        }
+      }
+    }
+  }
+  .rightTitle {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    font-weight: 600;
+    color: #131415;
+    font-size: 20px;
+    .titleDot {
+      width: 5px;
+      height: 16px;
+      background: #198cfe;
+      margin-right: 8px;
+    }
+  }
+  .rightTeachingWrap {
+    overflow: hidden;
+    flex: 1;
+    background-color: #fff;
+    padding: 20px;
+    border-radius: 20px;
+    margin-top: 20px;
+  }
+}
+.teachListWrap {
+}
+.teachGroup {
+  margin-top: 12px;
+  .teachGroupTitle {
+    font-size: 14px;
+    font-weight: 400;
+    color: #aaaaaa;
+    width: 40px;
+    text-align: center;
+    margin-bottom: 12px;
+  }
+  .teachGroupList {
+    padding-bottom: 12px;
+    margin-left: 20px;
+    border-left: 1px solid #d1e8ff;
+    min-height: 92px;
+    position: relative;
+    .teachGroupListDot {
+      width: 12px;
+      height: 12px;
+      background: #198cfe;
+      border: 3px solid #d1e8ff;
+      border-radius: 50%;
+      top: 31px;
+      left: -6px;
+      position: absolute;
+    }
+  }
+  .teachGroupItemWrap {
+    margin-left: 28px;
+    background: #f7f9ff;
+    border-radius: 12px;
+    display: flex;
+    flex-direction: row;
+    align-items: top;
+    &:nth-last-of-type(1) {
+      margin-bottom: 0;
+    }
+    margin-bottom: 12px;
+    padding: 10px;
+    .teachGroupItemLeft {
+      margin-right: 12px;
+      width: 50px;
+      height: 50px;
+      border-radius: 50%;
+      overflow: hidden;
+      border: 2px solid #198cfe;
+      .teachGroupItemHeader {
+        border: 2px solid #fff;
+        border-radius: 50%;
+        overflow: hidden;
+        img {
+          width: 44px;
+          height: 44px;
+        }
+      }
+    }
+    .teachGroupItemRight {
+      flex: 1;
+      .teachGroupItemName {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-between;
+        font-size: 16px;
+        margin-top: 2px;
+        font-weight: 600;
+        color: #131415;
+        span {
+          font-size: 13px;
+          font-weight: 400;
+          color: #1677ff;
+          line-height: 18px;
+        }
+      }
+      .teachGroupItemInfo {
+        font-size: 13px;
+        line-height: 18px;
+        color: rgba(0, 0, 0, 0.5);
+      }
+    }
+  }
+}

+ 157 - 3
src/views/home/index.tsx

@@ -1,10 +1,78 @@
-import { defineComponent } from 'vue';
+import { defineComponent, reactive } from 'vue';
 import styles from './index.module.less';
 import bannerPerson from './images/bannerPerson.png';
-import { NImage } from 'naive-ui';
+import { NIcon, NImage, NDataTable } from 'naive-ui';
+import leftDot from './images/leftDot.png';
+import rightDot from './images/rightDot.png';
+import lineIcon from './images/lineIcon.png';
+import musicIcon from './images/musicIcon.png';
+import Study from './components/study';
+import Pagination from '@/components/pagination';
+import headerD from './images/headerD.png';
+import defultHeade from '@/components/layout/images/teacherIcon.png';
+import cloundIcon from './images/cloundIcon.png';
+import goClass from './images/goClass.png';
+import boxIcon from './images/boxIcon.png';
+import TeachList from './components/teachList';
 export default defineComponent({
   name: 'home-page',
   setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      tableList: [] as any
+    });
+    const columns = () => {
+      return [
+        {
+          title: '布置老师',
+          key: 'id'
+        },
+        {
+          title: '布置时间',
+          key: 'id'
+        },
+        {
+          title: '截止时间',
+          key: 'id'
+        },
+        {
+          title: '训练状态',
+          key: 'id'
+        },
+        {
+          title: '布置人数',
+          key: 'id'
+        },
+        {
+          title: '提交人数',
+          key: 'id'
+        },
+        {
+          title: '合格人数',
+          key: 'id'
+        },
+        {
+          title: '提交率',
+          key: 'id'
+        },
+        {
+          title: '合格率',
+          key: 'id'
+        },
+        {
+          title: '操作',
+          key: 'id'
+        }
+      ];
+    };
+    const getList = () => {
+      console.log('1');
+    };
     return () => (
       <div class={styles.homeWrap}>
         <div class={styles.homeInfoLeft}>
@@ -18,10 +86,96 @@ export default defineComponent({
                 <span class={styles.bannerRed}> 3 </span>
                 门课程未布置作业,请及时处理哦!
               </p>
+              <div class={styles.bannerBtn}>
+                立即处理{' '}
+                <NIcon class={styles.rotueRight}>
+                  <svg
+                    xmlns="http://www.w3.org/2000/svg"
+                    xmlns:xlink="http://www.w3.org/1999/xlink"
+                    viewBox="0 0 24 24">
+                    <path
+                      d="M7.38 21.01c.49.49 1.28.49 1.77 0l8.31-8.31a.996.996 0 0 0 0-1.41L9.15 2.98c-.49-.49-1.28-.49-1.77 0s-.49 1.28 0 1.77L14.62 12l-7.25 7.25c-.48.48-.48 1.28.01 1.76z"
+                      fill="currentColor"></path>
+                  </svg>
+                </NIcon>
+              </div>
             </div>
+            <NImage class={styles.leftDot} src={leftDot}></NImage>
+            <NImage class={styles.rightDot} src={rightDot}></NImage>
+            <NImage class={styles.lineIcon} src={lineIcon}></NImage>
+            <NImage class={styles.musicIcon} src={musicIcon}></NImage>
+          </div>
+          <div class={styles.leftBottomWrap}>
+            <Study></Study>
+            <div class={styles.tableWrap}>
+              <NDataTable
+                class={styles.classTable}
+                loading={state.loading}
+                columns={columns()}
+                data={state.tableList}></NDataTable>
+              <Pagination
+                v-model:page={state.pagination.page}
+                v-model:pageSize={state.pagination.rows}
+                v-model:pageTotal={state.pagination.pageTotal}
+                onList={getList}
+                sync
+                saveKey="orchestraRegistration-key"
+              />
+            </div>
+          </div>
+        </div>
+        <div class={styles.homeInfoRight}>
+          <div class={styles.homeInfoRightTop}>
+            <div class={styles.HeaderWrap}>
+              <NImage
+                previewDisabled
+                class={styles.headerD}
+                src={headerD}></NImage>
+              <NImage
+                previewDisabled
+                class={styles.defultHeade}
+                src={defultHeade}></NImage>
+            </div>
+            <div class={styles.headerInfo}>
+              <p class={styles.headerTitle}>张晚意</p>
+              <p class={styles.headerSubTitle}>武汉小学 | 音乐老师</p>
+            </div>
+            <div class={styles.quickEnter}>
+              <h3 class={styles.rightTitle}>
+                <div class={styles.titleDot}></div>快捷入口
+              </h3>
+              <div class={styles.quickList}>
+                <div class={styles.quickItem}>
+                  <NImage
+                    previewDisabled
+                    class={styles.quickItemImg}
+                    src={goClass}></NImage>
+                  <p>开始上课</p>
+                </div>
+                <div class={styles.quickItem}>
+                  <NImage
+                    previewDisabled
+                    class={styles.quickItemImg}
+                    src={cloundIcon}></NImage>
+                  <p>我的资源</p>
+                </div>
+                <div class={styles.quickItem}>
+                  <NImage
+                    previewDisabled
+                    class={styles.quickItemImg}
+                    src={boxIcon}></NImage>
+                  <p>工具箱</p>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class={styles.rightTeachingWrap}>
+            <h3 class={styles.rightTitle}>
+              <div class={styles.titleDot}></div>教学进度
+            </h3>
+            <TeachList></TeachList>
           </div>
         </div>
-        <div class={styles.homeInfoRight}></div>
       </div>
     );
   }

+ 22 - 0
src/views/home/modals/teachGroup.tsx

@@ -0,0 +1,22 @@
+import { defineComponent } from 'vue';
+import styles from '../index.module.less';
+import TeachItem from './teachItem';
+export default defineComponent({
+  props: ['list', 'keys'],
+  name: 'home-teachGroup',
+  setup(props, { emit }) {
+    return () => (
+      <>
+        <div class={styles.teachGroup}>
+          <p class={styles.teachGroupTitle}>{props.keys}</p>
+          <div class={styles.teachGroupList}>
+            <div class={styles.teachGroupListDot}></div>
+            {props.list.map((item: any) => (
+              <TeachItem item={item}></TeachItem>
+            ))}
+          </div>
+        </div>
+      </>
+    );
+  }
+});

+ 30 - 0
src/views/home/modals/teachItem.tsx

@@ -0,0 +1,30 @@
+import { defineComponent } from 'vue';
+import styles from '../index.module.less';
+import { NImage } from 'naive-ui';
+import teacherHeader from '@/components/layout/images/teacherIcon.png';
+export default defineComponent({
+  name: 'home-teachItem',
+  props: ['item'],
+  setup(props, { emit }) {
+    return () => (
+      <>
+        <div class={styles.teachGroupItemWrap}>
+          <div class={styles.teachGroupItemLeft}>
+            <NImage
+              previewDisabled
+              src={props.item.image ? props.item.image : teacherHeader}
+              class={styles.teachGroupItemHeader}
+              object-fit="cover"></NImage>
+          </div>
+          <div class={styles.teachGroupItemRight}>
+            <p class={styles.teachGroupItemName}>
+              {props.item.teacherName} <span>{props.item.classGroup}</span>
+            </p>
+
+            <p class={styles.teachGroupItemInfo}>{props.item.conent}</p>
+          </div>
+        </div>
+      </>
+    );
+  }
+});