Browse Source

第一版

xiaoxin 2 years ago
parent
commit
fdcf3ff9c5
77 changed files with 9192 additions and 1 deletions
  1. 14 0
      .eslintrc.cjs
  2. 28 0
      .gitignore
  3. 8 0
      .prettierrc.json
  4. 3 0
      .vscode/extensions.json
  5. 35 1
      README.md
  6. 13 0
      index.html
  7. 5281 0
      package-lock.json
  8. 31 0
      package.json
  9. BIN
      public/favicon.ico
  10. 45 0
      src/App.vue
  11. BIN
      src/assets/images/address.png
  12. BIN
      src/assets/images/bg.png
  13. BIN
      src/assets/images/bg2.png
  14. BIN
      src/assets/images/bg3.png
  15. BIN
      src/assets/images/book-bottom-bg.png
  16. BIN
      src/assets/images/book-center-bg.png
  17. BIN
      src/assets/images/book-header-bg.png
  18. BIN
      src/assets/images/book-title-bg.png
  19. BIN
      src/assets/images/border-green.png
  20. BIN
      src/assets/images/bottom_bg.png
  21. BIN
      src/assets/images/box.png
  22. BIN
      src/assets/images/center-box-bg.png
  23. BIN
      src/assets/images/center-box-bg2.png
  24. BIN
      src/assets/images/classRoom-bg.png
  25. BIN
      src/assets/images/classRoom-right.png
  26. BIN
      src/assets/images/device.png
  27. BIN
      src/assets/images/finish-box-bg.png
  28. BIN
      src/assets/images/finish-box-bg2.png
  29. BIN
      src/assets/images/finish-box-bg3.png
  30. BIN
      src/assets/images/header_bg.png
  31. BIN
      src/assets/images/house.png
  32. BIN
      src/assets/images/icon-bottom.png
  33. BIN
      src/assets/images/icon-top.png
  34. BIN
      src/assets/images/index-bg.png
  35. BIN
      src/assets/images/off.png
  36. BIN
      src/assets/images/on.png
  37. BIN
      src/assets/images/report-bottom-icon.png
  38. BIN
      src/assets/images/right-box-bg.png
  39. BIN
      src/assets/images/room-center-bg.png
  40. BIN
      src/assets/images/room-form-bg.png
  41. BIN
      src/assets/images/room-header-bg.png
  42. BIN
      src/assets/images/sun.png
  43. BIN
      src/assets/images/title-icon.png
  44. BIN
      src/assets/images/title-icon2.png
  45. BIN
      src/assets/images/title_bg.png
  46. BIN
      src/assets/images/user.png
  47. BIN
      src/assets/images/video-bg.png
  48. BIN
      src/assets/images/video-menu.png
  49. BIN
      src/assets/images/warning.png
  50. 38 0
      src/assets/main.css
  51. 85 0
      src/components/bookRoom/bookRoom-header.vue
  52. 19 0
      src/components/bookRoom/bookRoom-main-center.vue
  53. 343 0
      src/components/bookRoom/bookRoom-main-left.vue
  54. 345 0
      src/components/bookRoom/bookRoom-main-right.vue
  55. 36 0
      src/components/bookRoom/bookRoom-main.vue
  56. 88 0
      src/components/classRoom/classRoom-header.vue
  57. 289 0
      src/components/classRoom/classRoom-main-center.vue
  58. 525 0
      src/components/classRoom/classRoom-main-left.vue
  59. 255 0
      src/components/classRoom/classRoom-main-right.vue
  60. 24 0
      src/components/classRoom/classRoom-main.vue
  61. 142 0
      src/components/dorm/dorm-header.vue
  62. 349 0
      src/components/dorm/dorm-main-center.vue
  63. 403 0
      src/components/dorm/dorm-main-left.vue
  64. 469 0
      src/components/dorm/dorm-main-right.vue
  65. 37 0
      src/components/dorm/dorm-main.vue
  66. 27 0
      src/main.js
  67. 33 0
      src/router/index.js
  68. 12 0
      src/utils/countUpNum.js
  69. 13 0
      src/utils/getNearSevenDay.js
  70. 18 0
      src/utils/getNearSevenMonth.js
  71. 15 0
      src/utils/getRandomNumList.js
  72. 32 0
      src/utils/getTime.js
  73. 14 0
      src/utils/isToday.js
  74. 25 0
      src/views/BookroomView.vue
  75. 26 0
      src/views/ClassroomView.vue
  76. 58 0
      src/views/DormView.vue
  77. 14 0
      vite.config.js

+ 14 - 0
.eslintrc.cjs

@@ -0,0 +1,14 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-prettier/skip-formatting'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "none"
+}

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 35 - 1
README.md

@@ -1 +1,35 @@
-#wanzai_smarischool
+# H5-equipmentManagementPlatform
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

File diff suppressed because it is too large
+ 5281 - 0
package-lock.json


+ 31 - 0
package.json

@@ -0,0 +1,31 @@
+{
+  "name": "h5-equipmentmanagementplatform",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "axios": "^1.4.0",
+    "countup.js": "^2.8.0",
+    "echarts": "^5.4.2",
+    "element-plus": "^2.3.7",
+    "sass": "^1.63.6",
+    "sass-loader": "^13.3.2",
+    "vue": "^3.3.4",
+    "vue-router": "^4.2.2"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.2.0",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "@vue/eslint-config-prettier": "^7.1.0",
+    "eslint": "^8.39.0",
+    "eslint-plugin-vue": "^9.11.0",
+    "prettier": "^2.8.8",
+    "vite": "^4.3.9"
+  }
+}

BIN
public/favicon.ico


+ 45 - 0
src/App.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="main">
+    <el-carousel height="1080px" :autoplay="false" @change="change">
+      <!-- 智慧宿舍区域 -->
+      <el-carousel-item>
+        <DormView />
+      </el-carousel-item>
+
+      <!-- 智慧教室区域 -->
+      <el-carousel-item>
+        <ClassroomView />
+      </el-carousel-item>
+
+      <!-- 智慧书房区域 -->
+      <el-carousel-item>
+        <BookroomView />
+      </el-carousel-item>
+    </el-carousel>
+  </div>
+</template>
+
+<script setup>
+import { ref, provide } from 'vue'
+import BookroomView from '@/views/BookroomView.vue'
+import ClassroomView from '@/views/ClassroomView.vue'
+import DormView from '@/views/DormView.vue'
+
+// 当前激活索引
+const activeIndex = ref(0)
+
+// 切换轮播图回调
+const change = (i) => {
+  activeIndex.value = i
+}
+
+// 将activeIndex传递出去
+provide('activeIndex', activeIndex)
+</script>
+
+<style lang="scss" scoped>
+.main {
+  width: 1920px;
+  height: 1080px;
+}
+</style>

BIN
src/assets/images/address.png


BIN
src/assets/images/bg.png


BIN
src/assets/images/bg2.png


BIN
src/assets/images/bg3.png


BIN
src/assets/images/book-bottom-bg.png


BIN
src/assets/images/book-center-bg.png


BIN
src/assets/images/book-header-bg.png


BIN
src/assets/images/book-title-bg.png


BIN
src/assets/images/border-green.png


BIN
src/assets/images/bottom_bg.png


BIN
src/assets/images/box.png


BIN
src/assets/images/center-box-bg.png


BIN
src/assets/images/center-box-bg2.png


BIN
src/assets/images/classRoom-bg.png


BIN
src/assets/images/classRoom-right.png


BIN
src/assets/images/device.png


BIN
src/assets/images/finish-box-bg.png


BIN
src/assets/images/finish-box-bg2.png


BIN
src/assets/images/finish-box-bg3.png


BIN
src/assets/images/header_bg.png


BIN
src/assets/images/house.png


BIN
src/assets/images/icon-bottom.png


BIN
src/assets/images/icon-top.png


BIN
src/assets/images/index-bg.png


BIN
src/assets/images/off.png


BIN
src/assets/images/on.png


BIN
src/assets/images/report-bottom-icon.png


BIN
src/assets/images/right-box-bg.png


BIN
src/assets/images/room-center-bg.png


BIN
src/assets/images/room-form-bg.png


BIN
src/assets/images/room-header-bg.png


BIN
src/assets/images/sun.png


BIN
src/assets/images/title-icon.png


BIN
src/assets/images/title-icon2.png


BIN
src/assets/images/title_bg.png


BIN
src/assets/images/user.png


BIN
src/assets/images/video-bg.png


BIN
src/assets/images/video-menu.png


BIN
src/assets/images/warning.png


+ 38 - 0
src/assets/main.css

@@ -0,0 +1,38 @@
+body {
+  margin: 0;
+  padding: 0;
+}
+
+/* 定义动画 */
+@keyframes float {
+  50% {
+    transform: translateY(10px);
+  }
+  100% {
+    transform: translateY(0);
+  }
+}
+
+@keyframes float2 {
+  50% {
+    transform: translateY(10px) scale(0.8);
+  }
+  100% {
+    transform:  translateY(0) scale(0.8);
+  }
+}
+
+@keyframes float3 {
+  50% {
+    transform: translateY(10px) scale(0.5);
+  }
+  100% {
+    transform:  translateY(0) scale(0.5);
+  }
+}
+
+@keyframes run {
+  100% {
+    transform: translateY(-100%);
+  }
+}

+ 85 - 0
src/components/bookRoom/bookRoom-header.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="content">
+    <!-- 天气区域 -->
+    <div class="weather">
+      <img class="weather_img" src="@/assets/images/sun.png" />
+      晴 5℃
+    </div>
+    <!-- 顶部标题区域 -->
+    <div class="title">家庭书房智能教育</div>
+    <!-- 时间区域 -->
+    <div class="time">
+      <div class="time_date">{{ currentDate }}</div>
+      <div>{{ currentTime }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getCurrentTime } from '@/utils/getTime.js'
+
+// 当前日期星期
+const currentDate = ref('')
+// 当前时分秒
+const currentTime = ref('')
+
+onMounted(() => {
+  getTimeData()
+})
+
+const getTimeData = () => {
+  currentDate.value = getCurrentTime().currentDate
+  currentTime.value = getCurrentTime().currentTime
+  setInterval(() => {
+    currentDate.value = getCurrentTime().currentDate
+    currentTime.value = getCurrentTime().currentTime
+  }, 1000)
+}
+</script>
+<style lang="scss" scoped>
+.content {
+  position: relative;
+  height: 140px;
+  color: #fff;
+  background-image: url(@/assets/images/book-header-bg.png);
+  background-size: 100% 100%;
+
+  .weather {
+    position: absolute;
+    top: 55px;
+    left: 48px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 16px;
+
+    .weather_img {
+      margin-right: 10px;
+      width: 30px;
+      height: 30px;
+    }
+  }
+
+  .title {
+    position: absolute;
+    top: 48px;
+    left: 808px;
+    font-size: 38px;
+    font-weight: bold;
+  }
+
+  .time {
+    position: absolute;
+    top: 65px;
+    right: 44px;
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+
+    .time_date {
+      margin: 0 15px 0 75px;
+    }
+  }
+}
+</style>

+ 19 - 0
src/components/bookRoom/bookRoom-main-center.vue

@@ -0,0 +1,19 @@
+<template>
+  <div class="content_center">
+    <img class="img" src="@/assets/images/book-center-bg.png" />
+  </div>
+</template>
+
+<script setup></script>
+<style lang="scss" scoped>
+.content_center {
+  z-index: 1;
+  margin: 0 104px 0 131px;
+  width: 612px;
+  .img {
+    margin-top: 34px;
+    height: 753px;
+    animation: float 3s linear infinite;
+  }
+}
+</style>

+ 343 - 0
src/components/bookRoom/bookRoom-main-left.vue

