Explorar o código

Merge branch 'muzi'

liyanbo hai 2 meses
pai
achega
df6e941580

+ 4 - 3
package-lock.json

@@ -17,6 +17,7 @@
         "@vue-office/pptx": "^1.0.1",
         "axios": "^1.10.0",
         "crypto-js": "^4.2.0",
+        "dayjs": "^1.11.19",
         "element-plus": "^2.10.2",
         "highlight.js": "^11.11.1",
         "hls.js": "^1.6.7",
@@ -4074,9 +4075,9 @@
       }
     },
     "node_modules/dayjs": {
-      "version": "1.11.13",
-      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
-      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+      "version": "1.11.19",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
       "license": "MIT"
     },
     "node_modules/debug": {

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "@vue-office/pptx": "^1.0.1",
     "axios": "^1.10.0",
     "crypto-js": "^4.2.0",
+    "dayjs": "^1.11.19",
     "element-plus": "^2.10.2",
     "highlight.js": "^11.11.1",
     "hls.js": "^1.6.7",

+ 6 - 0
src/api/date/dateUtil.js

@@ -0,0 +1,6 @@
+import dayjs from "dayjs";
+
+export function formatLocalDateTime(time, format = 'YYYY-MM-DD HH:mm:ss') {
+    if (!time) return '' // 处理空值,避免报错
+    return dayjs(time).format(format)
+}

+ 10 - 0
src/api/login/login.js

@@ -47,4 +47,14 @@ export function smsCode(headers,data){
         data,
         headers: headers,
     })
+}
+
+// 注册
+export function registerSignUp(headers,data){
+    return axios({
+        url: "bjdxWeb/web/register",
+        method: 'post',
+        data: data,
+        headers: headers,
+    })
 }

+ 17 - 0
src/api/user/user.js

@@ -0,0 +1,17 @@
+import axios from '@/utils/request';
+
+// 用户信息
+export function getUserInfo(userId) {
+  return axios({
+    url: 'bjdxWeb/web/getUserInfo?userId=' + userId,
+    method: 'get'
+  });
+}
+
+// 激活邀请码
+export function activateInviteCode(inviteCode){
+  return axios({
+    url: `bjdxWeb/web/activateInviteCode?inviteCode=${inviteCode}`,
+    method: 'post'
+  })
+}

BIN=BIN
src/assets/images/homeBG.png


BIN=BIN
src/assets/images/weblogin.png


+ 60 - 82
src/components/HomePage.vue

@@ -5,10 +5,12 @@
         <span>{{ platformTitle }}</span>
         <div class="dropdown-box">
           <!-- 下拉菜单 -->
-          <el-dropdown v-model="selectedGrade" @command="handleGradeSelect" popper-class="no-arrow-dropdown">
+          <el-dropdown v-model="selectedGrade" @command="handleGradeSelect" @visible-change="handleVisibleChange" popper-class="no-arrow-dropdown">
             <el-button type="primary">
               {{ selectedGrade }}
-              <el-icon class="el-icon--right"><ArrowDown /></el-icon>
+              <!-- 根据下拉框状态显示不同的箭头图标 -->
+              <el-icon class="el-icon--right" v-if="!dropdownVisible"><ArrowDownBold /></el-icon>
+              <el-icon class="el-icon--right" v-else><ArrowUpBold /></el-icon>
             </el-button>
             <template #dropdown>
               <el-dropdown-menu class="dropdown-menu">
@@ -25,38 +27,18 @@
       </div>
       <div class="inner-box right-box">
         <div class="top-right-box">
+          <!-- 动态渲染按钮 -->
           <el-button
+            v-for="button in buttonConfigs"
+            :key="button.name"
             round
             class="top-right-btn"
-            :class="{ 'is-active': selectedButton === 'AI编程课' }"
-            @click="router.push(blocklyRoutes.home)"
-            >AI编程课</el-button
-          >
-          <el-button
-            round
-            class="top-right-btn"
-            :class="{ 'is-active': selectedButton === 'AI实验课' }"
-            @click="router.push(aiCourseRoutes.home)"
-            >AI实验课</el-button
-          >
-          <el-button
-            round
-            class="top-right-btn"
-            :class="{ 'is-active': selectedButton === 'AI艺术课' }"
-            @click="
-              ElMessage.warning('此版本未开放,敬请期待!')
-            "
-            >AI艺术课</el-button
+            :class="{ 'is-active': selectedButton === button.name }"
+            @click="handleButtonClick(button)"
+            >{{ button.name }}</el-button
           >
           <!-- 用户名显示 -->
-          <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>
@@ -99,8 +81,10 @@
 import { ref, onMounted, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import { ClassList } from '@/api/class.js'
-import {ArrowDown} from '@element-plus/icons-vue'
+import {ArrowUpBold, ArrowDownBold} from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
+import UserInfoPopover from '@/components/user/UserInfoPopover.vue'
+
 
 // 导入图片
 import intelligenceImg from '@/assets/images/intelligence.png'
@@ -114,35 +98,40 @@ import logoutIcon from '@/assets/icon/logout.png'
 import {logoutLogic, removeLocalStorageKey} from '@/utils/loginUtils.js'
 import {aiCourseRoutes, blocklyRoutes, homeRoutes} from "@/router/index.js";
 
+
+
 // 平台标题响应式变量
 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) => {
+  selectedButton.value = button.name
+  router.push(button.route)
 }
 
