Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/wanzi' into muzi

liyanbo 2 mesiacov pred
rodič
commit
02cdd42db4

+ 6 - 230
src/components/HomePage.vue

@@ -36,49 +36,7 @@
             >{{ button.name }}</el-button
           >
           <!-- 用户名显示 -->
-          <el-dropdown>
-            <div class="user-name-box">
-              {{ userName }}
-            </div>
-            <template #dropdown>
-              <el-dropdown-menu class="dropdown-menu user-popover-content">
-                <!-- 退出登录按钮 -->
-                <div class="user-logout-btn" @click="LogoutClick()">
-                  退出登录
-                </div>
-                <!-- 用户信息 -->
-                <div class="top-box">
-                  <div class="top-inner-box">
-                    <p>已激活课程</p>
-                  </div>
-                  <div class="bottom-inner-box">
-                    <div v-for="(course, index) in courses" :key="index" class="course-item">
-                      <div class="check-circle">✓</div>
-                      <span>{{ course.name }}</span>
-                      <span>{{ course.expireDate }}</span>
-                    </div>
-                  </div>
-                </div>
-                <!-- 激活码 -->
-                <div class="bottom-box">
-                   <el-input
-                    v-model="activationCode"
-                    placeholder="请输入课程激活码"
-                    class="activation-input"
-                    size="small"
-                  />
-                  <el-button
-                    type="primary"
-                    size="small"
-                    class="activation-btn"
-                    @click="handleActivation"
-                  >
-                    激活
-                  </el-button>
-                </div>
-              </el-dropdown-menu>
-            </template>
-          </el-dropdown>
+          <UserInfoPopover />
         </div>
       </div>
     </div>
@@ -123,6 +81,8 @@ import { useRouter } from 'vue-router'
 import { ClassList } from '@/api/class.js'
 import {ArrowDown} from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
+import UserInfoPopover from '@/components/user/UserInfoPopover.vue'
+
 
 // 导入图片
 import intelligenceImg from '@/assets/images/intelligence.png'
@@ -136,34 +96,18 @@ import logoutIcon from '@/assets/icon/logout.png'
 import {logoutLogic, removeLocalStorageKey} from '@/utils/loginUtils.js'
 import {aiCourseRoutes, blocklyRoutes, homeRoutes} from "@/router/index.js";
 
-// 激活码响应式变量
-const activationCode = ref('')
 
-// 课程列表响应式变量
-const courses = ref([
-  { name: 'AI编程课', expireDate: '2026.02.21到期' },
-  { name: 'AI实验课', expireDate: '2026.05.12到期' }
-])
 
 // 平台标题响应式变量
 const platformTitle = ref(import.meta.env.VITE_APP_TITLE)
-// 用户名响应式变量
-const userName = ref(import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME)
 // 更新平台标题
 const updatePlatformTitle = () => {
   platformTitle.value = localStorage.getItem('tenantName') || import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT
 }
-// 更新用户名
-const updateUserName = () => {
-  userName.value = localStorage.getItem('userName') || import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME
-}
 
 // 获取当前路由对象
 const router = useRouter()
-// 退出登录
-const LogoutClick = async () => {
-  await logoutLogic(router, homeRoutes.login)
-}
+
 
 // 处理按钮点击事件
 const handleButtonClick = (button) => {
@@ -260,31 +204,17 @@ const handleGradeSelect = (command) => {
   }
 }
 