@@ -0,0 +1,343 @@
+<template>
+  <div class="content_left">
+    <!-- 学生基本信息区域 -->
+    <div class="title">学生基本信息</div>
+    <div class="info">
+      <div class="info_photo">
+        <img
+          class="img"
+          src="https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
+        />
+        <div class="photo_icon">
+          <el-icon size="20" color="#2493F1"><ArrowDownBold /></el-icon>
+        </div>
+      </div>
+      <div class="info_form">
+        <div class="form_item">姓名:张晓晓</div>
+        <div class="form_item">学号:18930228199</div>
+        <div class="form_item">班级:8(5)班</div>
+        <div class="form_item">出生年月:1998年5月16日</div>
+        <div class="form_item">标签:干饭达人,运动达人,北方人节约,19岁,小资生活</div>
+      </div>
+    </div>
+
+    <!-- 学习报告区域 -->
+    <div class="title">学习报告</div>
+    <div class="report">
+      <div class="report_top">
+        <!-- 累计天数区域 -->
+        <div class="top_box">
+          <div class="icon_top" ref="addDayDom">{{ addDay }}</div>
+          <div class="icon_bottom">
+            <span>累计天数</span>
+          </div>
+        </div>
+
+        <!-- 连续天数区域 -->
+        <div class="top_box">
+          <div class="icon_top" ref="conDayDom">{{ conDay }}</div>
+          <div class="icon_bottom">
+            <span>连续天数</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="report_bottom">
+        不积路步,无以至千里,不积小流,无以成江海
+        <img class="icon" src="@/assets/images/report-bottom-icon.png" />
+        <img class="icon2" src="@/assets/images/report-bottom-icon.png" />
+      </div>
+    </div>
+
+    <!-- 学生完成情况分布区域 -->
+    <div class="title">学生完成情况分布 <span> (本月)</span></div>
+    <div class="finish">
+      <!-- 总布置数区域 -->
+      <div class="finish_box">
+        <div class="box_img"></div>
+        <div class="box_info">
+          <div class="info_num" ref="totalNumDom">{{ totalNum }}</div>
+          <div class="info_title">总布置数</div>
+        </div>
+      </div>
+
+      <!-- 平均提交率区域 -->
+      <div class="finish_box">
+        <div class="box_img img2"></div>
+        <div class="box_info">
+          <div class="info_num yellow">
+            <span ref="submitNumDom"> {{ submitNum }}</span
+            >%
+          </div>
+          <div class="info_title">平均提交率</div>
+        </div>
+      </div>
+
+      <!-- 平均正确率区域 -->
+      <div class="finish_box">
+        <div class="box_img img3"></div>
+        <div class="box_info">
+          <div class="info_num green">
+            <span ref="correctNumDom">{{ correctNum }}</span
+            >%
+          </div>
+          <div class="info_title">平均正确率</div>
+        </div>
+      </div>
+
+      <!-- 日平均用时区域 -->
+      <div class="finish_box">
+        <div class="box_img"></div>
+        <div class="box_info">
+          <div class="info_num" ref="timeNumDom">{{ timeNum }}分钟</div>
+          <div class="info_title">日平均用时</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { countUpNum } from '@/utils/countUpNum.js'
+
+// 累计天数
+const addDay = ref(321)
+const addDayDom = ref()
+// 连续天数
+const conDay = ref(145)
+const conDayDom = ref()
+
+// 总布置数
+const totalNum = ref(568)
+const totalNumDom = ref()
+// 平均提交率
+const submitNum = ref(52)
+const submitNumDom = ref()
+
+// 平均正确率
+const correctNum = ref(95)
+const correctNumDom = ref()
+
+// 日平均用时
+const timeNum = ref(588)
+const timeNumDom = ref()
+
+onMounted(() => {
+  countUpNum(addDayDom.value, addDay.value)
+  countUpNum(conDayDom.value, conDay.value)
+  countUpNum(totalNumDom.value, totalNum.value)
+  countUpNum(submitNumDom.value, submitNum.value)
+  countUpNum(correctNumDom.value, correctNum.value)
+  countUpNum(timeNumDom.value, timeNum.value)
+})
+</script>
+<style lang="scss" scoped>
+.content_left {
+  z-index: 1;
+  margin-left: 44px;
+  width: 490px;
+  color: #fff;
+
+  .title {
+    box-sizing: border-box;
+    padding-top: 2px;
+    padding-left: 30px;
+    height: 38px;
+    font-size: 16px;
+    font-weight: bold;
+    background-image: url(@/assets/images/book-title-bg.png);
+    background-size: 100% 100%;
+
+    span {
+      font-size: 14px;
+    }
+  }
+
+  .info {
+    display: flex;
+    align-items: center;
+    height: 270px;
+
+    .info_photo {
+      position: relative;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 122px;
+      height: 122px;
+      border-radius: 50%;
+      background-color: #2493f1;
+
+      img {
+        width: 112px;
+        height: 112px;
+        border-radius: 50%;
+        object-fit: cover;
+      }
+
+      .photo_icon {
+        position: absolute;
+        top: 45px;
+        right: -14px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 34px;
+        height: 34px;
+        border-radius: 50%;
+        background-color: #fff;
+      }
+    }
+
+    .info_form {
+      margin-top: 10px;
+      margin-left: 44px;
+      width: 314px;
+      font-size: 14px;
+
+      .form_item {
+        margin-bottom: 8px;
+        line-height: 20px;
+        display: -webkit-box;
+        -webkit-box-orient: vertical;
+        -webkit-line-clamp: 2;
+        overflow: hidden;
+      }
+    }
+  }
+  .report {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-evenly;
+    height: 270px;
+
+    .report_top {
+      display: flex;
+      justify-content: space-evenly;
+      margin-left: 20px;
+      width: 430px;
+
+      .top_box {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 98px;
+
+        .icon_top {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          margin-bottom: -35px;
+          width: 73px;
+          height: 73px;
+          font-size: 18px;
+          font-weight: bold;
+          background-image: url(@/assets/images/icon-top.png);
+          background-size: 100% 100%;
+          animation: float 5s linear infinite;
+        }
+
+        .icon_bottom {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          width: 98px;
+          height: 90px;
+          background-image: url(@/assets/images/icon-bottom.png);
+          background-size: 100% 100%;
+
+          span {
+            margin-top: 10px;
+            padding: 2px 8px;
+            height: 18px;
+            font-size: 12px;
+            border-radius: 9px;
+            background-image: linear-gradient(#12fee0, #07889b);
+          }
+        }
+      }
+    }
+
+    .report_bottom {
+      position: relative;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin-left: 8px;
+      width: 450px;
+      height: 53px;
+      font-size: 16px;
+      background-color: rgba(20, 144, 245, 0.1);
+
+      .icon {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 9px;
+        height: 13px;
+      }
+
+      .icon2 {
+        position: absolute;
+        top: 0;
+        right: 0;
+        width: 9px;
+        height: 13px;
+        transform: rotate(180deg) rotateX(180deg);
+      }
+    }
+  }
+
+  .finish {
+    display: flex;
+    flex-wrap: wrap;
+    height: 250px;
+
+    .finish_box {
+      display: flex;
+      align-items: center;
+      width: 50%;
+      .box_img {
+        margin-left: 32px;
+        width: 66px;
+        height: 66px;
+        background-image: url(@/assets/images/finish-box-bg.png);
+        background-size: cover;
+      }
+
+      .img2 {
+        background-image: url(@/assets/images/finish-box-bg2.png);
+      }
+
+      .img3 {
+        background-image: url(@/assets/images/finish-box-bg3.png);
+      }
+
+      .box_info {
+        margin-left: 20px;
+        height: 66px;
+
+        .info_num {
+          margin-top: 5px;
+          color: #5cb3ff;
+          font-size: 24px;
+        }
+
+        .yellow {
+          color: #ffeb3b;
+        }
+
+        .green {
+          color: #57fff4;
+        }
+
+        .info_title {
+          margin-top: 2px;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 345 - 0
src/components/bookRoom/bookRoom-main-right.vue

@@ -0,0 +1,345 @@
+<template>
+  <div class="content_right">
+    <!-- 战力竞技场区域 -->
+    <div class="title">
+      战力竞技场
+      <span> (本年)</span>
+    </div>
+    <div class="power">
+      <el-table :data="tableData">
+        <el-table-column label="排名" align="center">
+          <template #default="scope">
+            <div :class="scope.$index > 2 ? 'green' : 'gray'">
+              <img class="indexImg" src="@/assets/images/index-bg.png" />
+              NO.{{ scope.$index + 1 }}
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="名称">
+          <template #default="{ row }">
+            <div class="power_name">
+              <img class="power_img" :src="row.imgurl" />
+              {{ row.name }}
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="战力值" align="center" width="220">
+          <template #default="{ row }">
+            <div class="power_value">{{ row.value }}</div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 课程使用占比区域 -->
+    <div class="title">
+      课程使用占比
+      <span> (本月)</span>
+    </div>
+    <div class="use" ref="barChart"></div>
+
+    <!-- 知识点掌握情况区域 -->
+    <div class="title">
+      知识点掌握情况
+      <span> (本月)</span>
+    </div>
+
+    <div class="knowledge" ref="pieChart"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import * as Echarts from 'echarts'
+
+const tableData = ref([
+  {
+    id: 1,
+    name: '朱冬梅',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    value: 35465
+  },
+  {
+    id: 2,
+    name: '朱梅',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    value: 30465
+  },
+  {
+    id: 3,
+    name: '朱一梅',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    value: 30465
+  },
+  {
+    id: 4,
+    name: '朱小梅',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    value: 30465
+  }
+])
+
+// 课程使用占比图表实例
+let myBarChart
+// 课程使用占比图表DOM
+const barChart = ref(null)
+
+// 知识点掌握情况图表实例
+let myPieChart
+// 知识点掌握情况图表DOM
+const pieChart = ref(null)
+
+onMounted(() => {
+  myBarChart = Echarts.init(barChart.value)
+  initBarChart()
+
+  myPieChart = Echarts.init(pieChart.value)
+  initPieChart()
+})
+
+// 初始化课程使用占比图表
+const initBarChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    grid: {
+      left: '8%',
+      right: '4%',
+      bottom: '18%'
+    },
+    xAxis: {
+      type: 'category',
+      data: ['人文学', '社会科学', '教育科学', '艺术学', '理化与材料', '资源环境学'],
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        margin: 12,
+        interval: 0,
+        fontSize: 13,
+        color: '#fff'
+      }
+    },
+    yAxis: {
+      name: '小时',
+      nameTextStyle: {
+        fontSize: 13,
+        color: '#fff'
+      },
+      type: 'value',
+      axisLabel: {
+        color: '#fff'
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: 'rgba(108, 128, 151, 0.1)'
+        }
+      }
+    },
+    series: [
+      {
+        data: [2.1, 1.6, 1.7, 1.9, 1.2, 5.5],
+        type: 'bar',
+        barWidth: '40%',
+        showBackground: true,
+        label: {
+          show: true,
+          position: 'top',
+          distance: 5,
+          color: '#fff'
+        },
+        itemStyle: {
+          color: new Echarts.graphic.LinearGradient(
+            0,
+            0,
+            100,
+            100,
+            [
+              { offset: 0, color: '#EE814C' },
+              { offset: 0.05, color: '#EE814C' },
+              { offset: 0.05, color: 'rgba(0, 58, 255, 0)' },
+              { offset: 1, color: '#159AFF' }
+            ],
+            true
+          )
+        }
+      }
+    ]
+  }
+
+  myBarChart.setOption(options)
+}
+
+// 初始化知识点掌握情况图表
+const initPieChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'item'
+    },
+    title: {
+      text: '知识点掌握率',
+      left: 'center',
+      top: '45%',
+      textStyle: {
+        fontSize: 12,
+        color: '#FFF'
+      },
+      subtext: '56%',
+      subtextStyle: {
+        fontSize: 13,
+        color: '#57FFF4'
+      }
+    },
+    color: ['#21D6F2', '#367BFB', '#B840EB'],
+    series: [
+      {
+        type: 'pie',
+        radius: [50, 90],
+        center: ['50%', '50%'],
+        roseType: 'area',
+        label: {
+          show: true,
+          color: 'inherit',
+          fontSize: 14,
+          fontWeight: 'bold',
+          formatter: (params) => {
+            return params.name + params.value + '个'
+          }
+        },
+        data: [
+          { value: 40, name: '实测已掌握' },
+          { value: 38, name: '实测未掌握' },
+          { value: 32, name: '推测已掌握' }
+        ]
+      }
+    ]
+  }
+
+  myPieChart.setOption(options)
+}
+</script>
+<style lang="scss" scoped>
+.content_right {
+  z-index: 1;
+  width: 490px;
+  color: #fff;
+
+  .title {
+    box-sizing: border-box;
+    padding-top: 2px;
+    padding-left: 30px;
+    height: 38px;
+    font-size: 16px;
+    font-weight: bold;
+    background-image: url(@/assets/images/book-title-bg.png);
+    background-size: 100% 100%;
+
+    span {
+      font-size: 14px;
+    }
+  }
+
+  .power {
+    margin-top: 18px;
+    margin-bottom: 20px;
+    height: 232px;
+    overflow: hidden;
+
+    .green {
+      color: #57fff4;
+    }
+
+    .gray {
+      color: #ff7734;
+    }
+
+    .indexImg {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 8px;
+      height: 8px;
+    }
+
+    .power_name {
+      display: flex;
+      align-items: center;
+      color: #fff;
+      .power_img {
+        margin-right: 10px;
+        width: 30px;
+        height: 30px;
+        border-radius: 50%;
+      }
+    }
+
+    .power_value {
+      font-size: 18px;
+      font-weight: bold;
+      color: #57fff4;
+    }
+  }
+
+  .use {
+    width: 490px;
+    height: 270px;
+  }
+
+  .knowledge {
+    width: 490px;
+    height: 250px;
+  }
+}
+
+/*最外层透明*/
+.power ::v-deep(.el-table),
+.power ::v-deep(.el-table__expanded-cell) {
+  background-color: transparent;
+  color: white;
+  border: none;
+}
+/* 表格内背景颜色 */
+.power ::v-deep(.el-table th),
+.power ::v-deep(.el-table tr),
+.power ::v-deep(.el-table td),
+::v-deep(.el-table th.el-table__cell.is-leaf) {
+  background-color: transparent;
+  color: #5cb3ff;
+  border: none;
+  height: 52px;
+  margin-bottom: 1px;
+}
+
+.power ::v-deep(.el-table td) {
+  border-bottom: 3px solid rgba(000, 000, 000, 0.3);
+  background-color: rgba(20, 144, 245, 0.1);
+}
+
+::v-deep(.el-table__inner-wrapper::before) {
+  height: 0;
+}
+
+::v-deep(.el-table__header) {
+  font-size: 16px;
+  background-color: rgba(20, 144, 245, 0.5);
+}
+
+::v-deep(.el-scrollbar__view) {
+  animation: run 10s linear infinite;
+}
+
+.power ::v-deep(.el-table tbody tr:hover > td) {
+  background-color: rgba(20, 144, 245, 0.1);
+}
+</style>