-// 默认选中 AI 通识课
-const selectedButton = ref('AI通识课')
+// 按钮配置数组
+const buttonConfigs = ref([
+  { name: 'AI编程课', route: blocklyRoutes.home },
+  { name: 'AI实验课', route: aiCourseRoutes.home }
+])
+// 取消默认选中状态
+const selectedButton = ref('')
 
 // 图片路径
 const indexImages = ref([intelligenceImg, roomImg, testImg, studyImg])
-// AI初体验
+
+// 智能课
 const goToAIGeneralCourse = title => {
   router.push({ path: '/ai-general-course', state: { title } })
 }
+
 //AI实验室
 const goToAILab = () => {
   router.push({
@@ -170,8 +159,16 @@ const goToPersonalized = () =>{
 const selectedGrade = ref(localStorage.getItem('selectedGrade') || '')
 // 年级ID存储变量
 const selectedGradeId = ref(localStorage.getItem('selectedGradeId') || '')
+// 下拉框可见性状态
+const dropdownVisible = ref(false)
 // 获取年级
 const classData = ref([])
+
+// 处理下拉框显示/隐藏事件
+const handleVisibleChange = (visible) => {
+  dropdownVisible.value = visible
+}
+
 const fetchCtTypes = async () => {
   try {
     const response = await ClassList()
@@ -219,20 +216,17 @@ const handleGradeSelect = (command) => {
   }
 }
 
+
+
 onMounted(() => {
   fetchCtTypes()
   // 初始化平台标题
   updatePlatformTitle()
-  // 初始化用户名
-  updateUserName()
   // storage事件监听器,监听其他标签页对localStorage的修改
   window.addEventListener('storage', (e) => {
     if (e.key === 'tenantName') {
       updatePlatformTitle()
     }
-    if (e.key === 'userName') {
-      updateUserName()
-    }
   })
 
   //删除所有以token开头的键值对
@@ -254,38 +248,8 @@ window.updateTenantName = (newName) => {
 @function rpx($px) {
   @return math.div($px, 750) * 100vw;
 }
-.logout-box {
-  width: rpx(100);
-  position: fixed;
-}
-.logout-box-btn {
-  width: rpx(65); // 使用 rpx 函数设置按钮宽度
-  height: rpx(15); // 使用 rpx 函数设置按钮高度
-  margin: rpx(10) rpx(10) 0 0; // 使用 rpx 函数设置外边距
-  background-color: transparent;
-  color: white;
-  border: none; // 移除默认边框
-  font-size: rpx(7); // 使用 rpx 函数设置字体大小
-  outline: none; // 移除默认的外边框
-}
-.logout-box-btn img {
-  width: rpx(10);
-}
-// 用户名显示
-.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;
@@ -413,7 +377,7 @@ window.updateTenantName = (newName) => {
   display: flex;
   justify-content: flex-end;
   align-items: center;
-  margin-right: rpx(25);
+  margin-right: rpx(60);
 }
 .top-right-box {
   width: 100%;
@@ -496,12 +460,14 @@ window.updateTenantName = (newName) => {
   );
   box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
 }
+
+
 </style>
 
 
 <style lang="scss">
-/* 消除小三角 */
-.el-popper__arrow{
+/* 消除非user-name-box的小三角 */
+.no-arrow-dropdown .el-popper__arrow{
   display: none;
 }
 .el-popper.is-light,
@@ -513,4 +479,16 @@ window.updateTenantName = (newName) => {
 .el-dropdown__popper{
   --el-dropdown-menuItem-hover-color: none;
 }
+/* 移除用户名下拉菜单的焦点边框 */
+.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>

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

@@ -0,0 +1,371 @@
+<template>
+  <!-- 用户信息下拉菜单 -->
+  <el-dropdown @visible-change="handleVisibleChange">
+    <div class="user-name-box">
+      {{ userInfo.nickname }}
+      <!-- 根据下拉框状态显示不同的箭头图标 -->
+      <el-icon v-if="!dropdownVisible" class="arrow-icon"><ArrowDownBold /></el-icon>
+      <el-icon v-else class="arrow-icon"><ArrowUpBold /></el-icon>
+    </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 class="course-item">
+              <div class="check-circle">✓</div>
+              <span>通识课:</span>
+              <span v-if="userInfo.isRegister">{{ userInfo.courseExpireTime ? userInfo.courseExpireTime+' 到期' : '无权限' }}</span>
+              <span v-else>{{userInfo.roleRouteSet.includes(ROLE_KEY.COURSE) ? '已开通' : '无权限'}}</span>
+            </div>
+            <div class="course-item">
+              <div class="check-circle">✓</div>
+              <span>编程课:</span>
+              <span v-if="userInfo.isRegister">{{ userInfo.blocklyExpireTime ? userInfo.blocklyExpireTime+' 到期' : '无权限' }}</span>
+              <span v-else>{{userInfo.roleRouteSet.includes(ROLE_KEY.BLOCKLY) ? '已开通' : '未开通'}}</span>
+            </div>
+            <div class="course-item">
+              <div class="check-circle">✓</div>
+              <span>AI实验课:</span>
+              <span v-if="userInfo.isRegister">{{ userInfo.aiCourseExpireTime ? userInfo.aiCourseExpireTime+' 到期' : '无权限' }}</span>
+              <span v-else>{{userInfo.roleRouteSet.includes(ROLE_KEY.AI_COURSE) ? '已开通' : '未开通'}}</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"
+// 用户信息接口
+import {activateInviteCode, getUserInfo} from '@/api/user/user.js'
+// 图标
+import { ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
+import {formatLocalDateTime} from "@/api/date/dateUtil.js";
+import {CONFIG, ROLE_KEY, setCacheWithExpiry} from "@/utils/roleUtils.js";
+// 激活码
+const activationCode = ref("")
+// 用户名信息变量
+const userInfo = ref({
+  userId: null,
+  userName: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME,
+  nickname: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME,
+  isRegister: true,
+  roleRouteSet: [],
+  courseExpireTime: null,
+  blocklyExpireTime: null,
+  aiCourseExpireTime: null,
+})
+
+// 下拉框可见性状态
+const dropdownVisible = ref(false)
+
+// 处理下拉框显示/隐藏事件
+const handleVisibleChange = (visible) => {
+  dropdownVisible.value = visible
+}
+
+// 获取用户信息
+const fetchUserInfo = async () => {
+  userInfo.value.userName = localStorage.getItem('userName') || import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME
+
+  try {
+    const userId = localStorage.getItem('userId');
+    // 检查userId是否存在
+    if (!userId) {
+      console.warn('用户未登录,无法获取用户信息');
+      return;
+    }
+    const res = await getUserInfo(userId);
+
+    console.log('用户信息:', res);
+    if (res.code === 0 && res.data) {
+      let user = res.data;
+
+      userInfo.value = {...user}
+      if (user.isRegister) {
+        userInfo.value.courseExpireTime = formatLocalDateTime(user.courseExpireTime);
+        userInfo.value.blocklyExpireTime = formatLocalDateTime(user.blocklyExpireTime);
+        userInfo.value.aiCourseExpireTime = formatLocalDateTime(user.aiCourseExpireTime);
+      }
+      localStorage.setItem('userName', user.nickname || user.userName);
+      localStorage.setItem('userInfo', user.nickname || user.userName);
+
+      //缓存用户角色路由
+      setCacheWithExpiry(CONFIG.USER_ROLE_ROUTE_KEY, user.roleRouteSet);
+    }
+  } catch (error) {
+    console.error('获取用户信息失败:', error);
+  }
+}
+
+//激活邀请码
+const activeInviteCode = async (inviteCode) => {
+  // 调用activeInviteCode API 激活邀请码
+  const activeRes = await activateInviteCode(inviteCode)
+
+  // 检查是否激活成功
+  if (activeRes && activeRes.code === 0) {
+    ElMessage.success('邀请码激活成功')
+    // 获取用户web权限角色
+    await fetchUserInfo();
+  } else {
+    ElMessage.error(activeRes?.message || '邀请码激活失败')
+  }
+}
+
+// 获取当前路由对象
+const router = useRouter()
+// 退出登录
+const LogoutClick = async () => {
+  await logoutLogic(router, homeRoutes.login)
+}
+
+// 处理激活按钮点击
+const handleActivation = () => {
+  if (!activationCode.value) {
+    ElMessage.warning('请输入激活码')
+    return
+  }
+
+  // 这里可以添加激活码验证逻辑
+  activeInviteCode(activationCode.value)
+  activationCode.value = ''
+}
+
+onMounted(() => {
+  // 获取用户信息
+  fetchUserInfo()
+  // 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);
+  gap: rpx(3);
+}
+
+// 箭头图标样式
+.arrow-icon {
+  font-size: rpx(7);
+  transition: transform 0.3s ease;
+}
+
+// 用户信息弹框样式
+.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>

+ 7 - 2
src/router/index.js

@@ -14,7 +14,11 @@ const routes = [
   { path: '/ai-login', component: () => import('../views/AiCourseLogin.vue') },
   //【blockly编程课】免租户登录
   { path: '/blockly-login', meta: {TENANT: '内部测试租户'}, component: () => import('../views/BlocklyLogin.vue') },
-
+  // 网页版登录注册页
+  {
+    path: '/register-login',
+    component: () => import('../views/RegisterLogin.vue')
+  },
   // 【通识课】首页
   {
     path: '/home',
@@ -209,7 +213,8 @@ const loginToHomeMap = {
   '/quick-login': homeRoutes.home,
   '/promotion-login': homeRoutes.home,
   '/blockly-login': blocklyRoutes.home,
-  '/ai-login': aiCourseRoutes.home
+  '/ai-login': aiCourseRoutes.home,
+   '/register-login': homeRoutes.home
 }
 
 const router = createRouter({

+ 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 {

+ 17 - 6
src/utils/loginUtils.js

@@ -1,11 +1,17 @@
-import { ref, computed } from 'vue'
-import { getTenantIdByName, login, smsLogin, smsCode, logout } from '@/api/login/login.js'
-import { ElLoading, ElMessage } from 'element-plus'
-import { refreshAllDictData } from './dictUtils.js';
+import {computed, ref} from 'vue'
+import {
+  getTenantIdByName,
+  login,
+  logout,
+  smsCode,
+  smsLogin
+} from '@/api/login/login.js'
+import {ElLoading, ElMessage} from 'element-plus'
+import {refreshAllDictData} from './dictUtils.js';
 // CryptoJS 库(用于密码加密)
 import CryptoJS from 'crypto-js';
 // 消息工具
-import { Message } from './message/Message.js';
+import {Message} from './message/Message.js';
 import {CONFIG, refreshRoleRoute} from "@/utils/roleUtils.js";
 import {homeRoutes} from "@/router/index.js";
 
@@ -14,6 +20,7 @@ const SECRET_KEY = import.meta.env.VITE_SECRET_KEY;
 
 //需要删除的缓存key列表
 const CACHE_KEYS_TO_DELETE = [
+  'userId',
   'token',
   'isLoggedIn',
   CONFIG.USER_ROLE_ROUTE_KEY,
@@ -168,9 +175,13 @@ const loginLogic = async (loginForm, tenantId, isAuthorized, router, redirectPat
       // 存储登录状态
       localStorage.setItem('isLoggedIn', 'true')
       localStorage.setItem('token', res.data.accessToken)
+      
+      // 登录成功后存储用户ID
+      if (res.data.userId) {
+        localStorage.setItem('userId', res.data.userId)
+      }
 
       // 总是存储用户名和租户名称
-      localStorage.setItem('userName', loginForm.username)
       localStorage.setItem('tenantName', loginForm.tenantName)
       // 存储记住我状态
       localStorage.setItem('rememberMe', loginForm.rememberMe)

+ 24 - 9
src/utils/roleUtils.js

@@ -8,20 +8,19 @@ const CONFIG = {
     EXPIRY_SUFFIX: '_expiry',
     EXPIRY_TIME: 24 * 60 * 60 * 1000 // 24小时
 }
+const ROLE_KEY = {
+    COURSE: 'course',
+    BLOCKLY: 'blockly',
+    AI_COURSE: 'aiCourse'
+}
 
 /**
  * 刷新角色路由缓存
  * @returns {Promise<Array>} - 角色路由数组
  */
-export const refreshRoleRoute = async (refresh = true) => {
+export const refreshRoleRoute = async () => {
     try {
 
-        // 检查缓存是否存在且未过期
-        const cachedData = localStorage.getItem(CONFIG.USER_ROLE_ROUTE_KEY);
-        if (!refresh && cachedData && !isCacheExpired(CONFIG.USER_ROLE_ROUTE_KEY)) {
-            return JSON.parse(cachedData);
-        }
-
         // 缓存不存在或已过期,从服务器获取
         const res = await getRoleRoute();
         if (res.code === 0) {
@@ -31,7 +30,8 @@ export const refreshRoleRoute = async (refresh = true) => {
 
             return res.data;
         } else {
-            throw new Error('获取角色路由数据失败');
+            console.error(`获取角色路由数据失败:`, res.msg);
+            return [];
         }
     } catch (error) {
         console.error(`获取角色路由数据失败:`, error);
@@ -70,4 +70,19 @@ const setCacheWithExpiry = (key, data) => {
 };
 
 
-export { CONFIG };
+/**
+ * 取指定key的缓存
+ * @returns {any} - 缓存数据
+ */
+const getRoleRouteCache = () => {
+    // 检查缓存是否存在且未过期
+    const cachedData = localStorage.getItem(CONFIG.USER_ROLE_ROUTE_KEY);
+    if (cachedData && !isCacheExpired(CONFIG.USER_ROLE_ROUTE_KEY)) {
+        return JSON.parse(cachedData);
+    }
+
+    return refreshRoleRoute();
+}
+
+
+export { CONFIG,ROLE_KEY,setCacheWithExpiry,getRoleRouteCache };

+ 15 - 4
src/views/AIPage/AIGeneralCourse.vue

@@ -61,10 +61,12 @@
           </div>
           <div class="dropdown-box">
             <!-- 下拉菜单 -->
-            <el-dropdown v-model="selectedGrade" @command="handleGradeSelect">
+            <el-dropdown v-model="selectedGrade" @command="handleGradeSelect" @visible-change="handleVisibleChange">
               <el-button type="primary">
-                {{ selectedGrade
-                }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
+                {{ selectedGrade }}
+                <!-- 根据下拉框状态显示不同的箭头图标 -->
+                <el-icon class="el-icon--right" v-if="!dropdownVisible"><ArrowDownBold /></el-icon>
+                <el-icon class="el-icon--right" v-else><ArrowUpBold /></el-icon>
               </el-button>
               <template #dropdown>
                 <el-dropdown-menu class="dropdown-menu">
@@ -145,7 +147,8 @@ import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'
 import { ClassList, ClassOutline,ClassOutlineSc } from '@/api/class.js'
 //  Element Plus 组件引入
 import {
-  ArrowDown,
+  ArrowUpBold,
+  ArrowDownBold,
   ArrowRightBold,
   Expand,
   Reading,
@@ -167,6 +170,14 @@ const selectedGrade = ref('')
 // 添加抽屉显示状态
 const drawerVisible = ref(true)
 
+// 下拉框可见性状态
+const dropdownVisible = ref(false)
+
+// 处理下拉框显示/隐藏事件
+const handleVisibleChange = (visible) => {
+  dropdownVisible.value = visible
+}
+
 // 实操课
 const ClassOutlineScData = ref([])
 

+ 131 - 2
src/views/AiCourseLogin.vue

@@ -81,6 +81,7 @@
         </el-form>
         <!-- 多选框 -->
         <div class="check-box">
+          <router-link :to="{path: '/register-login', query: {bgImage: 'aiCourseBG', loginType: 'aiCourseLogin'}}" class="register-link">没有账号?立即注册</router-link>
           <el-checkbox
               v-model="loginData.loginForm.rememberMe"
               label="记住我"
@@ -289,17 +290,26 @@ onMounted(() => {
   height: rpx(18);
   margin: rpx(5) auto;
   display: flex;
-  justify-content: flex-end;
+  justify-content: space-between;
   align-items: center;
 }
 .check-box .el-checkbox {
   color: white;
-  padding-right: rpx(10);
+  // padding-right: rpx(10);
   font-size: rpx(6);
 }
 .el-checkbox ::v-deep(.el-checkbox__label){
   font-size: rpx(6);
 }
+.check-box .register-link {
+  color: white;
+  font-size: rpx(6);
+  text-decoration: none;
+  margin-right: rpx(5);
+}
+.check-box .register-link:hover {
+  text-decoration: underline;
+}
 .check-box .forgot-password {
   color: white;
   font-size: rpx(6);
@@ -342,4 +352,123 @@ onMounted(() => {
   // background: linear-gradient(to bottom, #78c0ff, #0070f3);
   border: none;
 }
+
+// 移动端响应式设计
+@media screen and (max-width: 768px) {
+  .login-content {
+    flex-direction: column;
+  }
+  
+  .bg-image-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: -1;
+    background-size: cover;
+    background-position: center;
+  }
+  
+  .login-wrapper {
+    flex: none;
+    width: 70%;
+    margin: 0 auto;
+    padding: 30px;
+    background: rgba(0, 17, 105, 0.85);
+    border-radius: 15px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    min-height: 40%;
+    overflow-y: auto;
+  }
+  
+  .login-input {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+  
+  .login-input span {
+    font-size: rpx(18);
+    padding-bottom: rpx(20);
+  }
+  
+  .input-item {
+    width: 100%;
+    align-items: stretch;
+  }
+  
+  .input-item .el-input {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+    font-size: rpx(14) !important;
+    margin-bottom: rpx(25) !important;
+  }
+  
+  .el-input ::v-deep(.el-input__wrapper) {
+    height: rpx(45) !important;
+  }
+  
+  .el-input ::v-deep(.el-input__inner) {
+    height: rpx(45) !important;
+    font-size: rpx(14) !important;
+  }
+  
+  .login-btn {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+    font-size: rpx(16) !important;
+    margin: rpx(25) 0 !important;
+    letter-spacing: rpx(15);
+  }
+  
+  .check-box {
+    width: 100% !important;
+    max-width: none;
+    height: auto;
+    margin: rpx(15) 0 !important;
+  }
+  
+  .check-box .el-checkbox {
+    font-size: rpx(12) !important;
+  }
+  
+  .check-box .register-link {
+    font-size: rpx(12) !important;
+    margin-right: rpx(15) !important;
+  }
+  
+  .el-checkbox ::v-deep(.el-checkbox__label) {
+    font-size: rpx(12) !important;
+  }
+  
+  .sms-code-container {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+  }
+  
+  .sms-input {
+    width: calc(100% - rpx(75)) !important;
+    height: rpx(45) !important;
+  }
+  
+  .get-code-btn {
+    width: rpx(75) !important;
+    height: rpx(45) !important;
+    font-size: rpx(10) !important;
+  }
+  
+  .el-form-item ::v-deep(.el-form-item__error) {
+    top: rpx(50) !important;
+    font-size: rpx(10) !important;
+  }
+}
 </style>

+ 130 - 2
src/views/BlocklyLogin.vue

@@ -82,6 +82,7 @@
         </el-form>
         <!-- 多选框 -->
         <div class="check-box">
+          <router-link :to="{path: '/register-login', query: {bgImage: 'blocklyBG', loginType: 'blocklyLogin'}}" class="register-link">没有账号?立即注册</router-link>
           <el-checkbox
               v-model="loginData.loginForm.rememberMe"
               label="记住我"
@@ -296,17 +297,26 @@ onMounted(() => {
   height: rpx(18);
   margin: rpx(5) auto;
   display: flex;
-  justify-content: flex-end;
+  justify-content: space-between;
   align-items: center;
 }
 .check-box .el-checkbox {
   color: white;
-  padding-right: rpx(10);
+  // padding-right: rpx(10);
   font-size: rpx(6);
 }
 .el-checkbox ::v-deep(.el-checkbox__label){
   font-size: rpx(6);
 }
+.check-box .register-link {
+  color: white;
+  font-size: rpx(6);
+  text-decoration: none;
+  margin-right: rpx(5);
+}
+.check-box .register-link:hover {
+  text-decoration: underline;
+}
 .check-box .forgot-password {
   color: white;
   font-size: rpx(6);
@@ -349,4 +359,122 @@ onMounted(() => {
   // background: linear-gradient(to bottom, #78c0ff, #0070f3);
   border: none;
 }
+// 移动端响应式设计
+@media screen and (max-width: 768px) {
+  .login-content {
+    flex-direction: column;
+  }
+  
+  .bg-image-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: -1;
+    background-size: cover;
+    background-position: center;
+  }
+  
+  .login-wrapper {
+    flex: none;
+    width: 70%;
+    margin: 0 auto;
+    padding: 30px;
+    background: rgba(0, 17, 105, 0.85);
+    border-radius: 15px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    min-height: 40%;
+    overflow-y: auto;
+  }
+  
+  .login-input {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+  
+  .login-input span {
+    font-size: rpx(18);
+    padding-bottom: rpx(20);
+  }
+  
+  .input-item {
+    width: 100%;
+    align-items: stretch;
+  }
+  
+  .input-item .el-input {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+    font-size: rpx(14) !important;
+    margin-bottom: rpx(25) !important;
+  }
+  
+  .el-input ::v-deep(.el-input__wrapper) {
+    height: rpx(45) !important;
+  }
+  
+  .el-input ::v-deep(.el-input__inner) {
+    height: rpx(45) !important;
+    font-size: rpx(14) !important;
+  }
+  
+  .login-btn {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+    font-size: rpx(16) !important;
+    margin: rpx(25) 0 !important;
+    letter-spacing: rpx(15);
+  }
+  
+  .check-box {
+    width: 100% !important;
+    max-width: none;
+    height: auto;
+    margin: rpx(15) 0 !important;
+  }
+  
+  .check-box .el-checkbox {
+    font-size: rpx(12) !important;
+  }
+  
+  .check-box .register-link {
+    font-size: rpx(12) !important;
+    margin-right: rpx(15) !important;
+  }
+  
+  .el-checkbox ::v-deep(.el-checkbox__label) {
+    font-size: rpx(12) !important;
+  }
+  
+  .sms-code-container {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+  }
+  
+  .sms-input {
+    width: calc(100% - rpx(75)) !important;
+    height: rpx(45) !important;
+  }
+  
+  .get-code-btn {
+    width: rpx(75) !important;
+    height: rpx(45) !important;
+    font-size: rpx(10) !important;
+  }
+  
+  .el-form-item ::v-deep(.el-form-item__error) {
+    top: rpx(50) !important;
+    font-size: rpx(10) !important;
+  }
+}
 </style>

+ 18 - 3
src/views/Login.vue

@@ -82,10 +82,10 @@
         </el-form>
         <!-- 多选框 -->
         <div class="check-box">
+          <router-link :to="{path: '/register-login', query: {bgImage: 'homeBG', loginType: 'login'}}" class="register-link">没有账号?立即注册</router-link>
           <el-checkbox
               v-model="loginData.loginForm.rememberMe"
               label="记住我"
-              size="large"
           />
         </div>
       </div>
@@ -265,6 +265,7 @@ onMounted(() => {
 }
 .el-form-item ::v-deep(.el-form-item__error) {
   top: rpx(25);
+  font-size: rpx(7);
 }
 .login-btn {
   width: rpx(150);
@@ -286,14 +287,23 @@ onMounted(() => {
   height: rpx(18);
   margin: rpx(5) auto;
   display: flex;
-  justify-content: flex-end;
+  justify-content: space-between;
   align-items: center;
 }
 .check-box .el-checkbox {
   color: white;
-  padding-right: rpx(10);
+  // padding-right: rpx(10);
   font-size: rpx(6);
 }
+.check-box .register-link {
+  color: white;
+  font-size: rpx(6);
+  text-decoration: none;
+  margin-right: rpx(5);
+}
+.check-box .register-link:hover {
+  text-decoration: underline;
+}
 .el-checkbox ::v-deep(.el-checkbox__label){
   font-size: rpx(6);
 }
@@ -427,6 +437,11 @@ onMounted(() => {
     font-size: rpx(12) !important;
   }
   
+  .check-box .register-link {
+    font-size: rpx(12) !important;
+    margin-right: rpx(15) !important;
+  }
+  
   .el-checkbox ::v-deep(.el-checkbox__label) {
     font-size: rpx(12) !important;
   }

+ 486 - 0
src/views/RegisterLogin.vue

@@ -0,0 +1,486 @@
+<template>
+  <!-- 注册输入框 -->
+  <div class="login-content">
+    <!-- 注册输入框 -->
+    <div class="login-wrapper">
+      <!-- 左侧图片背景盒子 -->
+      <div class="login-left">
+        <img :src="registerBG" alt="背景图片" class="bg-image">
+      </div>
+      <!-- 右侧输入框盒子 -->
+      <div class="login-right">
+        <div class="login-input">
+          <span>{{ appTitle }}</span>
+          <el-form
+            ref="loginFormRef"
+            :model="loginForm"
+            :rules="rules"
+            label-width="0px"
+            class="input-item"
+          >
+            <!-- 学校输入框 -->
+            <el-form-item prop="tenantName" v-if="!isBlocklyLogin">
+              <el-input
+                  v-model="loginForm.tenantName"
+                  :prefix-icon="HomeFilled"
+                  placeholder="请输入学校"
+              />
+            </el-form-item>
+            <el-form-item prop="phone">
+              <el-input
+                  v-model="loginForm.phone"
+                  :prefix-icon="Iphone"
+                  placeholder="请输入手机号"
+              />
+            </el-form-item>
+            <el-form-item prop="password">
+              <el-input
+                v-model="loginForm.password"
+                class="password-input"
+                type="password"
+                :prefix-icon="Lock"
+                placeholder="请输入密码"
+                show-password
+              />
+            </el-form-item>
+            <el-form-item prop="confirmPassword">
+              <el-input
+                v-model="loginForm.confirmPassword"
+                class="password-input"
+                type="password"
+                :prefix-icon="Lock"
+                placeholder="请再次确认密码"
+                show-password
+              />
+            </el-form-item>
+            <el-form-item prop="inviteCode">
+              <el-input v-model="loginForm.inviteCode" :prefix-icon="Key" placeholder="请输入邀请码" />
+            </el-form-item>
+
+            <!-- 注册按钮 -->
+            <el-form-item>
+              <div class="login-btn-container">
+                <el-button @click="goBackToLogin" link class="register-link">已有账号,去登录</el-button>
+                <el-button @click="handleRegister" class="login-btn" type="primary">注册</el-button>
+              </div>
+            </el-form-item>
+
+          </el-form>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch } from "vue";
+import { Lock, Iphone, Key, HomeFilled } from '@element-plus/icons-vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+import homeBG from "@/assets/images/homeBG.png";
+import blocklyBG from "@/assets/images/blocklyBG.png";
+import aiCourseBG from "@/assets/images/aiCourseBG.png";
+import { registerSignUp } from '@/api/login/login.js';
+import { getTenantId } from '@/utils/loginUtils.js';
+
+// 获取环境变量
+const appTitle = ref("注册账号");
+
+const router = useRouter();
+
+const loginFormRef = ref(null);
+
+// 获取查询参数
+const bgImageType = computed(() => router.currentRoute.value.query.bgImage || 'homeBG');
+const loginTypePage = computed(() => router.currentRoute.value.query.loginType || 'login');
+
+// 注册表单数据
+const loginForm = ref({
+  tenantName: "", // 学校
+  phone: "", // 手机号
+  password: "", // 密码
+  confirmPassword: "", // 确认密码
+  inviteCode: "", // 邀请码
+  rememberMe: false,
+});
+
+// 判断是否为blocklyLogin类型
+const isBlocklyLogin = computed(() => loginTypePage.value === 'blocklyLogin');
+
+// 设置默认值并监听变化
+watch(isBlocklyLogin, (newVal) => {
+  if (newVal) {
+    loginForm.value.tenantName = '内部测试租户';
+  }
+}, { immediate: true });
+
+// 动态选择背景图
+const registerBG = computed(() => {
+  switch(bgImageType.value) {
+    case 'blocklyBG':
+      return blocklyBG;
+    case 'aiCourseBG':
+      return aiCourseBG;
+    default:
+      return homeBG;
+  }
+});
+
+// 返回登录页
+const goBackToLogin = () => {
+  let loginPath = '/';
+  switch(loginTypePage.value) {
+    case 'blocklyLogin':
+      loginPath = '/blockly-login';
+      break;
+    case 'aiCourseLogin':
+      loginPath = '/ai-login';
+      break;
+    default:
+      loginPath = '/';
+  }
+  router.replace(loginPath);
+};
+
+// 表单验证规则
+const rules = ref({
+  tenantName: [
+    { required: true, message: "请输入学校", trigger: "blur" }
+  ],
+  phone: [
+    { required: true, message: "请输入手机号", trigger: "blur" },
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号格式", trigger: "blur" }
+  ],
+  password: [
+    { required: true, message: "请输入密码", trigger: "blur" },
+    { min: 6, max: 20, message: "密码长度应在6-20位之间", trigger: "blur" }
+  ],
+  confirmPassword: [
+    { required: true, message: "请确认密码", trigger: "blur" },
+    {
+      validator: (rule, value, callback) => {
+        if (value !== loginForm.value.password) {
+          callback(new Error("两次输入的密码不一致"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "blur"
+    }
+  ],
+  inviteCode: [{ required: true, message: "请输入邀请码", trigger: "blur" }],
+});
+
+// 处理注册
+const handleRegister = async () => {
+  try {
+    // 表单验证
+    await loginFormRef.value.validate();
+    // 获取租户名称
+    const tenantName = loginForm.value.tenantName;
+
+    if (tenantName === import.meta.env.VITE_APP_TITLE) {
+      ElMessage.error('此租户不支持注册!');
+      return;
+    }
+
+    // 获取租户ID
+    const tenantId = await getTenantId(tenantName);
+    if (!tenantId) {
+      // 租户验证失败
+      return;
+    }
+    // 准备请求数据
+    const registerData = {
+      username: loginForm.value.phone,
+      nickname: loginForm.value.phone,
+      password: loginForm.value.password,
+      inviteCode: loginForm.value.inviteCode,
+      rememberMe: loginForm.value.rememberMe
+    };
+    // 调用注册接口
+    const res = await registerSignUp({ 'Tenant-Id': tenantId },registerData);
+    // 注册成功处理
+    if (res && res.code === 0) {
+      ElMessage.success('注册成功,请登录');
+      goBackToLogin();
+    } else {
+      ElMessage.error(res && res.message || '注册失败,请稍后重试');
+    }
+  } catch (error) {
+    console.error('注册失败:', error);
+    ElMessage.error('注册失败,请检查输入信息或网络连接');
+  }
+};
+
+</script>
+
+<style scoped lang="scss">
+@use "sass:math";
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.login-content {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  justify-content: center; // 水平居中
+  align-items: center; // 垂直居中
+  background: linear-gradient(to bottom, #001169, #8a78d0);
+}
+
+.login-wrapper {
+  width: rpx(500); // 固定宽度
+  height: rpx(300); // 固定高度
+  max-width: 90%; // 响应式最大宽度
+  padding: 0;
+  border-radius: rpx(15);
+  box-shadow: 0 rpx(5) rpx(15) rgba(0, 0, 0, 0.3);
+  display: flex;
+  overflow: hidden;
+  background-color: white;
+}
+
+/* 左侧图片背景盒子 */
+.login-left {
+  width: 50%;
+  position: relative;
+  overflow: hidden;
+}
+
+/* 背景图片样式 */
+.bg-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+/* 右侧输入框盒子 */
+.login-right {
+  width: 50%;
+  padding: rpx(20);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: white;
+}
+.login-input {
+  width: 80%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  text-align: center;
+}
+
+
+.login-input span {
+  color: black;
+  font-size: rpx(12);
+  padding-bottom: rpx(10);
+  letter-spacing: rpx(1);
+}
+.input-item {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+}
+.el-input ::v-deep(.el-input__wrapper) {
+  border-radius: rpx(5);
+}
+.input-item .el-form-item {
+  margin-bottom: 0;
+  width: 100%;
+}
+.input-item .el-input {
+  width: 100%;
+  height: rpx(22);
+  margin-bottom: rpx(15);
+  font-size: rpx(8);
+}
+.el-form-item ::v-deep(.el-form-item__error) {
+  top: rpx(25);
+  font-size: rpx(7);
+}
+
+/* 按钮容器,用于控制按钮位置 */
+.login-btn-container {
+  width: 100%;
+  display: flex;
+  justify-content: flex-end;
+}
+.register-link {
+  color: black;
+  font-size: rpx(6);
+  text-decoration: none;
+  position: absolute;
+  left: rpx(8);
+  bottom: rpx(10);
+}
+.register-link:hover {
+  text-decoration: underline;
+}
+.login-btn {
+  width: rpx(70); /* 缩小宽度 */
+  height: rpx(25);
+  color: black;
+  font-size: rpx(9);
+  letter-spacing: rpx(5); /* 调整字间距以适应较小宽度 */
+  border-radius: rpx(5);
+  margin: 0;
+  border: none;
+  outline: none;
+  background: linear-gradient(to bottom, #fee78a, #ffce1b);
+  box-shadow: 0 rpx(4) rpx(4) rgb(0, 0, 0, 0.2);
+}
+.check-box {
+  width: 100%;
+  height: rpx(18);
+  margin: rpx(10) 0;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+.check-box .el-checkbox {
+  color: white;
+  padding-right: rpx(10);
+  font-size: rpx(7);
+}
+.el-checkbox ::v-deep(.el-checkbox__label) {
+  font-size: rpx(7);
+}
+.el-checkbox ::v-deep(.el-checkbox__label) {
+  font-size: 14px;
+}
+
+// 移动端响应式设计
+@media screen and (max-width: 768px) {
+  .login-content {
+    flex-direction: column;
+    background: url('@/assets/images/homeBG.png') no-repeat center center fixed;
+    background-size: cover;
+  }
+  
+  .bg-image-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: -1;
+    background-size: cover;
+    background-position: center;
+  }
+  
+  .login-wrapper {
+    flex: none;
+    width: 70%;
+    margin: 0 auto;
+    padding: 30px;
+    border-radius: 15px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    min-height: 40%;
+    overflow-y: auto;
+  }
+
+  .login-left{
+    display: none;
+  }
+  .login-right{
+    width: 100%;
+  }
+  
+  .login-input {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+  
+  .login-input span {
+    font-size: rpx(18);
+    padding-bottom: rpx(20);
+  }
+  
+  .input-item {
+    width: 100%;
+    align-items: stretch;
+  }
+  
+  .input-item .el-input {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+    font-size: rpx(14) !important;
+    margin-bottom: rpx(25) !important;
+  }
+  
+  .el-input ::v-deep(.el-input__wrapper) {
+    height: rpx(45) !important;
+    border-radius: rpx(15);
+  }
+  
+  .el-input ::v-deep(.el-input__inner) {
+    height: rpx(45) !important;
+    font-size: rpx(14) !important;
+  }
+  
+  .login-btn {
+    width: 40% !important;
+    max-width: none;
+    height: rpx(45) !important;
+    font-size: rpx(16) !important;
+    margin: rpx(25) 0 !important;
+    letter-spacing: rpx(15);
+    border-radius: rpx(10);
+  }
+  
+  .check-box {
+    width: 100% !important;
+    max-width: none;
+    height: auto;
+    margin: rpx(15) 0 !important;
+  }
+  
+  .check-box .el-checkbox {
+    font-size: rpx(12) !important;
+  }
+  
+  .el-checkbox ::v-deep(.el-checkbox__label) {
+    font-size: rpx(12) !important;
+  }
+  
+  .sms-code-container {
+    width: 100% !important;
+    max-width: none;
+    height: rpx(45) !important;
+  }
+  
+  .sms-input {
+    width: calc(100% - rpx(75)) !important;
+    height: rpx(45) !important;
+  }
+  
+  .get-code-btn {
+    width: rpx(75) !important;
+    height: rpx(45) !important;
+    font-size: rpx(10) !important;
+  }
+  
+  .el-form-item ::v-deep(.el-form-item__error) {
+    top: rpx(50) !important;
+    font-size: rpx(10) !important;
+  }
+}
+</style>

+ 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%;