-// 处理激活按钮点击
-const handleActivation = () => {
-  if (!activationCode.value) {
-    ElMessage.warning('请输入激活码')
-    return
-  }
-  // 这里可以添加激活码验证逻辑
-  ElMessage.success('激活码:' + activationCode.value + ' 提交成功')
-  activationCode.value = ''
-}
+
 
 onMounted(() => {
   fetchCtTypes()
   // 初始化平台标题
   updatePlatformTitle()
-  // 初始化用户名
-  updateUserName()
   // storage事件监听器,监听其他标签页对localStorage的修改
   window.addEventListener('storage', (e) => {
     if (e.key === 'tenantName') {
       updatePlatformTitle()
     }
-    if (e.key === 'userName') {
-      updateUserName()
-    }
   })
 
   //删除所有以token开头的键值对
@@ -307,21 +237,7 @@ window.updateTenantName = (newName) => {
   @return math.div($px, 750) * 100vw;
 }
 
-// 用户名显示
-.user-name-box {
-  width: auto;
-  height: rpx(15);
-  margin: rpx(10) rpx(-10) 0 0;
-  color: white;
-  font-size: rpx(8);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background-color: rgba(255, 255, 255, 0.2);
-  border-radius: rpx(15);
-  padding: rpx(0) rpx(10);
-  min-width: rpx(20);
-}
+
 .home-container {
   position: fixed;
   top: 0;
@@ -533,147 +449,7 @@ window.updateTenantName = (newName) => {
   box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
 }
 
-// 用户信息弹框样式
-
 
-.user-popover-content {
-  width: auto;
-  min-width: rpx(100);
-  max-width: rpx(200);
-  border-radius: rpx(5);
-  background-color: white;
-  backdrop-filter: blur(rpx(5));
-  box-shadow: 0 rpx(2) rpx(4) rgba(202, 52, 52, 0.1);
-  padding-bottom: rpx(0);
-  position: relative;
-}
-
-.user-logout-btn {
-  position: absolute;
-  top: 0;
-  right: 0;
-  padding: rpx(2) rpx(6);
-  background: linear-gradient(
-    to bottom,
-    #fee78a,
-    #ffce1b
-  );
-  border-bottom-left-radius: rpx(5);
-  border-top-right-radius: rpx(5);
-  color: black;
-  font-size: rpx(6);
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.user-popover-content p {
-  font-size: rpx(8);
-  color: black;
-  margin: rpx(5) 0;
-}
-
-.user-popover-content p:first-child {
-  font-weight: bold;
-}
-
-.top-box {
-  display: flex;
-  flex-direction: column;
-}
-.top-inner-box{
-  padding-top: rpx(5);
-}
-
-.top-inner-box,
-.bottom-inner-box {
-  padding-left: rpx(6);
-}
-
-.top-inner-box p{
-  font-size: rpx(8);
-  margin: rpx(2) 0;
-}
-
-.top-inner-box p:first-child {
-  font-weight: bold;
-}
-
-.bottom-inner-box {
-  display: flex;
-  flex-direction: column;
-}
-
-.course-item {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  gap: rpx(4);
-  padding: rpx(2);
-  border-radius: rpx(3);
-}
-
-.course-item span {
-  font-size: rpx(7);
-  color: #525050;
-  white-space: nowrap;
-}
-.course-item span:nth-child(3) {
-   font-size: rpx(6);
-   color: #a6a4a4;
-}
-
-.check-circle {
-  width: rpx(7);
-  height: rpx(7);
-  border-radius: 50%;
-  background: linear-gradient(
-    to bottom,
-    #fee78a,
-    #ffce1b
-  );
-  color: white;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: rpx(5);
-  font-weight: bold;
-}
-
-.bottom-box {
-  border-bottom-right-radius: rpx(5);
-  border-bottom-left-radius: rpx(5);
-  background-color: rgba(240, 240, 240, 0.9);
-  display: flex;
-  align-items: center;
-  gap: rpx(10);
-  padding: rpx(8);
-}
-
-.activation-input {
-  flex: 1;
-}
-
-.activation-input :deep(.el-input__wrapper) {
-  border-radius: rpx(3);
-  width: rpx(60);
-  height: rpx(15);
-  font-size: rpx(7);
-}
-
-.activation-btn {
-  width: rpx(40);
-  height: rpx(15);
-  font-size: rpx(7);
-  border: none;
-  color: black;
-  border-radius: rpx(10);
-  background: linear-gradient(
-    to bottom,
-    #fee78a,
-    #ffce1b
-  );
-}
 </style>
 
 

+ 283 - 0
src/components/user/UserInfoPopover.vue

@@ -0,0 +1,283 @@
+<template>
+  <!-- 用户信息下拉菜单 -->
+  <el-dropdown>
+    <div class="user-name-box">
+      {{ userName }}
+    </div>
+    <template #dropdown>
+      <el-dropdown-menu class="dropdown-menu user-popover-content">
+        <!-- 退出登录按钮 -->
+        <div class="user-logout-btn" @click="LogoutClick()">
+          退出登录
+        </div>
+        <!-- 用户信息 -->
+        <div class="top-box">
+          <div class="top-inner-box">
+            <p>已激活课程</p>
+          </div>
+          <div class="bottom-inner-box">
+            <div v-for="(course, index) in courses" :key="index" class="course-item">
+              <div class="check-circle">✓</div>
+              <span>{{ course.name }}</span>
+              <span>{{ course.expireDate }}</span>
+            </div>
+          </div>
+        </div>
+        <!-- 激活码 -->
+        <div class="bottom-box">
+           <el-input
+            v-model="activationCode"
+            placeholder="请输入课程激活码"
+            class="activation-input"
+            size="small"
+          />
+          <el-button
+            type="primary"
+            size="small"
+            class="activation-btn"
+            @click="handleActivation"
+          >
+            激活
+          </el-button>
+        </div>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { logoutLogic } from '@/utils/loginUtils.js'
+import { homeRoutes } from "@/router/index.js"
+
+// 激活码响应式变量
+const activationCode = ref('')
+
+// 课程列表响应式变量
+const courses = ref([
+  { name: 'AI编程课', expireDate: '2026.02.21到期' },
+  { name: 'AI实验课', expireDate: '2026.05.12到期' }
+])
+
+// 用户名响应式变量
+const userName = ref(import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME)
+
+// 更新用户名
+const updateUserName = () => {
+  userName.value = localStorage.getItem('userName') || import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME
+}
+
+// 获取当前路由对象
+const router = useRouter()
+// 退出登录
+const LogoutClick = async () => {
+  await logoutLogic(router, homeRoutes.login)
+}
+
+// 处理激活按钮点击
+const handleActivation = () => {
+  if (!activationCode.value) {
+    ElMessage.warning('请输入激活码')
+    return
+  }
+  // 这里可以添加激活码验证逻辑
+  ElMessage.success('激活码:' + activationCode.value + ' 提交成功')
+  activationCode.value = ''
+}
+
+onMounted(() => {
+  // 初始化用户名
+  updateUserName()
+  // storage事件监听器,监听其他标签页对localStorage的修改
+  window.addEventListener('storage', (e) => {
+    if (e.key === 'userName') {
+      updateUserName()
+    }
+  })
+})
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+// 用户名显示
+.user-name-box {
+  width: auto;
+  height: rpx(15);
+  margin: rpx(10) rpx(-10) 0 0;
+  color: white;
+  font-size: rpx(8);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: rgba(255, 255, 255, 0.2);
+  border-radius: rpx(15);
+  padding: rpx(0) rpx(10);
+  min-width: rpx(20);
+}
+
+// 用户信息弹框样式
+.user-popover-content {
+  width: auto;
+  min-width: rpx(100);
+  max-width: rpx(200);
+  border-radius: rpx(5);
+  background-color: white;
+  backdrop-filter: blur(rpx(5));
+  box-shadow: 0 rpx(2) rpx(4) rgba(202, 52, 52, 0.1);
+  padding-bottom: rpx(0);
+  position: relative;
+}
+
+.user-logout-btn {
+  position: absolute;
+  top: 0;
+  right: 0;
+  padding: rpx(3) rpx(9);
+  background: linear-gradient(
+    to bottom,
+    #fee78a,
+    #ffce1b
+  );
+  border-bottom-left-radius: rpx(5);
+  border-top-right-radius: rpx(5);
+  color: black;
+  font-size: rpx(7);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.user-popover-content p {
+  font-size: rpx(8);
+  color: black;
+  margin: rpx(5) 0;
+}
+
+.user-popover-content p:first-child {
+  font-weight: bold;
+}
+
+.top-box {
+  display: flex;
+  flex-direction: column;
+  cursor: pointer;
+}
+
+.top-inner-box{
+  padding-top: rpx(5);
+}
+
+.top-inner-box,
+.bottom-inner-box {
+  padding-left: rpx(6);
+}
+
+.top-inner-box p{
+  font-size: rpx(8);
+  margin: rpx(2) 0;
+}
+
+.top-inner-box p:first-child {
+  font-weight: bold;
+}
+
+.bottom-inner-box {
+  display: flex;
+  flex-direction: column;
+}
+
+.course-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: rpx(4);
+  padding: rpx(2);
+  border-radius: rpx(3);
+}
+
+.course-item span {
+  font-size: rpx(7);
+  color: #525050;
+  white-space: nowrap;
+}
+
+.course-item span:nth-child(3) {
+   font-size: rpx(6);
+   color: #a6a4a4;
+}
+
+.check-circle {
+  width: rpx(7);
+  height: rpx(7);
+  border-radius: 50%;
+  background: linear-gradient(
+    to bottom,
+    #fee78a,
+    #ffce1b
+  );
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: rpx(5);
+  font-weight: bold;
+}
+
+.bottom-box {
+  border-bottom-right-radius: rpx(5);
+  border-bottom-left-radius: rpx(5);
+  background-color: rgba(240, 240, 240, 0.9);
+  display: flex;
+  align-items: center;
+  gap: rpx(10);
+  padding: rpx(8);
+}
+
+.activation-input {
+  flex: 1;
+}
+
+.activation-input :deep(.el-input__wrapper) {
+  border-radius: rpx(3);
+  width: rpx(60);
+  height: rpx(15);
+  font-size: rpx(7);
+}
+
+.activation-btn {
+  width: rpx(40);
+  height: rpx(15);
+  font-size: rpx(7);
+  border: none;
+  color: black;
+  border-radius: rpx(10);
+  background: linear-gradient(
+    to bottom,
+    #fee78a,
+    #ffce1b
+  );
+}
+</style>
+
+<style lang="scss">
+/* 移除用户名下拉菜单的焦点边框 */
+.user-name-box:focus,
+.user-name-box:focus-within,
+.user-name-box:hover{
+  outline: none;
+  box-shadow: none;
+}
+/* 确保Element Plus下拉菜单触发元素没有焦点边框 */
+.el-dropdown .el-dropdown__trigger:focus{
+  outline: none;
+  box-shadow: none;
+}
+</style>

+ 4 - 0
src/style.css

@@ -17,6 +17,10 @@
   text-rendering: optimizeLegibility;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
 }
 
 a {

+ 9 - 64
src/views/laboratory/ExperimentalTheme.vue

@@ -20,14 +20,7 @@
         <div class="top-right-box">
           <div class="top-right-inner-box">
             <!-- 用户名显示 -->
-            <div class="user-name-box" v-if="userName">
-              {{ userName }}
-            </div>
-            <!-- 退出登录 -->
-            <el-button round class="logout-box-btn" @click="LogoutClick()">
-              <img :src="logoutIcon" alt="Logout" />
-              退出登录
-            </el-button>
+            <UserInfoPopover />
           </div>
         </div>
       </div>
@@ -106,10 +99,10 @@ import { useRouter } from 'vue-router';
 // 导入按钮图片
 import leftbtn from '@/assets/programming/leftbtn.png'
 import rightbtn from '@/assets/programming/rightbtn.png'
-// 退出登录图标
-import logoutIcon from '@/assets/icon/logout.png'
+// 导入用户信息组件
+import UserInfoPopover from '@/components/user/UserInfoPopover.vue'
 // 退出登录
-import {logoutLogic, removeLocalStorageKey} from '@/utils/loginUtils.js'
+import { removeLocalStorageKey} from '@/utils/loginUtils.js'
 // 实验课主题接口
 import { getThemeExperimentList } from '@/api/laboratory/index.js'
 import {aiCourseRoutes, homeRoutes} from "@/router/index.js";
@@ -130,8 +123,7 @@ const blocklyActiveButton = ref("AiCourseThemeActiveButton")
 const activeButton = ref(Number(localStorage.getItem(blocklyActiveButton.value)) || 0)
 // 定义实验主题数据
 const experimentList = reactive([])
-// 用户名响应式变量
-const userName = ref('')
+
 // 获取middleBox元素的引用
 const middleBox = ref(null)
 // 拖动相关变量
@@ -146,10 +138,7 @@ const circleButtons = computed(() => {
   }))
 })
 
-// 更新用户名
-const updateUserName = () => {
-  userName.value = localStorage.getItem('userName') || ''
-}
+
 
 // 获取实验课主题列表
 const fetchExperimentList = async () => {
@@ -260,34 +249,18 @@ const goToHomePage = () => {
 
    router.push({path: homeRoutes.home})
 }
-
-// 退出登录
-const LogoutClick = async () => {
-  await logoutLogic(router, aiCourseRoutes.login)
-}
-
-// 存储事件处理函数
-const handleStorageChange = (e) => {
-  if (e.key === 'userName') {
-    updateUserName()
-  }
-}
-
 onMounted(() => {
-  // 初始化用户名
-  updateUserName()
   // 获取实验主题列表
   fetchExperimentList()
   // storage事件监听器,监听其他标签页对localStorage的修改
-  window.addEventListener('storage', handleStorageChange)
-
+  // window.addEventListener('storage', handleStorageChange)
   // 删除AI实验课对话缓存
   removeLocalStorageKey(localStorage.getItem("token") + "_aiCourse_")//AI实验课
 })
 
 onUnmounted(() => {
   // 移除事件监听器
-  window.removeEventListener('storage', handleStorageChange)
+  // window.removeEventListener('storage', handleStorageChange)
 })
 
 </script>
@@ -384,39 +357,11 @@ onUnmounted(() => {
   display: flex;
   align-items: center; /* 垂直居中对齐 */
   justify-content: flex-end; /* 水平靠右对齐 */
-  padding-right: rpx(0);
-}
-
-.user-name-box {
-  width: auto;
-  height: rpx(15);
-  margin: rpx(10) 0 0 0;
-  color: white;
-  font-size: rpx(8);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background-color: rgba(255, 255, 255, 0.2);
-  border-radius: rpx(15);
-  padding: rpx(0) rpx(10);
-  min-width: rpx(20);
+  padding-right: rpx(30);
   cursor: pointer;
 }
 
-.logout-box-btn {
-  width: rpx(65);
-  height: rpx(15);
-  margin: rpx(10) rpx(10) 0 0;
-  background-color: transparent;
-  color: white;
-  border: none;
-  font-size: rpx(7);
-  outline: none;
-}
 
-.logout-box-btn img {
-  width: rpx(10);
-}
   
 .middle-wrapper {
   height: 75%;

+ 9 - 53
src/views/programming/ProgrammingGame.vue

@@ -20,14 +20,7 @@
         <div class="top-right-box">
           <div class="top-right-inner-box">
             <!-- 用户名显示 -->
-            <div class="user-name-box">
-              {{ userName }}
-            </div>
-            <!-- 退出登录 -->
-            <el-button round class="logout-box-btn" @click="LogoutClick()">
-              <img :src="logoutIcon" alt="Logout" />
-              退出登录
-            </el-button>
+            <UserInfoPopover />
           </div>
         </div>
       </div>
@@ -91,10 +84,11 @@ import { getThemeList } from '@/api/programming/index.js'
 import leftbtn from '@/assets/programming/leftbtn.png'
 import rightbtn from '@/assets/programming/rightbtn.png'
 
-// 退出登录图标
-import logoutIcon from '@/assets/icon/logout.png'
+// 导入用户信息组件
+import UserInfoPopover from '@/components/user/UserInfoPopover.vue'
+
 // 退出登录
-import {logoutLogic, removeLocalStorageKey} from '@/utils/loginUtils.js'
+import { removeLocalStorageKey} from '@/utils/loginUtils.js'
 import {blocklyRoutes, homeRoutes} from "@/router/index.js";
 import {scrollToCenter} from "@/utils/pageCss/scrollToCenter.js";
 
@@ -109,12 +103,7 @@ const activeButton = ref(Number(localStorage.getItem(blocklyActiveButton.value))
 const circleButtons = reactive([])
 // 定义课程类别数据
 const courseCategories = reactive([])
-// 用户名响应式变量
-const userName = ref('')
-// 更新用户名
-const updateUserName = () => {
-  userName.value = localStorage.getItem('userName')
-}
+
 
 // 获取主题列表
 const themeList = async () => {
@@ -227,20 +216,15 @@ const goToHomePage = () => {
   })
 }
 
-// 退出登录
-const LogoutClick = async () => {
-  await logoutLogic(router, blocklyRoutes.login)
-}
+
 
 onMounted(() => {
   // 获取主题列表
   themeList()
-  // 初始化用户名
-  updateUserName()
   // storage事件监听器,监听其他标签页对localStorage的修改
   window.addEventListener('storage', (e) => {
     if (e.key === 'userName') {
-      updateUserName()
+      // updateUserName()
     }
   })
 
@@ -342,39 +326,11 @@ onMounted(() => {
   display: flex;
   align-items: center; /* 垂直居中对齐 */
   justify-content: flex-end; /* 水平靠右对齐 */
-  padding-right: rpx(0);
-}
-
-.user-name-box {
-  width: auto;
-  height: rpx(15);
-  margin: rpx(10) 0 0 0;
-  color: white;
-  font-size: rpx(8);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background-color: rgba(255, 255, 255, 0.2);
-  border-radius: rpx(15);
-  padding: rpx(0) rpx(10);
-  min-width: rpx(20);
+  padding-right: rpx(30);
   cursor: pointer;
 }
 
-.logout-box-btn {
-  width: rpx(65);
-  height: rpx(15);
-  margin: rpx(10) rpx(10) 0 0;
-  background-color: transparent;
-  color: white;
-  border: none;
-  font-size: rpx(7);
-  outline: none;
-}
 
-.logout-box-btn img {
-  width: rpx(10);
-}
   
 .middle-wrapper {
   height: 75%;