+ 36 - 0
src/components/bookRoom/bookRoom-main.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="content">
+    <!-- 主体内容左边区域 -->
+    <brMainLeft />
+
+    <!-- 主体内容中间区域 -->
+    <brMainCenter />
+
+    <!-- 主体内容右边区域 -->
+    <brMainRight />
+
+    <!-- 底部图片区域 -->
+    <img class="main_img" src="@/assets/images/book-bottom-bg.png" />
+  </div>
+</template>
+
+<script setup>
+import brMainLeft from '@/components/bookRoom/bookRoom-main-left.vue'
+import brMainCenter from '@/components/bookRoom/bookRoom-main-center.vue'
+import brMainRight from '@/components/bookRoom/bookRoom-main-right.vue'
+</script>
+<style lang="scss" scoped>
+.content {
+  position: relative;
+  display: flex;
+  height: 940px;
+
+  .main_img {
+    position: absolute;
+    left: 308px;
+    bottom: 0;
+    width: 1358px;
+    height: 70px;
+  }
+}
+</style>

+ 88 - 0
src/components/classRoom/classRoom-header.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="content">
+    <!-- 天气区域 -->
+    <div class="weather">
+      <img class="weather_img" src="@/assets/images/sun.png" />
+      晴 5℃
+    </div>
+    <!-- 头部标题区域 -->
+    <div class="title">智慧教室统一管控平台</div>
+    <!-- 时间区域 -->
+    <div class="time">
+      <div class="time_date">{{ currentDate }}</div>
+      <div>{{ currentTime }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getCurrentTime } from '@/utils/getTime.js'
+
+// 当前日期星期
+const currentDate = ref('')
+// 当前时分秒
+const currentTime = ref('')
+
+onMounted(() => {
+  getTimeData()
+})
+
+const getTimeData = () => {
+  currentDate.value = getCurrentTime().currentDate
+  currentTime.value = getCurrentTime().currentTime
+  setInterval(() => {
+    currentDate.value = getCurrentTime().currentDate
+    currentTime.value = getCurrentTime().currentTime
+  }, 1000)
+}
+</script>
+<style lang="scss" scoped>
+.content {
+  position: relative;
+  margin-bottom: 25px;
+  height: 98px;
+  color: #fff;
+  background-image: url(@/assets/images/room-header-bg.png);
+  background-size: 100% 100%;
+
+  .weather {
+    position: absolute;
+    top: 25px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 368px;
+    height: 70px;
+    font-size: 16px;
+
+    .weather_img {
+      margin-right: 10px;
+      width: 30px;
+      height: 30px;
+    }
+  }
+  .title {
+    position: absolute;
+    top: 10px;
+    left: 745px;
+    height: 65px;
+    font-size: 42px;
+    font-weight: bold;
+  }
+  .time {
+    position: absolute;
+    top: 25px;
+    right: 0;
+    display: flex;
+    align-items: center;
+    width: 368px;
+    height: 70px;
+    font-size: 16px;
+
+    .time_date {
+      margin: 0 15px 0 75px;
+    }
+  }
+}
+</style>

+ 289 - 0
src/components/classRoom/classRoom-main-center.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="content_center">
+    <!-- 顶部标题区域 -->
+    <div class="line_bg">
+      <img class="line_icon" src="@/assets/images/warning.png" />
+      <div class="line_type">教学预警</div>
+      <div class="line_status">待处置</div>
+      <div class="line_msg">【班级预警】班级平均预习时间短、出勤率低</div>
+    </div>
+
+    <!-- 监控区域 -->
+    <div class="video">
+      <div class="video_title">
+        监控实时预览
+        <span>/ 1002教室</span>
+      </div>
+
+      <div class="video_box">
+        <!-- 选择教室区域 -->
+        <div class="box_left">
+          <div class="left_list">
+            <!-- 每一个教室区域 -->
+            <div class="room_item" v-for="(item, index) in roomList" :key="index">
+              {{ item }}
+              <img class="item_img" src="@/assets/images/classRoom-right.png" />
+            </div>
+          </div>
+        </div>
+        <!-- 展示监控区域 -->
+        <div class="box_right">
+          <div class="inner_msg">前</div>
+          <div class="inner_video">
+            <span>后</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 教学建议区域 -->
+    <div class="suggest">
+      <div class="suggest_title">教学建议</div>
+      <div class="suggest_list">
+        <div class="list_animation">
+          <div class="list_item" v-for="(item, index) in suggestList" :key="index">
+            <div class="item_num">NO.{{ index + 1 }}</div>
+            <div class="item_msg">
+              {{ item }}
+            </div>
+            <img class="item_img" src="@/assets/images/index-bg.png" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+// 教室数组
+const roomList = ref([
+  '1001',
+  '1002',
+  '1003',
+  '1004',
+  '1005',
+  '1006',
+  '1007',
+  '1008',
+  '1009',
+  '1010'
+])
+// 教学建议数组
+const suggestList = ref([
+  '张老师治学严说,要求严格,能深入了解学生的学习和生活状况,希望能提高声音,增加互动次数。',
+  '计科2020需要提高出勤率/整体上课纪律需要提高。',
+  '李瑞同学多提问/多预习/少玩手机。',
+  '备课时多看几本参考书,多借鉴,备课时注意本课与前后两课之间的衔接,以及本章节与单元的关系。',
+  '上课要是能多互动互动就好了。',
+  '上课要是能多互动互动就好了。',
+  '上课要是能多互动互动就好了。'
+])
+</script>
+<style lang="scss" scoped>
+.content_center {
+  margin: 0 20px 0 26px;
+  width: 915px;
+  height: 934px;
+  color: #fff;
+
+  .line_bg {
+    display: flex;
+    align-items: center;
+    margin: auto;
+    margin-top: 5px;
+    width: 882px;
+    height: 61px;
+    font-size: 20px;
+    background-image: url(@/assets/images/room-center-bg.png);
+    background-size: 100% 100%;
+
+    .line_icon {
+      margin-left: 10px;
+      width: 52px;
+      height: 52px;
+    }
+
+    .line_type {
+      margin: 0 40px 0 7px;
+      font-size: 30px;
+      font-weight: bold;
+      color: #ffba1a;
+    }
+
+    .line_status {
+      color: #d43030;
+    }
+
+    .line_msg {
+      margin-left: 5px;
+    }
+  }
+
+  .video {
+    margin-top: 26px;
+    width: 913px;
+    height: 505px;
+    background-image: url(@/assets/images/center-box-bg.png);
+    background-size: 100% 100%;
+
+    .video_title {
+      padding-left: 40px;
+      height: 36px;
+      line-height: 36px;
+      font-size: 20px;
+      font-weight: bold;
+
+      span {
+        font-size: 12px;
+        font-weight: 400;
+      }
+    }
+
+    .video_box {
+      display: flex;
+      justify-content: space-between;
+      box-sizing: border-box;
+      padding: 23px 15px 15px;
+      height: 469px;
+
+      .box_left {
+        width: 233px;
+        background-image: url(@/assets/images/video-menu.png);
+        background-size: 100% 100%;
+
+        .left_list {
+          margin: auto;
+          margin-top: 20px;
+          width: 171px;
+          height: 385px;
+          overflow: auto;
+          overflow-x: hidden;
+
+          .room_item {
+            display: flex;
+            align-items: center;
+            justify-content: space-around;
+            margin-bottom: 8px;
+            width: 171px;
+            height: 35px;
+            font-size: 14px;
+            background-image: url(@/assets/images/classRoom-bg.png);
+            background-size: 100% 100%;
+            cursor: pointer;
+
+            .item_img {
+              width: 16px;
+              height: 16px;
+            }
+          }
+        }
+      }
+
+      .box_right {
+        position: relative;
+        width: 640px;
+        height: 425px;
+        color: #000;
+        font-size: 18px;
+        background-image: url(@/assets/images/video-bg.png);
+        background-size: 100% 100%;
+
+        .inner_msg {
+          margin-top: 14px;
+          margin-left: 18px;
+        }
+
+        .inner_video {
+          position: absolute;
+          bottom: 14px;
+          left: 13px;
+          width: 179px;
+          height: 117px;
+          background-image: url(@/assets/images/video-bg.png);
+          background-size: 100% 100%;
+
+          span {
+            display: inline-block;
+            margin-top: 12px;
+            margin-left: 17px;
+          }
+        }
+      }
+    }
+  }
+
+  .suggest {
+    margin-top: 20px;
+    width: 913px;
+    height: 318px;
+    background-image: url(@/assets/images/center-box-bg2.png);
+    background-size: 100% 100%;
+    overflow: hidden;
+
+    .suggest_title {
+      padding-left: 40px;
+      height: 36px;
+      line-height: 36px;
+      font-size: 20px;
+      font-weight: bold;
+    }
+
+    .suggest_list {
+      margin: auto;
+      margin-top: 18px;
+      width: 855px;
+      height: 255px;
+      overflow: hidden;
+
+      .list_animation {
+        animation: run 15s linear infinite;
+
+        .list_item {
+          display: flex;
+          align-items: center;
+          position: relative;
+          margin-bottom: 8px;
+          height: 45px;
+          background-image: url(@/assets/images/room-form-bg.png);
+          background-size: 100% 100%;
+
+          .item_num {
+            margin: 0 35px 0 24px;
+            font-size: 14px;
+            color: #ff7734;
+          }
+
+          .item_msg {
+            padding-right: 8px;
+            font-size: 13px;
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+          }
+
+          .item_img {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 8px;
+            height: 8px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 滚动条样式
+::-webkit-scrollbar {
+  width: 12px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 3px;
+  height: 10px;
+  background-color: rgba($color: #066d70, $alpha: 0.1);
+}
+</style>

+ 525 - 0
src/components/classRoom/classRoom-main-left.vue

@@ -0,0 +1,525 @@
+<template>
+  <div class="box">
+    <div class="box_title">课程情况</div>
+
+    <!-- 课程情况信息区域 -->
+    <div class="box_info">
+      <div class="info_detail">
+        <div class="detail_title">课程名称</div>
+        <div class="detail_msg">Android开发基础</div>
+        <div class="detail_tip">第五章第三小节</div>
+      </div>
+
+      <div class="info_detail">
+        <div class="detail_title">授课老师</div>
+        <div class="detail_msg">张志华</div>
+      </div>
+
+      <div class="info_detail">
+        <div class="detail_title">上课班级</div>
+        <div class="detail_msg">计科2020</div>
+      </div>
+
+      <div class="info_detail">
+        <div class="detail_title">上课人数</div>
+        <div class="detail_msg" ref="peopleDom">{{ people }}</div>
+      </div>
+    </div>
+
+    <!-- 考勤统计区域 -->
+    <div class="stat">
+      <div class="stat_title">
+        <img src="@/assets/images/title-icon.png" />
+        <img class="icon" src="@/assets/images/title-icon2.png" />
+        考勤统计
+        <span>(本节)</span>
+      </div>
+
+      <div class="stat_chart" ref="statChart"></div>
+      <div class="chart_tip left">
+        <img class="icon" src="@/assets/images/on.png" />
+        已到
+        <span ref="arrivedNumDom">{{ arrivedNum }} </span>
+        人
+      </div>
+      <div class="chart_tip right">
+        <img class="icon" src="@/assets/images/off.png" />
+        缺席
+        <span ref="absenceNumDom">{{ absenceNum }}</span>
+        人
+      </div>
+    </div>
+
+    <!-- 互动次数统计区域 -->
+    <div class="interact">
+      <div class="interact_title">
+        <img src="@/assets/images/title-icon.png" />
+        <img class="icon" src="@/assets/images/title-icon2.png" />
+        互动次数统计
+        <span>(本节)</span>
+      </div>
+
+      <div class="interact_chart" ref="interactChart"></div>
+    </div>
+
+    <!-- 课件预习统计区域 -->
+    <div class="preview">
+      <div class="preview_title">
+        <img src="@/assets/images/title-icon.png" />
+        <img class="icon" src="@/assets/images/title-icon2.png" />
+        课件预习统计
+        <span>(本节)</span>
+      </div>
+
+      <div class="preview_chart" ref="previewChart"></div>
+      <div class="chart_tip2 left">已预习 28人</div>
+      <div class="chart_tip2 right">未预习 28人</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import * as Echarts from 'echarts'
+import { countUpNum } from '@/utils/countUpNum.js'
+
+// 上课人数
+const people = ref(56)
+const peopleDom = ref()
+
+// 考勤统计已到人数
+const arrivedNum = ref(54)
+const arrivedNumDom = ref()
+// 考勤统计缺席人数
+const absenceNum = ref(2)
+const absenceNumDom = ref()
+
+// 考勤率
+const salaryPCT = ref((arrivedNum.value / (arrivedNum.value + absenceNum.value)).toFixed(2) * 100)
+
+// 考勤统计图表实例
+let myStatChart
+// 考勤统计图表DOM
+const statChart = ref(null)
+
+// 互动次数统计图表实例
+let myInteractChart
+// 互动次数统计图表DOM
+const interactChart = ref(null)
+
+// 课件预习统计图表实例
+let myPreviewChart
+// 课件预习统计图表Dom
+const previewChart = ref(null)
+
+onMounted(() => {
+  countUpNum(peopleDom.value, people.value)
+  countUpNum(arrivedNumDom.value, arrivedNum.value)
+  countUpNum(absenceNumDom.value, absenceNum.value)
+
+  myStatChart = Echarts.init(statChart.value)
+  initStatChart()
+
+  myInteractChart = Echarts.init(interactChart.value)
+  initInteractChart()
+
+  myPreviewChart = Echarts.init(previewChart.value)
+  initPreviewChart()
+})
+
+// 初始化考勤统计图表
+const initStatChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'item'
+    },
+    title: {
+      text: '考勤率',
+      left: '40%',
+      top: '36%',
+      textStyle: {
+        fontSize: 12,
+        color: '#fff'
+      },
+      subtext: '{a|' + salaryPCT.value + '%}',
+      subtextStyle: {
+        color: '#28E3E4',
+        rich: {
+          a: {
+            fontSize: 20,
+            padding: [0, 0, 0, -2]
+          }
+        }
+      },
+      textVerticalAlign: 'top',
+      itemGap: 10
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['55%', '70%'],
+        center: ['45%', '48%'],
+        startAngle: 100,
+        label: {
+          show: false
+        },
+        data: [
+          { value: arrivedNum.value, name: '已到' },
+          { value: absenceNum.value, name: '缺席' }
+        ]
+      }
+    ],
+    color: [
+      {
+        type: 'linear',
+        x: 0,
+        y: 1,
+        x2: 0,
+        y2: 0,
+        colorStops: [
+          {
+            offset: 0,
+            color: '#13F5E5FF' // 0% 处的颜色
+          },
+          {
+            offset: 1,
+            color: '#13F5E533' // 100% 处的颜色
+          }
+        ]
+      },
+      '#293F47'
+    ]
+  }
+
+  myStatChart.setOption(options)
+}
+
+// 初始化互动次数统计图表
+const initInteractChart = () => {
+  const options = {
+    title: {
+      text: 18,
+      left: '33%',
+      top: 'center',
+      textStyle: {
+        fontSize: 24,
+        color: '#99FFFF'
+      },
+      subtext: '  次',
+      subtextStyle: {
+        fontSize: 14,
+        color: '#FFF',
+        align: 'center'
+      },
+      textVerticalAlign: 'top',
+      itemGap: 1
+    },
+    tooltip: {
+      trigger: 'item'
+    },
+    legend: {
+      align: 'right',
+      orient: 'vertical',
+      right: '5%',
+      top: 'center',
+      icon: 'circle',
+      textStyle: {
+        color: '#fff'
+      }
+    },
+    color: ['#5CDAF5', '#129BFF', '#4DD1FF', '#FFCF5F', '#4120E6', '#065DFF'],
+    series: [
+      {
+        name: '互动次数',
+        type: 'pie',
+        center: ['38%', '50%'],
+        radius: ['55%', '75%'],
+        avoidLabelOverlap: false,
+        label: {
+          show: true,
+          position: 'outer',
+          alignTo: 'labelLine',
+          color: '#fff',
+          fontSize: 12,
+          lineHeight: 15,
+          formatter: (params) => {
+            return params.value + '次  ' + params.percent + '%'
+          }
+        },
+        labelLine: {
+          show: true
+        },
+        labelLayout: {
+          hideOverlap: false
+        },
+        data: [
+          { value: 5, name: '抢答' },
+          { value: 6, name: '提问' },
+          { value: 3, name: '点名' },
+          { value: 2, name: '讨论' },
+          { value: 1, name: '投票' },
+          { value: 5, name: '其他' }
+        ]
+      }
+    ]
+  }
+
+  myInteractChart.setOption(options)
+}
+
+// 初始化课件预习统计图表
+const initPreviewChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'item'
+    },
+    title: {
+      text: '50%',
+      left: '39%',
+      top: '50%',
+      textStyle: {
+        fontSize: 20,
+        color: '#99FFFF'
+      },
+      subtext: '{a|课件预习率}',
+      subtextStyle: {
+        fontSize: 12,
+        color: '#fff',
+        rich: {
+          a: {
+            padding: [0, 0, 0, -8]
+          }
+        }
+      },
+      textVerticalAlign: 'top',
+      itemGap: 25
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['75%', '90%'],
+        center: ['45%', '75%'],
+        startAngle: 180,
+        label: {
+          show: false
+        },
+        data: [
+          { value: 28, name: '已预习' },
+          { value: 28, name: '未预习' },
+          {
+            value: 28 + 28,
+            itemStyle: {
+              color: 'none',
+              decal: {
+                symbol: 'none'
+              }
+            },
+            label: {
+              show: false
+            }
+          }
+        ]
+      }
+    ],
+    color: [
+      {
+        type: 'linear',
+        x: 0,
+        y: 1,
+        x2: 0,
+        y2: 0,
+        colorStops: [
+          {
+            offset: 0,
+            color: '#008CFF' // 0% 处的颜色
+          },
+          {
+            offset: 1,
+            color: '#00FFF4' // 100% 处的颜色
+          }
+        ]
+      },
+      '#113940'
+    ]
+  }
+
+  myPreviewChart.setOption(options)
+}
+</script>
+<style lang="scss" scoped>
+.box {
+  margin-left: 27px;
+  width: 450px;
+  height: 934px;
+  color: #fff;
+  background-image: url(@/assets/images/box.png);
+  background-size: 100% 100%;
+
+  .box_title {
+    margin-top: 12px;
+    margin-left: 42px;
+    height: 55px;
+    font-size: 20px;
+    font-weight: bold;
+  }
+
+  .box_info {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-evenly;
+    align-content: space-evenly;
+    height: 250px;
+
+    .info_detail {
+      box-sizing: border-box;
+      padding: 18px;
+      width: 176px;
+      height: 96px;
+      background-image: url(@/assets/images/border-green.png);
+      background-size: 100% 100%;
+
+      .detail_title {
+        font-size: 13px;
+      }
+
+      .detail_msg {
+        margin: 8px 0 5px 0;
+        color: #70ecff;
+        font-size: 16px;
+      }
+
+      .detail_tip {
+        color: #70ecff;
+        font-size: 10px;
+      }
+    }
+  }
+
+  .stat {
+    position: relative;
+    margin-left: 35px;
+    .stat_title {
+      height: 21px;
+      font-size: 18px;
+      font-weight: bold;
+      img {
+        width: 12px;
+        height: 12px;
+      }
+
+      .icon {
+        margin-left: -5px;
+        margin-right: 5px;
+      }
+
+      span {
+        font-size: 14px;
+        font-weight: 400;
+      }
+    }
+
+    .stat_chart {
+      margin-top: 2px;
+      width: 415px;
+      height: 160px;
+    }
+
+    .chart_tip {
+      display: flex;
+      align-items: center;
+      position: absolute;
+      top: 90px;
+      font-size: 12px;
+
+      .icon {
+        margin-top: -1px;
+        margin-right: 8px;
+        width: 17px;
+        height: 17px;
+      }
+    }
+
+    .left {
+      left: 35px;
+    }
+
+    .right {
+      left: 275px;
+    }
+  }
+
+  .interact {
+    margin-left: 35px;
+    .interact_title {
+      height: 21px;
+      font-size: 18px;
+      font-weight: bold;
+      img {
+        width: 12px;
+        height: 12px;
+      }
+
+      .icon {
+        margin-left: -5px;
+        margin-right: 5px;
+      }
+
+      span {
+        font-size: 14px;
+        font-weight: 400;
+      }
+    }
+
+    .interact_chart {
+      margin: 22px 0 30px 0;
+      width: 415px;
+      height: 150px;
+    }
+  }
+
+  .preview {
+    position: relative;
+    margin-left: 35px;
+    .preview_title {
+      height: 21px;
+      font-size: 18px;
+      font-weight: bold;
+      img {
+        width: 12px;
+        height: 12px;
+      }
+
+      .icon {
+        margin-left: -5px;
+        margin-right: 5px;
+      }
+
+      span {
+        font-size: 14px;
+        font-weight: 400;
+      }
+    }
+
+    .preview_chart {
+      margin-top: 2px;
+      width: 415px;
+      height: 179px;
+    }
+
+    .chart_tip2 {
+      position: absolute;
+      top: 100px;
+      left: 15px;
+      font-size: 12px;
+    }
+
+    .left {
+      left: 15px;
+    }
+
+    .right {
+      left: 290px;
+    }
+  }
+}
+</style>

+ 255 - 0
src/components/classRoom/classRoom-main-right.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="content_right">
+    <!-- 学情分析学生区域 -->
+    <div class="student">
+      <div class="student_title">学情分析(学生)</div>
+      <div class="student_form">
+        <el-table :data="tableData">
+          <el-table-column label="学情类型" align="center">
+            <template #default="{ row }">
+              <div :class="row.type === '起立' ? 'green' : 'blue'">{{ row.type }}</div>
+            </template>
+          </el-table-column>
+
+          <el-table-column prop="name" label="姓名" align="center" />
+          <el-table-column label="图片" align="center">
+            <template #default="{ row }">
+              <img class="form_img" :src="row.imgurl" />
+            </template>
+          </el-table-column>
+          <el-table-column prop="time" label="时间" align="center" width="130" />
+        </el-table>
+      </div>
+    </div>
+    <!-- 学情分析老师区域 -->
+    <div class="teacher">
+      <div class="teacher_title">学情分析(老师)</div>
+      <div class="teacher_chart" ref="pieChart"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import * as Echarts from 'echarts'
+
+let myPieChart
+const pieChart = ref(null)
+
+const tableData = ref([
+  {
+    id: 1,
+    type: '起立',
+    name: '朱冬梅',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  },
+  {
+    id: 2,
+    type: '听讲',
+    name: '李乐松',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  },
+  {
+    id: 3,
+    type: '阅读',
+    name: '罗鸿文',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  },
+  {
+    id: 4,
+    type: '书写',
+    name: '夏皓君',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  },
+  {
+    id: 5,
+    type: '举手',
+    name: '舒霖',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  },
+  {
+    id: 6,
+    type: '趴桌子',
+    name: '刘景辉',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  },
+  {
+    id: 7,
+    type: '讨论',
+    name: '甘旭彬',
+    imgurl:
+      'https://img1.baidu.com/it/u=1398895526,1958525606&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
+    time: new Date().toLocaleDateString()
+  }
+])
+
+onMounted(() => {
+  myPieChart = Echarts.init(pieChart.value)
+  initPieChart()
+})
+
+const initPieChart = () => {
+  const options = {
+    title: {
+      text: '学情分析', //主标题文本
+      left: 'center',
+      top: '36%',
+      textStyle: {
+        fontSize: 18,
+        color: '#FFA951',
+        align: 'center'
+      }
+    },
+
+    tooltip: {
+      trigger: 'item'
+    },
+    legend: {
+      bottom: '5%',
+      left: 'center',
+      textStyle: {
+        color: '#fff'
+      }
+    },
+    color: ['#3BAC7C', '#014AF6', '#1DADFF', '#808080'],
+    series: [
+      {
+        name: '学情分析',
+        type: 'pie',
+        center: ['50%', '40%'],
+        radius: ['30%', '40%'],
+        label: {
+          show: true,
+          position: 'outside',
+          color: 'inherit',
+          fontSize: 18,
+          lineHeight: 25,
+          formatter: (params) => {
+            return params.percent + '%' + '\n' + '{a|' + params.name + '}'
+          },
+          rich: {
+            a: {
+              color: '#fff',
+              fontSize: 16
+            }
+          }
+        },
+        labelLine: {
+          show: true,
+          length: 25,
+          length2: 30
+        },
+        data: [
+          { value: 1048, name: '离开讲桌' },
+          { value: 735, name: '低头或弯腰操作桌面' },
+          { value: 580, name: '正在授课' },
+          { value: 484, name: '玩手机' }
+        ]
+      }
+    ]
+  }
+
+  myPieChart.setOption(options)
+}
+</script>
+<style lang="scss" scoped>
+.content_right {
+  width: 450px;
+  height: 934px;
+  color: #fff;
+
+  .student {
+    height: 446px;
+    background-image: url(@/assets/images/right-box-bg.png);
+    background-size: 100% 100%;
+    overflow: hidden;
+
+    .student_title {
+      margin-top: 12px;
+      margin-left: 42px;
+      height: 55px;
+      font-size: 20px;
+      font-weight: bold;
+    }
+
+    .student_form {
+      box-sizing: border-box;
+      padding-right: 20px;
+      height: 360px;
+      overflow: hidden;
+
+      .green {
+        color: #54e2a9;
+      }
+
+      .blue {
+        color: #00a0e9;
+      }
+      .form_img {
+        width: 55px;
+        height: 30px;
+      }
+    }
+  }
+
+  .teacher {
+    margin-top: 29px;
+    height: 459px;
+    background-image: url(@/assets/images/right-box-bg.png);
+    background-size: 100% 100%;
+    overflow: hidden;
+
+    .teacher_title {
+      margin-top: 12px;
+      margin-left: 42px;
+      height: 55px;
+      font-size: 20px;
+      font-weight: bold;
+    }
+
+    .teacher_chart {
+      width: 450px;
+      height: 360px;
+    }
+  }
+}
+
+/*最外层透明*/
+.student_form ::v-deep(.el-table),
+.student_form ::v-deep(.el-table__expanded-cell) {
+  background-color: transparent;
+  color: white;
+  border: none;
+}
+/* 表格内背景颜色 */
+.student_form ::v-deep(.el-table th),
+.student_form ::v-deep(.el-table tr),
+.student_form ::v-deep(.el-table td),
+::v-deep(.el-table th.el-table__cell.is-leaf) {
+  background-color: transparent !important;
+  //表格字体颜色
+  color: white;
+  border: none;
+  height: 52px;
+}
+
+::v-deep(.el-table__inner-wrapper::before) {
+  height: 0;
+}
+
+::v-deep(.el-scrollbar__view) {
+  animation: run 12s linear infinite;
+}
+</style>

+ 24 - 0
src/components/classRoom/classRoom-main.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="content">
+    <!-- 主体内容左边区域 -->
+    <crMainLeft />
+
+    <!-- 主体内容中间区域 -->
+    <crMainCenter />
+
+    <!-- 主体内容右边区域 -->
+    <crMainRight />
+  </div>
+</template>
+
+<script setup>
+import crMainLeft from '@/components/classRoom/classRoom-main-left.vue'
+import crMainCenter from '@/components/classRoom/classRoom-main-center.vue'
+import crMainRight from '@/components/classRoom/classRoom-main-right.vue'
+</script>
+<style lang="scss" scoped>
+.content {
+  display: flex;
+  height: 934px;
+}
+</style>

+ 142 - 0
src/components/dorm/dorm-header.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="box">
+    <!-- 左边区域 -->
+    <div class="box_left">
+      <div class="left_time">{{ currentTime }}</div>
+      <div class="left_date">{{ currentDate }}</div>
+    </div>
+
+    <!-- 中间区域 -->
+    <div class="box_center">智慧宿舍可视化大屏</div>
+
+    <!-- 右边区域 -->
+    <div class="box_right">
+      <!-- 天气区域 -->
+      <div class="right_weather">
+        <img class="weather_img" src="@/assets/images/sun.png" />
+        <div class="weather_type">多云</div>
+        <div class="weather_num">28 ℃</div>
+      </div>
+      <!-- 用户头像区域 -->
+      <div class="right_user">
+        <img class="user_img" src="@/assets/images/user.png" />
+        <div class="user_name">王大伟</div>
+      </div>
+      <!-- 下拉区域 -->
+      <el-dropdown trigger="click">
+        <el-icon color="#C0EAFF"><CaretBottom /></el-icon>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item>退出</el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getCurrentTime } from '@/utils/getTime.js'
+
+// 当前日期星期
+const currentDate = ref('')
+// 当前时分秒
+const currentTime = ref('')
+
+onMounted(() => {
+  getTimeData()
+})
+
+const getTimeData = () => {
+  currentDate.value = getCurrentTime().currentDate
+  currentTime.value = getCurrentTime().currentTime
+  setInterval(() => {
+    currentDate.value = getCurrentTime().currentDate
+    currentTime.value = getCurrentTime().currentTime
+  }, 1000)
+}
+</script>
+<style lang="scss" scoped>
+.box {
+  position: relative;
+  z-index: 999;
+  display: flex;
+  padding: 0 30px;
+  height: 100px;
+  color: #c0eaff;
+  background-image: url(@/assets/images/header_bg.png);
+  background-size: 100% 100%;
+  overflow: hidden;
+
+  .box_left {
+    display: flex;
+    align-items: center;
+    margin-top: 8px;
+    height: 42px;
+    .left_time {
+      font-size: 32px;
+    }
+
+    .left_date {
+      margin-left: 20px;
+      font-size: 16px;
+    }
+  }
+
+  .box_center {
+    margin-left: 418px;
+    font-size: 40px;
+    font-weight: bold;
+    color: #fff;
+  }
+
+  .box_right {
+    display: flex;
+    align-items: center;
+    margin-left: 438px;
+    margin-top: 8px;
+    height: 30px;
+
+    .right_weather {
+      display: flex;
+      align-items: center;
+
+      .weather_img {
+        margin-top: 5px;
+        width: 40px;
+        height: 40px;
+        object-fit: cover;
+      }
+
+      .weather_type {
+        margin-left: 8px;
+        font-size: 20px;
+      }
+
+      .weather_num {
+        margin-left: 10px;
+        font-size: 16px;
+      }
+    }
+
+    .right_user {
+      display: flex;
+      align-items: center;
+      margin-left: 44px;
+
+      .user_img {
+        margin-top: 5px;
+        width: 30px;
+        height: 30px;
+        object-fit: cover;
+      }
+
+      .user_name {
+        margin: 0 8px 0 13px;
+        font-size: 16px;
+      }
+    }
+  }
+}
+</style>

+ 349 - 0
src/components/dorm/dorm-main-center.vue

@@ -0,0 +1,349 @@
+<template>
+  <div class="box">
+    <!-- 选择宿舍区域 -->
+    <div class="select">
+      6栋502
+      <el-dropdown trigger="click">
+        <el-icon size="30" color="#fff"><CaretBottom /></el-icon>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item> 选项1 </el-dropdown-item>
+            <el-dropdown-item>选项2</el-dropdown-item>
+            <el-dropdown-item>选项3</el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+
+    <!-- 定位区域 -->
+    <img class="address" src="@/assets/images/address.png" />
+    <div class="student">
+      <div class="student_icon">
+        <div class="icon_bg">
+          <img src="@/assets/images/house.png" />
+        </div>
+      </div>
+      {{ tableData[0].name }}{{ tableData[0].type === 1 ? '(在床)' : '(不在床)' }}
+    </div>
+
+    <img class="address address2" src="@/assets/images/address.png" />
+    <div class="student student2">
+      <div class="student_icon">
+        <div class="icon_bg">
+          <img src="@/assets/images/house.png" />
+        </div>
+      </div>
+      {{ tableData[1].name }}{{ tableData[1].type === 1 ? '(在床)' : '(不在床)' }}
+    </div>
+
+    <img class="address address3" src="@/assets/images/address.png" />
+    <div class="student student3">
+      <div class="student_icon">
+        <div class="icon_bg">
+          <img src="@/assets/images/house.png" />
+        </div>
+      </div>
+      {{ tableData[2].name }}{{ tableData[2].type === 1 ? '(在床)' : '(不在床)' }}
+    </div>
+
+    <img class="address address4" src="@/assets/images/address.png" />
+    <div class="student student4">
+      <div class="student_icon">
+        <div class="icon_bg">
+          <img src="@/assets/images/house.png" />
+        </div>
+      </div>
+      {{ tableData[3].name }}{{ tableData[3].type === 1 ? '(在床)' : '(不在床)' }}
+    </div>
+
+    <img class="address address5" src="@/assets/images/address.png" />
+    <div class="student student5">
+      <div class="student_icon">
+        <div class="icon_bg">
+          <img src="@/assets/images/house.png" />
+        </div>
+      </div>
+      {{ tableData[4].name }}{{ tableData[4].type === 1 ? '(在床)' : '(不在床)' }}
+    </div>
+
+    <img class="address address6" src="@/assets/images/address.png" />
+    <div class="student student6">
+      <div class="student_icon">
+        <div class="icon_bg">
+          <img src="@/assets/images/house.png" />
+        </div>
+      </div>
+      {{ tableData[5].name }}{{ tableData[5].type === 1 ? '(在床)' : '(不在床)' }}
+    </div>
+
+    <!-- 底部数量显示区域 -->
+    <div class="info">
+      <!-- 传感器数量区域 -->
+      <div class="info_box">
+        <img class="info_img" src="@/assets/images/device.png" />
+        <div class="info_detail">
+          <div class="detail_num" ref="deviceNumDom">{{ deviceNum }}</div>
+          <div>传感器数量</div>
+        </div>
+      </div>
+      <!-- 在线数量区域 -->
+      <div class="info_box">
+        <img class="info_img" src="@/assets/images/device.png" />
+        <div class="info_detail">
+          <div class="detail_num" ref="deviceNumOnDom">{{ deviceNumOn }}</div>
+          <div>在线数量</div>
+        </div>
+      </div>
+      <!-- 离线数量区域 -->
+      <div class="info_box">
+        <img class="info_img" src="@/assets/images/device.png" />
+        <div class="info_detail">
+          <div class="detail_num" ref="deviceNumOffDom">{{ deviceNumOff }}</div>
+          <div>离线数量</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getRandomNum } from '@/utils/getRandomNumList.js'
+import { countUpNum } from '@/utils/countUpNum.js'
+import { isToday } from '@/utils/isToday.js'
+
+// 传感器数量
+const deviceNum = ref(6)
+const deviceNumDom = ref()
+
+// 在线数量
+const deviceNumOn = ref(5)
+const deviceNumOnDom = ref()
+
+// 离线数量
+const deviceNumOff = ref(1)
+const deviceNumOffDom = ref()
+
+// 表格数组
+const tableData = ref([
+  {
+    type: getRandomNum(1, 3),
+    date: new Date().toLocaleDateString(),
+    name: '何文羽',
+    college: '计科-2020'
+  },
+  {
+    type: getRandomNum(1, 3),
+    date: new Date().toLocaleDateString(),
+    name: '夏叶轩',
+    college: '计科-2020'
+  },
+  {
+    type: getRandomNum(1, 3),
+    date: new Date().toLocaleDateString(),
+    name: '李芮',
+    college: '计科-2020'
+  },
+  {
+    type: getRandomNum(1, 3),
+    date: new Date().toLocaleDateString(),
+    name: '但学文',
+    college: '计科-2020'
+  },
+  {
+    type: getRandomNum(1, 3),
+    date: new Date().toLocaleDateString(),
+    name: '罗凡',
+    college: '计科-2020'
+  },
+  {
+    type: getRandomNum(1, 3),
+    date: new Date().toLocaleDateString(),
+    name: '单承宣',
+    college: '计科-2020'
+  }
+])
+
+onMounted(() => {
+  getData()
+})
+
+const getData = () => {
+  countUpNum(deviceNumDom.value, deviceNum.value)
+  countUpNum(deviceNumOnDom.value, deviceNumOn.value)
+  countUpNum(deviceNumOffDom.value, deviceNumOff.value)
+  isToday(getData)
+}
+</script>
+<style lang="scss" scoped>
+.box {
+  position: relative;
+  width: 1041px;
+  height: 928px;
+
+  .select {
+    display: flex;
+    align-items: center;
+    justify-content: space-evenly;
+    margin-left: 18px;
+    width: 181px;
+    height: 55px;
+    color: #fff;
+    font-size: 20px;
+    border-radius: 12px;
+    background-image: linear-gradient(rgba(8, 31, 55, 0.8), rgba(18, 50, 81, 0.8));
+  }
+
+  .address {
+    position: absolute;
+    top: 230px;
+    left: 108px;
+    width: 55px;
+    height: 95px;
+  }
+
+  .address2 {
+    left: 838px;
+  }
+
+  .address3 {
+    top: 166px;
+    left: 224px;
+    transform: scale(0.8);
+  }
+
+  .address4 {
+    top: 166px;
+    left: 735px;
+    transform: scale(0.8);
+  }
+
+  .address5 {
+    top: 95px;
+    left: 285px;
+    transform: scale(0.5);
+  }
+
+  .address6 {
+    top: 95px;
+    left: 650px;
+    transform: scale(0.5);
+  }
+
+  .student {
+    position: absolute;
+    top: 200px;
+    left: 60px;
+    box-sizing: border-box;
+    padding-left: 45px;
+    display: flex;
+    align-items: center;
+    width: 184px;
+    height: 39px;
+    color: #fff;
+    font-size: 14px;
+    border-radius: 9px;
+    border: 1px solid #82e0ff;
+    background-color: #000000;
+    animation: float 3s linear infinite;
+
+    .student_icon {
+      position: absolute;
+      left: -4px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 38px;
+      height: 38px;
+      border-radius: 50%;
+      border: 3px solid #00fffb;
+
+      .icon_bg {
+        box-sizing: border-box;
+        padding: 4px;
+        width: 30px;
+        height: 30px;
+        border-radius: 50%;
+        background-color: #3edbfa;
+
+        img {
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+        }
+      }
+    }
+  }
+
+  .student2 {
+    left: 780px;
+    box-sizing: border-box;
+  }
+
+  .student3 {
+    top: 148px;
+    left: 162px;
+    transform: scale(0.8);
+    animation: float2 3s linear infinite;
+  }
+
+  .student4 {
+    top: 148px;
+    left: 670px;
+    transform: scale(0.8);
+    animation: float2 3s linear infinite;
+  }
+
+  .student5 {
+    top: 94px;
+    left: 220px;
+    transform: scale(0.5);
+    animation: float3 3s linear infinite;
+  }
+
+  .student6 {
+    top: 94px;
+    left: 588px;
+    transform: scale(0.5);
+    animation: float3 3s linear infinite;
+  }
+
+  .info {
+    display: flex;
+    justify-content: space-evenly;
+    position: absolute;
+    bottom: 20px;
+    left: 0;
+    right: 0;
+    height: 92px;
+
+    .info_box {
+      display: flex;
+      align-items: center;
+      width: 295px;
+      height: 92px;
+      border: 1px solid #275079;
+      background-image: linear-gradient(rgba(8, 31, 55, 0.8), rgba(18, 50, 81, 0.8));
+      animation: float 3s linear infinite;
+
+      .info_img {
+        margin-left: 30px;
+        width: 64px;
+        height: 54px;
+      }
+
+      .info_detail {
+        margin-left: 29px;
+        font-size: 18px;
+        color: #90d7fc;
+
+        .detail_num {
+          font-weight: bold;
+          font-size: 24px;
+          color: #fff;
+        }
+      }
+    }
+  }
+}
+</style>

+ 403 - 0
src/components/dorm/dorm-main-left.vue

@@ -0,0 +1,403 @@
+<template>
+  <div class="box">
+    <!-- 睡眠时长区域 -->
+    <div class="bar">
+      <div class="bar_title">
+        睡眠时长
+        <span>(昨日)</span>
+      </div>
+
+      <div ref="barChart" class="bar_chart"></div>
+    </div>
+
+    <!-- 起夜次数区域 -->
+    <div class="row">
+      <div class="row_title">
+        起夜次数
+        <span>(昨日)</span>
+      </div>
+
+      <div ref="rowChart" class="row_chart"></div>
+    </div>
+
+    <!-- 睡觉时间趋势区域 -->
+    <div class="line">
+      <div class="line_title">近7日上床睡觉时间趋势</div>
+
+      <div ref="lineChart" class="line_chart"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import * as Echarts from 'echarts'
+import { getNearSevenDay } from '@/utils/getNearSevenDay.js'
+import { getRandomNumList } from '@/utils/getRandomNumList.js'
+import { isToday } from '@/utils/isToday.js'
+
+// 睡眠时长DOM元素
+const barChart = ref(null)
+// 起夜次数DOM元素
+const rowChart = ref(null)
+//睡觉时间趋势DOM元素
+const lineChart = ref(null)
+
+// 人员数组
+const peopleList = ref(['何文羽', '夏叶轩', '李芮', '但学文', '罗凡', '单承宣'])
+// 近七天日期数组
+const NearSevenDayList = ref([])
+// 睡眠时长数组
+const sleepTimes = ref([])
+// 起夜次数数组
+const upList = ref([])
+// 上床睡觉时间数组
+const goSleepTimeList = ref([])
+
+let myBarChart
+let myRowChart
+let myLineChart
+
+onMounted(() => {
+  myBarChart = Echarts.init(barChart.value)
+  myRowChart = Echarts.init(rowChart.value)
+  myLineChart = Echarts.init(lineChart.value)
+  getData()
+})
+
+const getData = () => {
+  sleepTimes.value = getRandomNumList(6, 10, 6)
+  upList.value = getRandomNumList(0, 5, 6)
+  goSleepTimeList.value = getRandomNumList(19, 24, 7)
+  NearSevenDayList.value = getNearSevenDay()
+
+  // 初始化睡眠时长柱状图
+  initBarChart()
+  // 初始化起夜次数柱状图
+  initRowChart()
+  // 初始化睡觉时间趋势折线图
+  initLineChart()
+
+  isToday(getData)
+}
+
+const initBarChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '13%',
+      containLabel: true
+    },
+    xAxis: [
+      {
+        type: 'category',
+        data: peopleList.value,
+        axisTick: {
+          alignWithLabel: true
+        },
+        axisLine: {
+          show: false
+        },
+        axisLabel: {
+          margin: 12,
+          color: '#fff'
+        }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        name: '时长(h)',
+        nameTextStyle: {
+          color: '#fff'
+        },
+        splitLine: {
+          show: false
+        },
+        axisLabel: {
+          color: '#fff'
+        }
+      }
+    ],
+    series: [
+      {
+        name: '睡眠时长',
+        type: 'bar',
+        barWidth: '40%',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value.toFixed(0) + 'h'
+          }
+        },
+        data: sleepTimes.value
+      }
+    ],
+    color: {
+      type: 'linear',
+      x: 0,
+      y: 0,
+      x2: 0,
+      y2: 1,
+      colorStops: [
+        {
+          offset: 0,
+          color: '#1890FF' // 0% 处的颜色
+        },
+        {
+          offset: 1,
+          color: '#1EE7E7' // 100% 处的颜色
+        }
+      ]
+    }
+  }
+  myBarChart.setOption(options)
+}
+
+const initRowChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    grid: {
+      left: '2%',
+      right: '8%',
+      top: '10%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: [
+      {
+        type: 'value',
+        splitLine: {
+          show: false
+        },
+        axisLabel: {
+          color: '#fff'
+        }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'category',
+        axisTick: {
+          show: false
+        },
+        axisLine: {
+          show: false
+        },
+        axisLabel: {
+          color: '#fff'
+        },
+        data: peopleList.value.reverse()
+      }
+    ],
+    series: [
+      {
+        name: '起夜次数',
+        type: 'bar',
+        barWidth: '50%',
+        showBackground: true,
+        label: {
+          show: true,
+          position: 'right',
+          distance: 15,
+          color: '#fff'
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: upList.value
+      }
+    ],
+    color: {
+      type: 'linear',
+      x: 0,
+      y: 0,
+      x2: 1,
+      y2: 1,
+      colorStops: [
+        {
+          offset: 0,
+          color: '#1EE7E7' // 0% 处的颜色
+        },
+        {
+          offset: 1,
+          color: '#3F95CE' // 100% 处的颜色
+        }
+      ]
+    }
+  }
+  myRowChart.setOption(options)
+}
+
+const initLineChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    grid: {
+      left: '2%',
+      right: '4%',
+      top: '18%',
+      bottom: '5%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: true,
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#fff'
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: ['#506482'],
+          type: 'dashed'
+        }
+      },
+      data: NearSevenDayList.value
+    },
+    yAxis: {
+      type: 'value',
+      name: '时间',
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: ['#506482'],
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        data: goSleepTimeList.value,
+        type: 'line',
+        smooth: true,
+        color: '#1F92F6',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value.toFixed(0) + ':00'
+          }
+        },
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0,
+            y: 1,
+            x2: 0,
+            y2: 0,
+            colorStops: [
+              {
+                offset: 0,
+                color: 'rgba(31, 146, 246, 0)' // 0% 处的颜色
+              },
+              {
+                offset: 1,
+                color: 'rgba(31, 146, 246, 0.3)' // 100% 处的颜色
+              }
+            ]
+          }
+        }
+      }
+    ]
+  }
+  myLineChart.setOption(options)
+}
+</script>
+<style lang="scss" scoped>
+.box {
+  box-sizing: border-box;
+  padding: 37px 20px 0 20px;
+  margin-left: 30px;
+  width: 410px;
+  height: 928px;
+  background: linear-gradient(rgba(8, 31, 55, 0.8), rgba(18, 50, 81, 0.8));
+
+  .bar {
+    width: 370px;
+
+    .bar_title {
+      display: flex;
+      align-items: center;
+      padding-left: 37px;
+      height: 38px;
+      color: #fff;
+      font-size: 20px;
+      font-weight: bold;
+      background-image: url(@/assets/images/title_bg.png);
+      background-size: 100% 100%;
+
+      span {
+        font-size: 14px;
+        font-weight: 400;
+      }
+    }
+
+    .bar_chart {
+      width: 370px;
+      height: 252px;
+    }
+  }
+
+  .row {
+    width: 370px;
+    .row_title {
+      display: flex;
+      align-items: center;
+      padding-left: 37px;
+      height: 38px;
+      color: #fff;
+      font-size: 20px;
+      font-weight: bold;
+      background-image: url(@/assets/images/title_bg.png);
+      background-size: 100% 100%;
+
+      span {
+        font-size: 14px;
+        font-weight: 400;
+      }
+    }
+
+    .row_chart {
+      width: 370px;
+      height: 281px;
+    }
+  }
+
+  .line {
+    width: 370px;
+    .line_title {
+      display: flex;
+      align-items: center;
+      padding-left: 37px;
+      height: 38px;
+      color: #fff;
+      font-size: 20px;
+      font-weight: bold;
+      background-image: url(@/assets/images/title_bg.png);
+      background-size: 100% 100%;
+    }
+
+    .line_chart {
+      width: 370px;
+      height: 230px;
+    }
+  }
+}
+</style>

+ 469 - 0
src/components/dorm/dorm-main-right.vue

@@ -0,0 +1,469 @@
+<template>
+  <div class="box">
+    <!-- 学生体重变化趋势 -->
+    <div class="polyline">
+      <div class="polyline_title">学生体重变化趋势</div>
+      <div ref="polylineChart" class="polyline_chart"></div>
+    </div>
+    <!-- 异常告警分析 -->
+    <div class="form">
+      <div class="form_title">异常告警分析</div>
+
+      <div class="form_table">
+        <el-table :data="tableData">
+          <el-table-column label="告警类型" align="center">
+            <template #default="{ row }">
+              <div class="green" v-if="row.type === 1">缺勤</div>
+              <div class="blue" v-if="row.type === 2">留宿</div>
+              <div class="red" v-if="row.type === 3">频繁起夜</div>
+            </template>
+          </el-table-column>
+
+          <el-table-column prop="name" label="姓名" align="center" />
+          <el-table-column prop="college" label="学院" width="100" align="center" />
+          <el-table-column prop="date" label="时间" width="110" align="center" />
+        </el-table>
+      </div>
+    </div>
+    <!-- 能耗统计趋势 -->
+    <div class="line">
+      <div class="line_title">能耗统计趋势</div>
+      <div ref="lineChart" class="line_chart"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import * as Echarts from 'echarts'
+import { getNearSevenMonth } from '@/utils/getNearSevenMonth.js'
+import { getNearSevenDay } from '@/utils/getNearSevenDay.js'
+import { getRandomNumList } from '@/utils/getRandomNumList.js'
+import { isToday } from '@/utils/isToday.js'
+
+const polylineChart = ref(null)
+const lineChart = ref(null)
+
+// 近七月数组
+const NearSevenMonthList = ref([])
+// 近七天数组
+const NearSevenDayList = ref([])
+
+// 表格数组
+const tableData = ref([
+  {
+    type: 1,
+    date: new Date().toLocaleDateString(),
+    name: '何文羽',
+    college: '计科-2020'
+  },
+  {
+    type: 2,
+    date: new Date().toLocaleDateString(),
+    name: '夏叶轩',
+    college: '计科-2020'
+  },
+  {
+    type: 2,
+    date: new Date().toLocaleDateString(),
+    name: '李芮',
+    college: '计科-2020'
+  },
+  {
+    type: 3,
+    date: new Date().toLocaleDateString(),
+    name: '但学文',
+    college: '计科-2020'
+  },
+  {
+    type: 2,
+    date: new Date().toLocaleDateString(),
+    name: '罗凡',
+    college: '计科-2020'
+  },
+  {
+    type: 2,
+    date: new Date().toLocaleDateString(),
+    name: '单承宣',
+    college: '计科-2020'
+  }
+])
+
+// 用电数组
+const electricityList = ref([])
+// 用水数组
+const waterList = ref([])
+
+let myPolylineChart
+let myLineChart
+
+onMounted(() => {
+  myPolylineChart = Echarts.init(polylineChart.value)
+  myLineChart = Echarts.init(lineChart.value)
+  getData()
+})
+
+const getData = () => {
+  electricityList.value = getRandomNumList(0, 15, 6)
+  waterList.value = getRandomNumList(0, 6, 6)
+  NearSevenDayList.value = getNearSevenDay().splice(1, 6)
+  NearSevenMonthList.value = getNearSevenMonth()
+  initpolylineChart()
+  initLineChart()
+
+  isToday(getData)
+}
+
+// 初始化学生体重变化趋势图
+const initpolylineChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      top: 15,
+      icon: 'roundRect',
+      data: ['何文羽', '夏叶轩', '李芮', '但学文'],
+      textStyle: {
+        color: '#fff'
+      }
+    },
+    grid: {
+      left: '2%',
+      right: '8%',
+      bottom: '5%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: true,
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#fff'
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: ['#506482'],
+          type: 'dashed'
+        }
+      },
+      data: NearSevenMonthList.value
+    },
+    yAxis: {
+      type: 'value',
+      name: 'kg',
+      min: 30,
+      max: 100,
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: ['#506482'],
+          type: 'dashed'
+        }
+      }
+    },
+    color: ['#00B3FF', '#44F36F', '#1AEECA', '#658DA7'],
+    series: [
+      {
+        name: '何文羽',
+        type: 'line',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value + 'kg'
+          }
+        },
+        data: getRandomNumList(50, 55, 7)
+      },
+      {
+        name: '夏叶轩',
+        type: 'line',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value + 'kg'
+          }
+        },
+        data: getRandomNumList(50, 65, 7)
+      },
+      {
+        name: '李芮',
+        type: 'line',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value + 'kg'
+          }
+        },
+        data: getRandomNumList(50, 60, 7)
+      },
+      {
+        name: '但学文',
+        type: 'line',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value + 'kg'
+          }
+        },
+        data: getRandomNumList(50, 50, 7)
+      }
+    ]
+  }
+  myPolylineChart.setOption(options)
+}
+
+// 初始化能耗统计趋势图
+const initLineChart = () => {
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      top: 15,
+      icon: 'roundRect',
+      data: ['用电', '用水'],
+      textStyle: {
+        color: '#fff'
+      }
+    },
+    grid: {
+      left: '5%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#fff'
+      },
+      data: NearSevenDayList.value
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '单位:kw/h',
+        nameTextStyle: {
+          color: '#00E0DB'
+        },
+        axisLabel: {
+          color: '#fff'
+        },
+        //分割线
+        splitLine: {
+          show: true,
+          lineStyle: {
+            color: ['#506482'],
+            type: 'dashed'
+          }
+        }
+      },
+      {
+        type: 'value',
+        name: '单位:t',
+        nameTextStyle: {
+          color: '#43BAFF'
+        },
+        axisLabel: {
+          color: '#fff'
+        },
+        //分割线
+        splitLine: {
+          show: false
+        }
+      }
+    ],
+    series: [
+      {
+        name: '用电',
+        type: 'line',
+        smooth: true,
+        yAxisIndex: 0,
+        tooltip: {
+          valueFormatter: function (value) {
+            return value.toFixed(0) + 'kw/h'
+          }
+        },
+        data: electricityList.value,
+        itemStyle: {
+          color: '#00E0DB'
+        },
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0,
+            y: 1,
+            x2: 0,
+            y2: 0,
+            colorStops: [
+              {
+                offset: 0,
+                color: 'rgba(31, 146, 246, 0)' // 0% 处的颜色
+              },
+              {
+                offset: 1,
+                color: 'rgba(31, 146, 246, 0.3)' // 100% 处的颜色
+              }
+            ]
+          }
+        }
+      },
+      {
+        name: '用水',
+        type: 'line',
+        smooth: true,
+        yAxisIndex: 1,
+        tooltip: {
+          valueFormatter: function (value) {
+            return value.toFixed(0) + 't'
+          }
+        },
+        data: waterList.value,
+        itemStyle: {
+          color: '#3061C0'
+        },
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0,
+            y: 1,
+            x2: 0,
+            y2: 0,
+            colorStops: [
+              {
+                offset: 0,
+                color: 'rgba(31, 146, 246, 0)' // 0% 处的颜色
+              },
+              {
+                offset: 1,
+                color: 'rgba(31, 146, 246, 0.3)' // 100% 处的颜色
+              }
+            ]
+          }
+        }
+      }
+    ]
+  }
+
+  myLineChart.setOption(options)
+}
+</script>
+<style lang="scss" scoped>
+.box {
+  box-sizing: border-box;
+  padding: 37px 20px 0 20px;
+  margin-right: 30px;
+  width: 410px;
+  height: 928px;
+  background: linear-gradient(rgba(8, 31, 55, 0.8), rgba(18, 50, 81, 0.8));
+
+  .polyline {
+    width: 370px;
+
+    .polyline_title {
+      display: flex;
+      align-items: center;
+      padding-left: 37px;
+      height: 38px;
+      color: #fff;
+      font-size: 20px;
+      font-weight: bold;
+      background-image: url(@/assets/images/title_bg.png);
+      background-size: 100% 100%;
+    }
+
+    .polyline_chart {
+      width: 370px;
+      height: 252px;
+    }
+  }
+
+  .form {
+    width: 370px;
+    .form_title {
+      display: flex;
+      align-items: center;
+      padding-left: 37px;
+      height: 38px;
+      color: #fff;
+      font-size: 20px;
+      font-weight: bold;
+      background-image: url(@/assets/images/title_bg.png);
+      background-size: 100% 100%;
+    }
+
+    .form_table {
+      margin-top: 10px;
+      height: 270px;
+      overflow: hidden;
+      .green {
+        color: #54e2a9;
+      }
+
+      .blue {
+        color: #00a0e9;
+      }
+
+      .red {
+        color: #e04236;
+      }
+    }
+  }
+
+  .line {
+    width: 370px;
+    .line_title {
+      display: flex;
+      align-items: center;
+      padding-left: 37px;
+      height: 38px;
+      color: #fff;
+      font-size: 20px;
+      font-weight: bold;
+      background-image: url(@/assets/images/title_bg.png);
+      background-size: 100% 100%;
+    }
+    .line_chart {
+      width: 370px;
+      height: 235px;
+    }
+  }
+}
+
+/*最外层透明*/
+.form_table ::v-deep(.el-table),
+.form_table ::v-deep(.el-table__expanded-cell) {
+  background-color: transparent;
+  color: white;
+  border: none;
+}
+/* 表格内背景颜色 */
+.form_table ::v-deep(.el-table th),
+.form_table ::v-deep(.el-table tr),
+.form_table ::v-deep(.el-table td),
+::v-deep(.el-table th.el-table__cell.is-leaf) {
+  background-color: transparent !important;
+  //表格字体颜色
+  color: white;
+  border: none;
+  height: 52px;
+}
+
+::v-deep(.el-table__inner-wrapper::before) {
+  height: 0;
+}
+
+::v-deep(.el-scrollbar__view) {
+  animation: run 12s linear infinite;
+}
+</style>

+ 37 - 0
src/components/dorm/dorm-main.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="main">
+    <!-- 主体内容左边区域 -->
+    <mainLeft />
+
+    <!-- 主体内容中间区域 -->
+    <mainCenter />
+
+    <!-- 主体内容右边区域 -->
+    <mainRight />
+
+    <!-- 底部边框区域 -->
+    <img class="main_img" src="@/assets/images/bottom_bg.png" />
+  </div>
+</template>
+
+<script setup>
+import mainLeft from '@/components/dorm/dorm-main-left.vue'
+import mainCenter from '@/components/dorm/dorm-main-center.vue'
+import mainRight from '@/components/dorm/dorm-main-right.vue'
+</script>
+<style lang="scss" scoped>
+.main {
+  position: relative;
+  z-index: 999;
+  display: flex;
+  height: 980px;
+
+  .main_img {
+    position: absolute;
+    left: 0;
+    bottom: 23px;
+    width: 100%;
+    height: 32px;
+  }
+}
+</style>

+ 27 - 0
src/main.js

@@ -0,0 +1,27 @@
+import './assets/main.css'
+
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+
+// 引入vue3 element 组件 和样式
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+
+// element组件默认语言设置为中文
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+const app = createApp(App)
+
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.use(router)
+
+app
+  .use(ElementPlus, {
+    locale: zhCn
+  })
+  .mount('#app')

+ 33 - 0
src/router/index.js

@@ -0,0 +1,33 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      name: 'dorm',
+      component: () => import('@/views/DormView.vue'),
+      meta: {
+        title: '智慧宿舍'
+      }
+    },
+    {
+      path: '/classroom',
+      name: 'classroom',
+      component: () => import('@/views/ClassroomView.vue'),
+      meta: {
+        title: '智慧教室'
+      }
+    },
+    {
+      path: '/bookroom',
+      name: 'bookroom',
+      component: () => import('@/views/BookroomView.vue'),
+      meta: {
+        title: '智慧书房'
+      }
+    }
+  ]
+})
+
+export default router

+ 12 - 0
src/utils/countUpNum.js

@@ -0,0 +1,12 @@
+import { CountUp } from 'countup.js'
+
+const options = {
+  // 小数点位
+  decimalPlaces: 0,
+  // 持续时间
+  durations: 2
+}
+
+export const countUpNum = (dom, value) => {
+  new CountUp(dom, value, options).start()
+}

+ 13 - 0
src/utils/getNearSevenDay.js

@@ -0,0 +1,13 @@
+// 获取近七天日期
+export const getNearSevenDay = () => {
+  // 近7天
+  const days = 7
+
+  const dateList = Array.from({ length: days }, (v, i) => i).map((day) => {
+    const date = new Date()
+    date.setDate(date.getDate() - day)
+    return date.getMonth() + 1 + '-' + date.getDate().toString().padStart(2, 0)
+  })
+
+  return dateList.reverse()
+}

+ 18 - 0
src/utils/getNearSevenMonth.js

@@ -0,0 +1,18 @@
+// 获取近七月
+export const getNearSevenMonth = () => {
+  let date = new Date()
+  let M = date.getMonth() + 1
+  // 近7月
+  const months = 6
+
+  let arr = []
+  for (let i = months; i > 0; i--) {
+    let sixMonth = M - i
+    if (sixMonth <= 0) {
+      sixMonth = 12 + sixMonth
+    }
+    arr.push(sixMonth + '月')
+  }
+  arr.push(M + '月')
+  return arr
+}

+ 15 - 0
src/utils/getRandomNumList.js

@@ -0,0 +1,15 @@
+export const getRandomNumList = (start, stop, count) => {
+  // start 最小数  stop 最大数  count 数组长度
+  const numList = Array.from({ length: count }, (v, i) => i).map(() => {
+    const num = parseInt(Math.random() * (stop - start) + start)
+    return num
+  })
+  return numList
+}
+
+export const getRandomNum = (start, stop) => {
+  // start 最小数  stop 最大数
+  const num = parseInt(Math.random() * (stop - start) + start)
+
+  return num
+}

+ 32 - 0
src/utils/getTime.js

@@ -0,0 +1,32 @@
+// 获取当前时间 星期
+export const getCurrentTime = () => {
+  let date = new Date()
+  // 年份
+  let Y = date.getFullYear()
+  // 月份
+  let M = (date.getMonth() + 1).toString().padStart(2, 0)
+  // 日期
+  let D = date.getDate().toString().padStart(2, 0)
+  // 星期
+  let week = date.getDay()
+  let weekArr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+
+  //   小时
+  let H = date.getHours().toString().padStart(2, 0)
+  // 分钟
+  let MM = date.getMinutes().toString().padStart(2, 0)
+
+  // 秒
+  let S = date.getSeconds().toString().padStart(2, 0)
+
+  // 当前日期星期
+  let currentDate = Y + '年' + M + '月' + D + '日 ' + weekArr[week]
+
+  // 当前时分秒
+  let currentTime = ' ' + H + ':' + MM + ':' + S
+
+  return {
+    currentDate,
+    currentTime
+  }
+}

+ 14 - 0
src/utils/isToday.js

@@ -0,0 +1,14 @@
+export const isToday = (callBack) => {
+  //明天零点 - 当前时间的时间戳 = 计时器倒计时毫秒
+  const Time =
+    Number(
+      new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() + 1, 0, 0, 0)
+    ) - Number(new Date())
+
+  let timer
+
+  timer = setInterval(() => {
+    clearInterval(timer)
+    callBack()
+  }, Time)
+}

+ 25 - 0
src/views/BookroomView.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="container">
+    <!-- 头部区域 -->
+    <classRoomHeader />
+    <!-- 主体内容区域 -->
+    <classRoomMain v-if="activeIndex === 2" />
+  </div>
+</template>
+
+<script setup>
+import { inject } from 'vue'
+import classRoomHeader from '@/components/bookRoom/bookRoom-header.vue'
+import classRoomMain from '@/components/bookRoom/bookRoom-main.vue'
+
+// 接受祖先元素传递过来的值
+const activeIndex = inject('activeIndex')
+</script>
+<style lang="scss" scoped>
+.container {
+  width: 100%;
+  height: 100%;
+  background-image: url(@/assets/images/bg3.png);
+  background-size: 100% 100%;
+}
+</style>

+ 26 - 0
src/views/ClassroomView.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="container">
+    <!-- 头部区域 -->
+    <classRoomHeader />
+
+    <!-- 主体内容区域 -->
+    <classRoomMain v-if="activeIndex === 1" />
+  </div>
+</template>
+
+<script setup>
+import { inject } from 'vue'
+import classRoomHeader from '@/components/classRoom/classRoom-header.vue'
+import classRoomMain from '@/components/classRoom/classRoom-main.vue'
+
+// 接受祖先元素传递过来的值
+const activeIndex = inject('activeIndex')
+</script>
+<style lang="scss" scoped>
+.container {
+  width: 100%;
+  height: 100%;
+  background-image: url(@/assets/images/bg2.png);
+  background-size: 100% 100%;
+}
+</style>

+ 58 - 0
src/views/DormView.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="container">
+    <!-- 头部区域 -->
+    <dormHeader />
+
+    <!-- 主体内容区域 -->
+    <dormMain v-if="activeIndex === 0" />
+
+    <!-- 遮罩层 -->
+    <div class="mask"></div>
+    <div class="mask2"></div>
+  </div>
+</template>
+
+<script setup>
+import { inject } from 'vue'
+import dormHeader from '@/components/dorm/dorm-header.vue'
+import dormMain from '@/components/dorm/dorm-main.vue'
+
+// 接受祖先元素传递过来的值
+const activeIndex = inject('activeIndex')
+</script>
+<style lang="scss" scoped>
+.container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background-image: url(@/assets/images/bg.png);
+  background-size: 100% 100%;
+
+  .mask {
+    position: absolute;
+    top: 0;
+    width: 100%;
+    height: 144.5px;
+    pointer-events: none;
+    background-image: linear-gradient(
+      #081f37 5px,
+      rgba(18, 50, 81, 0.8) 20px,
+      rgba(18, 50, 81, 0) 100%
+    );
+  }
+
+  .mask2 {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    height: 200px;
+    pointer-events: none;
+    background-image: linear-gradient(
+      to top,
+      #081f37 5px,
+      rgba(18, 50, 81, 0.8) 20px,
+      rgba(18, 50, 81, 0) 100%
+    );
+  }
+}
+</style>

+ 14 - 0
vite.config.js

@@ -0,0 +1,14 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  }
+})