refactor: 统一项目名称为互联网产品开发实训系统并优化代码格式

- 替换所有文件中的项目名称“电商互联网产品开发实训系统”为“互联网产品开发实训系统”
- 优化多个文件的代码换行格式,提升可读
- 停用教师端和班级管理中院系/专业相关的旧逻辑
- 设计登录页ui
master
qinzhenpen 3 days ago
parent e175e3e4f9
commit 4b20ea3d99

@ -7,7 +7,7 @@
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE # @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
### ###
# 页面标题 # 页面标题
VITE_APP_TITLE = 电商互联网产品开发实训系统 VITE_APP_TITLE = 互联网产品开发实训系统
# 开发环境配置 # 开发环境配置
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'

@ -7,7 +7,7 @@
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE # @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
### ###
# 页面标题 # 页面标题
VITE_APP_TITLE = 电商互联网产品开发实训系统 VITE_APP_TITLE = 互联网产品开发实训系统
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'production' VITE_APP_ENV = 'production'

@ -1,5 +1,5 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 电商互联网产品开发实训系统 VITE_APP_TITLE = 互联网产品开发实训系统
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'staging' VITE_APP_ENV = 'staging'

1
.gitignore vendored

@ -12,6 +12,7 @@ selenium-debug.log
# Editor directories and files # Editor directories and files
.idea .idea
.workbuddy
.cursor/ .cursor/
.vscode .vscode
*.suo *.suo

@ -1,4 +1,4 @@
# 电商互联网产品开发实训系统 - 项目认知 # 互联网产品开发实训系统 - 项目认知
> 本文件为 AI 助手提供项目上下文,每次新对话开始时会读取此文件。 > 本文件为 AI 助手提供项目上下文,每次新对话开始时会读取此文件。
@ -6,7 +6,7 @@
## 1. 项目基本信息 ## 1. 项目基本信息
- **项目名称**:电商互联网产品开发实训系统 - **项目名称**:互联网产品开发实训系统
- **技术栈**:Vue 3 (Composition API) + Vite 3 + Element Plus 2.2.x + Pinia + Vue Router 4 + ECharts 5.4 + SCSS - **技术栈**:Vue 3 (Composition API) + Vite 3 + Element Plus 2.2.x + Pinia + Vue Router 4 + ECharts 5.4 + SCSS
- **基础框架**:RuoYi-Vue 3.8.6(若依前后端分离系统增强版) - **基础框架**:RuoYi-Vue 3.8.6(若依前后端分离系统增强版)
- **项目路径**:`D:\TZ\dianshang-qianduan` 或 `d:\TZ\dianshang-qianduan` - **项目路径**:`D:\TZ\dianshang-qianduan` 或 `d:\TZ\dianshang-qianduan`

@ -1,14 +1,17 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico" />
<title>电商互联网产品开发实训系统</title> <title>互联网产品开发实训系统</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <!--[if lt IE 11]>
<script>
window.location.href = "/html/ie.html";
</script>
<![endif]-->
<style> <style>
:root { :root {
--ink: #06101f; --ink: #06101f;
@ -37,13 +40,13 @@
height: 100vh; height: 100vh;
color: var(--text); color: var(--text);
font-family: "Microsoft YaHei", "PingFang SC", "Source Han Sans CN", sans-serif; font-family: "Microsoft YaHei", "PingFang SC", "Source Han Sans CN", sans-serif;
background: background: linear-gradient(rgba(92, 192, 255, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(92, 192, 255, 0.08) 1px, transparent 1px), radial-gradient(circle at 18% 18%, rgba(83, 214, 255, 0.14), transparent 28%), radial-gradient(circle at 82% 72%, rgba(143, 247, 209, 0.1), transparent 30%), linear-gradient(135deg, #071426 0%, #06101f 50%, #091827 100%);
linear-gradient(rgba(92, 192, 255, 0.08) 1px, transparent 1px), background-size:
linear-gradient(90deg, rgba(92, 192, 255, 0.08) 1px, transparent 1px), 52px 52px,
radial-gradient(circle at 18% 18%, rgba(83, 214, 255, 0.14), transparent 28%), 52px 52px,
radial-gradient(circle at 82% 72%, rgba(143, 247, 209, 0.1), transparent 30%), auto,
linear-gradient(135deg, #071426 0%, #06101f 50%, #091827 100%); auto,
background-size: 52px 52px, 52px 52px, auto, auto, auto; auto;
} }
#loading-wrapper::before, #loading-wrapper::before,
@ -55,9 +58,7 @@
} }
#loading-wrapper::before { #loading-wrapper::before {
background: background: linear-gradient(120deg, transparent 0 22%, rgba(83, 214, 255, 0.14) 22.3%, transparent 22.8% 62%, rgba(255, 217, 120, 0.12) 62.3%, transparent 62.8%), linear-gradient(0deg, rgba(0, 0, 0, 0.24), transparent 36%, rgba(0, 0, 0, 0.2));
linear-gradient(120deg, transparent 0 22%, rgba(83, 214, 255, 0.14) 22.3%, transparent 22.8% 62%, rgba(255, 217, 120, 0.12) 62.3%, transparent 62.8%),
linear-gradient(0deg, rgba(0, 0, 0, 0.24), transparent 36%, rgba(0, 0, 0, 0.2));
opacity: 0.75; opacity: 0.75;
} }
@ -77,10 +78,10 @@
padding: 34px 38px; padding: 34px 38px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
background: background: linear-gradient(145deg, rgba(14, 42, 68, 0.92), rgba(6, 20, 38, 0.9)), var(--panel);
linear-gradient(145deg, rgba(14, 42, 68, 0.92), rgba(6, 20, 38, 0.9)), box-shadow:
var(--panel); 0 24px 70px rgba(0, 0, 0, 0.34),
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.06); inset 0 1px 0 rgba(255, 255, 255, 0.06);
animation: shell-in 0.7s ease both; animation: shell-in 0.7s ease both;
} }
@ -91,10 +92,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(83, 214, 255, 0.28); border: 1px solid rgba(83, 214, 255, 0.28);
border-radius: 8px; border-radius: 8px;
background: background: linear-gradient(90deg, transparent 23px, rgba(83, 214, 255, 0.11) 24px, transparent 25px), linear-gradient(0deg, transparent 23px, rgba(83, 214, 255, 0.1) 24px, transparent 25px), rgba(5, 22, 39, 0.9);
linear-gradient(90deg, transparent 23px, rgba(83, 214, 255, 0.11) 24px, transparent 25px),
linear-gradient(0deg, transparent 23px, rgba(83, 214, 255, 0.1) 24px, transparent 25px),
rgba(5, 22, 39, 0.9);
background-size: 25px 25px; background-size: 25px 25px;
} }
@ -124,7 +122,9 @@
height: 5px; height: 5px;
border-radius: 99px; border-radius: 99px;
background: var(--mint); background: var(--mint);
box-shadow: 0 15px 0 rgba(211, 239, 255, 0.7), 0 30px 0 rgba(211, 239, 255, 0.42); box-shadow:
0 15px 0 rgba(211, 239, 255, 0.7),
0 30px 0 rgba(211, 239, 255, 0.42);
} }
.learning-card::after { .learning-card::after {
@ -146,7 +146,10 @@
height: 10px; height: 10px;
border-radius: 50%; border-radius: 50%;
background: var(--cyan); background: var(--cyan);
box-shadow: 28px -8px 0 var(--gold), 58px 4px 0 var(--mint), 86px -18px 0 rgba(83, 214, 255, 0.72); box-shadow:
28px -8px 0 var(--gold),
58px 4px 0 var(--mint),
86px -18px 0 rgba(83, 214, 255, 0.72);
} }
.pulse-node::before { .pulse-node::before {
@ -338,9 +341,18 @@
<p class="loading-subtitle">正在同步课程资源、实训数据与学习进度,请稍候。</p> <p class="loading-subtitle">正在同步课程资源、实训数据与学习进度,请稍候。</p>
<div class="progress-track" aria-hidden="true"></div> <div class="progress-track" aria-hidden="true"></div>
<div class="loading-steps" aria-hidden="true"> <div class="loading-steps" aria-hidden="true">
<div class="loading-step"><span>01</span>课程资源</div> <div class="loading-step">
<div class="loading-step"><span>02</span>实训任务</div> <span>01</span>
<div class="loading-step"><span>03</span>学习数据</div> 课程资源
</div>
<div class="loading-step">
<span>02</span>
实训任务
</div>
<div class="loading-step">
<span>03</span>
学习数据
</div>
</div> </div>
</div> </div>
</div> </div>
@ -348,5 +360,4 @@
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

@ -1,7 +1,8 @@
<template> <template>
<div :class="['navbar', { 'teacher-navbar': role === 3 || role === 1, 'student-navbar': role === 4 }]"> <div :class="['navbar', { 'teacher-navbar': role === 3 || role === 1, 'student-navbar': role === 4 }]">
<!-- 背景装饰(学生端已移除,仅平台/学校管理员保留) --> <!-- 背景装饰(学生端已移除,仅平台/学校管理员保留) -->
<svg v-if="role === 1 || role === 2" class="nav-bg-decoration" viewBox="0 0 1920 60" preserveAspectRatio="xMidYMid slice"> <svg v-if="role === 1 || role === 2" class="nav-bg-decoration" viewBox="0 0 1920 60"
preserveAspectRatio="xMidYMid slice">
<defs> <defs>
<linearGradient id="navGrad" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="navGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#001529;stop-opacity:0.95" /> <stop offset="0%" style="stop-color:#001529;stop-opacity:0.95" />
@ -47,26 +48,32 @@
<div class="logo-area"> <div class="logo-area">
<svg class="logo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none"> <svg class="logo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none">
<!-- 购物车主体(车身 + 车把)--> <!-- 购物车主体(车身 + 车把)-->
<path d="M3 3h2l2.2 12h10.5l2.3-8H6" stroke="#00aaff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="rgba(0,170,255,0.12)"/> <path d="M3 3h2l2.2 12h10.5l2.3-8H6" stroke="#00aaff" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" fill="rgba(0,170,255,0.12)" />
<!-- 代码左尖括号 < 叠加在车身 = 产品开发 --> <!-- 代码左尖括号 < 叠加在车身 = 产品开发 -->
<path d="M10 7.5L7.8 10l2.2 2.5" stroke="#00aaff" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/> <path d="M10 7.5L7.8 10l2.2 2.5" stroke="#00aaff" stroke-width="1.3" stroke-linecap="round"
stroke-linejoin="round" opacity="0.9" />
<!-- 代码右尖括号 > 叠加在车身 = 产品开发 --> <!-- 代码右尖括号 > 叠加在车身 = 产品开发 -->
<path d="M17 7.5L19.2 10l-2.2 2.5" stroke="#00aaff" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/> <path d="M17 7.5L19.2 10l-2.2 2.5" stroke="#00aaff" stroke-width="1.3" stroke-linecap="round"
stroke-linejoin="round" opacity="0.9" />
<!-- 左车轮 --> <!-- 左车轮 -->
<circle cx="8" cy="18.5" r="1.5" stroke="#00aaff" stroke-width="1.4" /> <circle cx="8" cy="18.5" r="1.5" stroke="#00aaff" stroke-width="1.4" />
<!-- 右车轮 --> <!-- 右车轮 -->
<circle cx="17" cy="18.5" r="1.5" stroke="#00aaff" stroke-width="1.4" /> <circle cx="17" cy="18.5" r="1.5" stroke="#00aaff" stroke-width="1.4" />
</svg> </svg>
<span class="nacber-name" v-if="role !== 3">电商互联网产品开发实训系统</span> <span class="nacber-name" v-if="role !== 3">互联网产品开发实训系统</span>
<span class="nacber-name" v-else>互联网产品开发实训系统</span> <span class="nacber-name" v-else>互联网产品开发实训系统</span>
</div> </div>
</div> </div>
<div :class="['el-ment', { 'platform-nav': role === 1 || role === 2, 'school-admin-nav': role === 2, 'teacher-nav': role === 3 }]"> <div
:class="['el-ment', { 'platform-nav': role === 1 || role === 2, 'school-admin-nav': role === 2, 'teacher-nav': role === 3 }]">
<template v-for="item in roleList" :key="item.name"> <template v-for="item in roleList" :key="item.name">
<div :class="['el-ment-item', { 'el-ment-item-active': isActive(item) }]" @click="go(item)"> <div :class="['el-ment-item', { 'el-ment-item-active': isActive(item) }]" @click="go(item)">
<span class="menu-indicator"></span> <span class="menu-indicator"></span>
<el-icon v-if="role === 4 && item.icon" class="menu-ico"><component :is="item.icon" /></el-icon> <el-icon v-if="role === 4 && item.icon" class="menu-ico">
<component :is="item.icon" />
</el-icon>
<span class="menu-text">{{ item.name }}</span> <span class="menu-text">{{ item.name }}</span>
</div> </div>
</template> </template>
@ -81,7 +88,8 @@
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="avatar-container"> <div class="avatar-container">
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click" :popper-class="role === 3 || role === 1 ? 'teacher-dropdown-popper' : role === 4 ? 'student-dropdown-popper' : ''"> <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click"
:popper-class="role === 3 || role === 1 ? 'teacher-dropdown-popper' : role === 4 ? 'student-dropdown-popper' : ''">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<div class="avatar-ring"> <div class="avatar-ring">
<img src="../../assets/images/头像.webp" class="user-avatar" /> <img src="../../assets/images/头像.webp" class="user-avatar" />
@ -99,14 +107,16 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<el-dropdown-menu class="dropdown-menu-custom"> <el-dropdown-menu class="dropdown-menu-custom">
<el-dropdown-item divided command="changePassword" class="dropdown-item"> <el-dropdown-item divided command="changePassword" class="dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" /> <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg> </svg>
<span>修改密码</span> <span>修改密码</span>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item divided command="logout" class="dropdown-item logout"> <el-dropdown-item divided command="logout" class="dropdown-item logout">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" /> <polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" /> <line x1="21" y1="12" x2="9" y2="12" />
@ -124,37 +134,37 @@
</template> </template>
<script setup> <script setup>
import { ElMessageBox } from "element-plus"; import { ElMessageBox } from "element-plus"
import { HomeFilled, Reading, Document, TrendCharts, DataLine } from "@element-plus/icons-vue"; import { HomeFilled, Reading, Document, TrendCharts, DataLine } from "@element-plus/icons-vue"
import Breadcrumb from "@/components/Breadcrumb"; import Breadcrumb from "@/components/Breadcrumb"
import TopNav from "@/components/TopNav"; import TopNav from "@/components/TopNav"
import Hamburger from "@/components/Hamburger"; import Hamburger from "@/components/Hamburger"
import Screenfull from "@/components/Screenfull"; import Screenfull from "@/components/Screenfull"
import SizeSelect from "@/components/SizeSelect"; import SizeSelect from "@/components/SizeSelect"
import HeaderSearch from "@/components/HeaderSearch"; import HeaderSearch from "@/components/HeaderSearch"
import RuoYiGit from "@/components/RuoYi/Git"; import RuoYiGit from "@/components/RuoYi/Git"
import RuoYiDoc from "@/components/RuoYi/Doc"; import RuoYiDoc from "@/components/RuoYi/Doc"
import ChangePasswordDialog from "@/views/profile/ChangePasswordDialog.vue"; import ChangePasswordDialog from "@/views/profile/ChangePasswordDialog.vue"
import useAppStore from "@/store/modules/app"; import useAppStore from "@/store/modules/app"
import useUserStore from "@/store/modules/user"; import useUserStore from "@/store/modules/user"
import useSettingsStore from "@/store/modules/settings"; import useSettingsStore from "@/store/modules/settings"
import { getCachedStudentTrainingTasks, loadStudentTrainingTasks } from "@/api/trainingTask"; import { getCachedStudentTrainingTasks, loadStudentTrainingTasks } from "@/api/trainingTask"
import { getComprehensiveTopTask } from "@/utils/studentTrainingNavigation"; import { getComprehensiveTopTask } from "@/utils/studentTrainingNavigation"
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance()
const router = useRouter(); const router = useRouter()
const appStore = useAppStore(); const appStore = useAppStore()
const userStore = useUserStore(); const userStore = useUserStore()
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const role = computed(() => Number(userStore.userInfo.roleId)); const role = computed(() => Number(userStore.userInfo.roleId))
const roleText = computed(() => { const roleText = computed(() => {
const roleMap = { 1: '平台管理员', 2: '学校管理员', 3: '教师', 4: '学生' }; const roleMap = { 1: '平台管理员', 2: '学校管理员', 3: '教师', 4: '学生' }
return roleMap[role.value] || '用户'; return roleMap[role.value] || '用户'
}); })
const studentTopTask = ref(null); const studentTopTask = ref(null)
const changePasswordVisible = ref(false); const changePasswordVisible = ref(false)
const mentList = ref([ const mentList = ref([
{ name: "首页", path: "/index", icon: HomeFilled }, { name: "首页", path: "/index", icon: HomeFilled },
@ -179,23 +189,23 @@ const mentList = ref([
{ name: "学生管理", path: "/student/index", role: 3, teacherMenu: true, activePrefix: "/student" }, { name: "学生管理", path: "/student/index", role: 3, teacherMenu: true, activePrefix: "/student" },
{ name: "任务分配", path: "/task/index", role: 3, teacherMenu: true, activePrefix: "/task" }, { name: "任务分配", path: "/task/index", role: 3, teacherMenu: true, activePrefix: "/task" },
{ name: "成绩中心", path: "/score/index", role: 3, teacherMenu: true, activePrefix: "/score" }, { name: "成绩中心", path: "/score/index", role: 3, teacherMenu: true, activePrefix: "/score" },
]); ])
const activement = ref(mentList.value[0]); const activement = ref(mentList.value[0])
function toggleSideBar() { function toggleSideBar() {
appStore.toggleSideBar(); appStore.toggleSideBar()
} }
function handleCommand(command) { function handleCommand(command) {
switch (command) { switch (command) {
case "changePassword": case "changePassword":
changePasswordVisible.value = true; changePasswordVisible.value = true
break; break
case "logout": case "logout":
logout(); logout()
break; break
default: default:
break; break
} }
} }
@ -213,11 +223,11 @@ function onPasswordChanged() {
) )
.then(() => { .then(() => {
userStore.logOut().then(() => { userStore.logOut().then(() => {
location.href = "/index"; location.href = "/index"
});
}) })
.catch(() => {}); })
}, 200); .catch(() => { })
}, 200)
} }
function logout() { function logout() {
@ -228,71 +238,71 @@ function logout() {
}) })
.then(() => { .then(() => {
userStore.logOut().then(() => { userStore.logOut().then(() => {
location.href = "/index"; location.href = "/index"
}); })
}) })
.catch(() => {}); .catch(() => { })
} }
const go = (item) => { const go = (item) => {
if (router.currentRoute.value.path === item.path) { if (router.currentRoute.value.path === item.path) {
return; return
}
activement.value = item
router.push(item.path)
} }
activement.value = item;
router.push(item.path);
};
const isActive = (item) => { const isActive = (item) => {
return item.activePrefix ? router.currentRoute.value.path.startsWith(item.activePrefix) : item.path === router.currentRoute.value.path; return item.activePrefix ? router.currentRoute.value.path.startsWith(item.activePrefix) : item.path === router.currentRoute.value.path
}; }
const roleList = computed(() => { const roleList = computed(() => {
const currentRole = role.value; const currentRole = role.value
if (currentRole === 3) { if (currentRole === 3) {
const teacherOrder = ["/index", "/analysis/index", "/ai/teaching-plan"]; const teacherOrder = ["/index", "/analysis/index", "/ai/teaching-plan"]
return teacherOrder.map((path) => mentList.value.find((item) => item.path === path)).filter(Boolean); return teacherOrder.map((path) => mentList.value.find((item) => item.path === path)).filter(Boolean)
} }
const items = mentList.value.filter((item) => { const items = mentList.value.filter((item) => {
if (currentRole === 1) { if (currentRole === 1) {
return item.path === "/index" || item.role === 1; return item.path === "/index" || item.role === 1
} }
if (currentRole === 2) { if (currentRole === 2) {
return item.path === "/index"; return item.path === "/index"
} }
if (!item.role || item.path === "/index" || item.path === "/analysis/index") return true; if (!item.role || item.path === "/index" || item.path === "/analysis/index") return true
return item.role == currentRole; return item.role == currentRole
}); })
if (currentRole !== 4 || !studentTopTask.value) return items; if (currentRole !== 4 || !studentTopTask.value) return items
const homeIndex = items.findIndex((item) => item.path === "/index"); const homeIndex = items.findIndex((item) => item.path === "/index")
return [ return [
...items.slice(0, homeIndex + 1), ...items.slice(0, homeIndex + 1),
{ ...studentTopTask.value, name: studentTopTask.value.title, icon: Reading }, { ...studentTopTask.value, name: studentTopTask.value.title, icon: Reading },
...items.slice(homeIndex + 1), ...items.slice(homeIndex + 1),
]; ]
}); })
async function loadStudentTopTask() { async function loadStudentTopTask() {
if (role.value !== 4) { if (role.value !== 4) {
studentTopTask.value = null; studentTopTask.value = null
return; return
} }
const cachedTasks = getCachedStudentTrainingTasks(); const cachedTasks = getCachedStudentTrainingTasks()
if (cachedTasks.length) { if (cachedTasks.length) {
studentTopTask.value = getComprehensiveTopTask(cachedTasks); studentTopTask.value = getComprehensiveTopTask(cachedTasks)
} }
try { try {
const res = await loadStudentTrainingTasks(); const res = await loadStudentTrainingTasks()
studentTopTask.value = getComprehensiveTopTask(res.data || []); studentTopTask.value = getComprehensiveTopTask(res.data || [])
} catch (error) { } catch (error) {
if (!cachedTasks.length) { if (!cachedTasks.length) {
studentTopTask.value = null; studentTopTask.value = null
} }
} }
} }
onMounted(() => { onMounted(() => {
loadStudentTopTask(); loadStudentTopTask()
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -606,7 +616,9 @@ onMounted(() => {
backdrop-filter: none; backdrop-filter: none;
// 蓝色光点装饰与极简白底不匹配,移除 // 蓝色光点装饰与极简白底不匹配,移除
.nav-bg-decoration { display: none; } .nav-bg-decoration {
display: none;
}
// 品牌区:去渐变去模糊,logo 描边收敛为教育蓝 // 品牌区:去渐变去模糊,logo 描边收敛为教育蓝
// 超管端(role=1)品牌区使用 .navTitle,教师端(role=3)使用 .TnavTitle,统一覆盖 // 超管端(role=1)品牌区使用 .navTitle,教师端(role=3)使用 .TnavTitle,统一覆盖
@ -619,8 +631,14 @@ onMounted(() => {
.logo-icon { .logo-icon {
filter: none; filter: none;
path, circle { stroke: #3b82f6; } path,
path:first-child { fill: rgba(59, 130, 246, 0.1); } circle {
stroke: #3b82f6;
}
path:first-child {
fill: rgba(59, 130, 246, 0.1);
}
} }
span, span,
@ -672,7 +690,9 @@ onMounted(() => {
background: #3b82f6; background: #3b82f6;
} }
&::before { display: none; } &::before {
display: none;
}
} }
// teacher-nav 定位规则保留,胶囊样式全部清除 // teacher-nav 定位规则保留,胶囊样式全部清除
@ -694,7 +714,9 @@ onMounted(() => {
border: 0; border: 0;
box-shadow: none; box-shadow: none;
.menu-indicator { background: #3b82f6; } .menu-indicator {
background: #3b82f6;
}
} }
} }
} }
@ -752,7 +774,9 @@ onMounted(() => {
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
box-shadow: none; box-shadow: none;
.nav-bg-decoration { display: none; } .nav-bg-decoration {
display: none;
}
// 品牌区:去渐变去发光,logo 描边收敛为教育蓝 // 品牌区:去渐变去发光,logo 描边收敛为教育蓝
.navTitle { .navTitle {
@ -762,10 +786,14 @@ onMounted(() => {
.logo-icon { .logo-icon {
filter: none; filter: none;
path, circle { path,
circle {
stroke: #2563eb; stroke: #2563eb;
} }
path:first-child { fill: rgba(37, 99, 235, 0.1); }
path:first-child {
fill: rgba(37, 99, 235, 0.1);
}
} }
.nacber-name { .nacber-name {
@ -794,7 +822,10 @@ onMounted(() => {
background: #2563eb; background: #2563eb;
} }
.menu-ico { font-size: 15px; color: inherit; } .menu-ico {
font-size: 15px;
color: inherit;
}
&:hover { &:hover {
color: #1f2937; color: #1f2937;
@ -819,7 +850,9 @@ onMounted(() => {
background: #2563eb; background: #2563eb;
} }
&::before { display: none; } &::before {
display: none;
}
} }
} }
@ -841,7 +874,9 @@ onMounted(() => {
} }
} }
.divider { background: #e5e7eb; } .divider {
background: #e5e7eb;
}
.avatar-container .avatar-wrapper { .avatar-container .avatar-wrapper {
background: #ffffff; background: #ffffff;
@ -863,7 +898,9 @@ onMounted(() => {
} }
} }
.dropdown-icon { color: #6b7280; } .dropdown-icon {
color: #6b7280;
}
} }
} }
} }
@ -976,7 +1013,9 @@ onMounted(() => {
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar { .navbar {
.navTitle, .TnavTitle {
.navTitle,
.TnavTitle {
.logo-area { .logo-area {
.nacber-name { .nacber-name {
font-size: 13px; font-size: 13px;
@ -1069,7 +1108,9 @@ onMounted(() => {
/* ===== 学生端用户下拉:白底墨黑 ===== */ /* ===== 学生端用户下拉:白底墨黑 ===== */
.student-dropdown-popper { .student-dropdown-popper {
.el-popper__arrow { display: none !important; } .el-popper__arrow {
display: none !important;
}
.dropdown-panel { .dropdown-panel {
margin: 0 !important; margin: 0 !important;
@ -1094,6 +1135,7 @@ onMounted(() => {
font-weight: 600; font-weight: 600;
letter-spacing: 0; letter-spacing: 0;
} }
.user-role { .user-role {
color: #6b7280 !important; color: #6b7280 !important;
font-size: var(--student-fs-caption); font-size: var(--student-fs-caption);
@ -1141,7 +1183,9 @@ onMounted(() => {
outline: none; outline: none;
transform: none !important; transform: none !important;
svg { color: #1f2937; } svg {
color: #1f2937;
}
} }
&.logout, &.logout,
@ -1150,14 +1194,19 @@ onMounted(() => {
&.logout:focus-visible { &.logout:focus-visible {
color: #d9554c !important; color: #d9554c !important;
svg { color: #d9554c; } svg {
color: #d9554c;
}
} }
&.logout:hover, &.logout:hover,
&.logout:focus, &.logout:focus,
&.logout:focus-visible { &.logout:focus-visible {
background: rgba(217, 85, 76, 0.08) !important; background: rgba(217, 85, 76, 0.08) !important;
svg { color: #c2413a; } svg {
color: #c2413a;
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

@ -753,7 +753,7 @@ const onClick = (type, idx) => {
if (!type?.isTemplateBased) { if (!type?.isTemplateBased) {
let arr = ["课程知识点", "核心知识点", "最难的部分", "实务知识概述", "学习建议", "业务现状", "理论研究", "发展趋势"] let arr = ["课程知识点", "核心知识点", "最难的部分", "实务知识概述", "学习建议", "业务现状", "理论研究", "发展趋势"]
if (arr.includes(type.name)) { if (arr.includes(type.name)) {
inputMessage.value = type.com.replace("xxx", "电商互联网产品开发实训系统") inputMessage.value = type.com.replace("xxx", "互联网产品开发实训系统")
} else { } else {
inputMessage.value = type.com inputMessage.value = type.com
} }
@ -908,7 +908,7 @@ onUnmounted(() => {
if (resizeObserver) { if (resizeObserver) {
resizeObserver.disconnect() resizeObserver.disconnect()
} }
}); })
</script> </script>
<style lang="scss"> <style lang="scss">
.index { .index {

@ -7,7 +7,7 @@
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
--> -->
<template> <template>
<div class="content">电商互联网产品开发实训系统</div> <div class="content">互联网产品开发实训系统</div>
</template> </template>
<script setup></script> <script setup></script>

@ -1,188 +1,728 @@
<template> <template>
<div class="login" v-loading="loadpage"> <div class="login" v-loading="loadpage">
<div class="login-main"> <div class="login-panel">
<!-- 左侧品牌区:科技数据看板 -->
<div class="brand-side">
<div class="grid-bg"></div>
<div class="scan-line"></div>
<div class="brand-deco deco-1"></div>
<div class="brand-deco deco-2"></div>
<div class="brand-deco deco-3"></div>
<div class="brand-content">
<div class="brand-badge anim" :style="{ animationDelay: '0s' }">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" stroke-linejoin="round">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" />
<path d="M3 6h18" />
<path d="M16 10a4 4 0 0 1-8 0" />
</svg>
<span>电商实训平台</span>
</div>
<h1 class="brand-title anim" :style="{ animationDelay: '0.08s' }">互联网产品<br />开发实训系统</h1>
<p class="brand-subtitle anim" :style="{ animationDelay: '0.16s' }">从市场需求到产品上线,完整还原电商产品全生命周期</p>
<!-- 实训全流程 -->
<div class="dash-card anim" :style="{ animationDelay: '0.24s' }">
<div class="dash-head">
<span class="live-dot"></span>
<span class="dash-title">实训全流程</span>
<span class="dash-tag">5 个阶段</span>
</div>
<div class="flow-row">
<template v-for="(step, i) in steps" :key="step.name">
<div class="flow-node" :class="{ active: i === 0 }">
<span class="node-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" v-html="step.icon"></svg>
</span>
<span class="node-label">{{ step.name }}</span>
<span class="node-index">STEP {{ i + 1 }}</span>
</div>
<div v-if="i < steps.length - 1" class="flow-link"></div>
</template>
</div>
<div class="module-grid">
<div v-for="mod in modules" :key="mod.name" class="module-item">
<span class="module-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" v-html="mod.icon"></svg>
</span>
<div>
<b>{{ mod.name }}</b>
<i>{{ mod.desc }}</i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧表单区 -->
<div class="form-side">
<div class="form-wrapper">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">电商互联网产品开发实训系统</h3> <h3 class="title">欢迎回来</h3>
<p class="subtitle">请登录你的账号,继续今天的实训</p>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" autocomplete="off" placeholder="账号"> <el-input v-model="loginForm.username" type="text" size="large" autocomplete="off"
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template> placeholder="账号">
<template #prefix><svg-icon icon-class="user"
class="el-input__icon input-icon" /></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input v-model="loginForm.password" class="password-input" type="password" size="large" autocomplete="new-password" placeholder="密码" @keyup.enter="handleLogin" show-password> <el-input v-model="loginForm.password" class="password-input" type="password" size="large"
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template> autocomplete="new-password" placeholder="密码" @keyup.enter="handleLogin" show-password>
<template #prefix><svg-icon icon-class="password"
class="el-input__icon input-icon" /></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0px 0px 25px 0px">记住密码</el-checkbox> <div class="form-options">
<el-checkbox v-model="loginForm.rememberMe">记住密码</el-checkbox>
<router-link v-if="register" class="link-type" :to="'/register'">立即注册</router-link>
</div>
<el-form-item style="width: 100%"> <el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin"> <el-button :loading="loading" size="large" type="primary" class="login-btn"
@click.prevent="handleLogin">
<span v-if="!loading">登 录</span> <span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span> <span v-else>登 录 中...</span>
</el-button> </el-button>
<div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<!-- 底部 --> </div>
<div class="el-login-footer" style="display: none">
<span>Copyright © 2018-2023 ruoyi.vip All Rights Reserved.</span>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { getCodeImg } from "@/api/login"; import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"; import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"; import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from "@/store/modules/user"; import useUserStore from "@/store/modules/user"
import { getUserInfo } from "@/utils/auth"; import { getUserInfo } from "@/utils/auth"
const userStore = useUserStore(); const userStore = useUserStore()
const route = useRoute(); const route = useRoute()
const router = useRouter(); const router = useRouter()
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance()
const loginForm = ref({ const loginForm = ref({
username: "", username: "",
password: "", password: "",
rememberMe: false, rememberMe: false,
TOKEN: "", TOKEN: "",
}); })
const loginRules = { const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }], username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }], password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
code: [{ required: true, trigger: "change", message: "请输入验证码" }], code: [{ required: true, trigger: "change", message: "请输入验证码" }],
}; }
const loading = ref(false); const loading = ref(false)
const loadpage = ref(false); const loadpage = ref(false)
// 验证码开关 // 验证码开关
const captchaEnabled = ref(true); const captchaEnabled = ref(true)
// 注册开关 // 注册开关
const register = ref(false); const register = ref(false)
const redirect = ref(undefined); const redirect = ref(undefined)
watch( watch(
route, route,
(newRoute) => { (newRoute) => {
if (newRoute.query.token) { if (newRoute.query.token) {
loadpage.value = true; loadpage.value = true
setTimeout(() => { setTimeout(() => {
loginForm.value.TOKEN = newRoute.query.token; loginForm.value.TOKEN = newRoute.query.token
return handleLogin(); return handleLogin()
}, 1000); }, 1000)
} }
}, },
{ immediate: true } { immediate: true }
); )
function handleLogin() { function handleLogin() {
// proxy.$refs.loginRef.validate(valid => { // proxy.$refs.loginRef.validate(valid => {
// if (valid) { // if (valid) {
loading.value = true; loading.value = true
loadpage.value = true; loadpage.value = true
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码 // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) { if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 }); Cookies.set("username", loginForm.value.username, { expires: 30 })
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 }); Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 }); Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 })
} else { } else {
// 否则移除 // 否则移除
Cookies.remove("username"); Cookies.remove("username")
Cookies.remove("password"); Cookies.remove("password")
Cookies.remove("rememberMe"); Cookies.remove("rememberMe")
} }
//登录 //登录
const params = { const params = {
userName: loginForm.value.username, userName: loginForm.value.username,
passwordEncode: loginForm.value.password !== "" ? encrypt(loginForm.value.password) : loginForm.value.password, passwordEncode: loginForm.value.password !== "" ? encrypt(loginForm.value.password) : loginForm.value.password,
TOKEN: loginForm.value.token, TOKEN: loginForm.value.token,
}; }
if (loginForm.value.TOKEN !== "") { if (loginForm.value.TOKEN !== "") {
params.token = loginForm.value.TOKEN; params.token = loginForm.value.TOKEN
} }
userStore userStore
.login(params) .login(params)
.then((res) => { .then((res) => {
if (params.token) { if (params.token) {
setTimeout(() => { setTimeout(() => {
loading.value = false; loading.value = false
loadpage.value = false; loadpage.value = false
router.push({ path: redirect.value || "/index" }); router.push({ path: redirect.value || "/index" })
}, 2000); }, 2000)
} else { } else {
loading.value = false; loading.value = false
router.push({ path: redirect.value || "/index" }); router.push({ path: redirect.value || "/index" })
} }
}) })
.catch(() => { .catch(() => {
loadpage.value = false; loadpage.value = false
loading.value = false; loading.value = false
}); })
} }
function getCookie() { function getCookie() {
const username = Cookies.get("username"); const username = Cookies.get("username")
const password = Cookies.get("password"); const password = Cookies.get("password")
const rememberMe = Cookies.get("rememberMe"); const rememberMe = Cookies.get("rememberMe")
loginForm.value = { loginForm.value = {
username: username === undefined ? loginForm.value.username : username, username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password : decrypt(password), password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe), rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
}; }
} }
// getCode(); // getCode();
getCookie(); getCookie()
/* ---------------- 实训流程与模块(真实课程内容,非模拟数据) ---------------- */
const steps = [
{
name: "市场需求分析",
icon: '<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />',
},
{
name: "产品设计",
icon: '<path d="M12 19l7-7 3 3-7 7-3-3z" /><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" /><path d="M2 2l7.586 7.586" /><circle cx="11" cy="11" r="2" />',
},
{
name: "概念测试",
icon: '<path d="M9 18h6" /><path d="M10 22h4" /><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.4 1 2.3h6c0-.9.4-1.8 1-2.3A7 7 0 0 0 12 2z" />',
},
{
name: "概念评估",
icon: '<path d="M3 3v18h18" /><path d="m19 9-5 5-4-4-3 3" />',
},
{
name: "渠道运营",
icon: '<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><path d="M3 6h18" /><path d="M16 10a4 4 0 0 1-8 0" />',
},
]
const modules = [
{
name: "市场需求分析",
desc: "洞察用户真实诉求,找准产品定位",
icon: '<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />',
},
{
name: "产品设计",
desc: "从概念到原型,打磨产品细节",
icon: '<path d="M12 19l7-7 3 3-7 7-3-3z" /><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" /><path d="M2 2l7.586 7.586" /><circle cx="11" cy="11" r="2" />',
},
{
name: "渠道运营",
desc: "多渠道布局,掌握增长方法论",
icon: '<path d="M3 3v18h18" /><path d="m19 9-5 5-4-4-3 3" />',
},
{
name: "概念测试评估",
desc: "数据驱动决策,验证产品价值",
icon: '<path d="M9 18h6" /><path d="M10 22h4" /><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.4 1 2.3h6c0-.9.4-1.8 1-2.3A7 7 0 0 0 12 2z" />',
},
]
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.login { .login {
height: 100%;
min-height: 100vh;
background: #f4f7fe;
}
.login-panel {
display: flex;
min-height: 100vh;
}
/* ---------- 入场动画 ---------- */
@keyframes riseIn {
from {
opacity: 0;
transform: translateY(22px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.anim {
animation: riseIn 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* ---------- 左侧品牌区 ---------- */
.brand-side {
position: relative;
flex: 1 1 55%;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
height: 100%; overflow: hidden;
background-image: url("@/assets/images/login-background.0dded3b4.webp"); padding: 52px 68px;
background-size: 100% 100%; background: linear-gradient(135deg, #0b1a4d 0%, #16306f 45%, #2447c4 100%);
color: #fff;
} }
.title {
margin: 0px auto 30px auto; /* 流动网格 */
.grid-bg {
position: absolute;
inset: -60px;
background-image:
linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
background-size: 46px 46px;
animation: gridMove 18s linear infinite;
mask-image: radial-gradient(circle at 30% 40%, #000 0%, transparent 78%);
-webkit-mask-image: radial-gradient(circle at 30% 40%, #000 0%, transparent 78%);
}
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(46px, 46px);
}
}
/* 扫描线 */
.scan-line {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 180px;
background: linear-gradient(180deg, rgba(125, 211, 252, 0) 0%, rgba(125, 211, 252, 0.16) 60%, rgba(125, 211, 252, 0) 100%);
animation: scan 7s ease-in-out infinite;
pointer-events: none;
}
@keyframes scan {
0% {
transform: translateY(-200px);
opacity: 0;
}
12% {
opacity: 1;
}
88% {
opacity: 1;
}
100% {
transform: translateY(100vh);
opacity: 0;
}
}
.brand-deco {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.deco-1 {
width: 520px;
height: 520px;
right: -180px;
top: -190px;
background: radial-gradient(circle at 30% 30%, rgba(56, 189, 248, 0.4), rgba(56, 189, 248, 0) 70%);
animation: breathe 9s ease-in-out infinite;
}
.deco-2 {
width: 420px;
height: 420px;
left: -150px;
bottom: -160px;
background: radial-gradient(circle at 60% 40%, rgba(167, 139, 250, 0.34), rgba(167, 139, 250, 0) 70%);
animation: breathe 11s ease-in-out infinite reverse;
}
.deco-3 {
width: 180px;
height: 180px;
right: 10%;
bottom: 10%;
border: 1px solid rgba(125, 211, 252, 0.22);
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(2px);
}
@keyframes breathe {
0%,
100% {
transform: scale(1);
opacity: 0.75;
}
50% {
transform: scale(1.12);
opacity: 1;
}
}
.brand-content {
position: relative;
z-index: 1;
width: 100%;
max-width: 600px;
}
.brand-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 16px;
border-radius: 999px;
background: rgba(125, 211, 252, 0.12);
border: 1px solid rgba(125, 211, 252, 0.28);
font-size: 13px;
letter-spacing: 2px;
svg {
width: 18px;
height: 18px;
}
}
.brand-title {
margin: 24px 0 14px;
font-size: 34px;
line-height: 1.35;
font-weight: 600;
letter-spacing: 1px;
text-shadow: 0 0 34px rgba(125, 211, 252, 0.35);
}
.brand-subtitle {
margin: 0 0 30px;
font-size: 15px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.68);
}
/* ---------- 数据看板卡片 ---------- */
.dash-card {
padding: 20px 22px 16px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.13);
backdrop-filter: blur(14px);
box-shadow: 0 18px 44px -22px rgba(0, 0, 0, 0.65);
animation: riseIn 0.7s cubic-bezier(0.22, 1, 0.36, 1) both, cardGlow 6s ease-in-out infinite;
}
/* 只做光影呼吸,不碰 transform,避免与入场动画冲突 */
@keyframes cardGlow {
0%,
100% {
box-shadow: 0 18px 44px -22px rgba(0, 0, 0, 0.65);
border-color: rgba(255, 255, 255, 0.13);
}
50% {
box-shadow: 0 18px 44px -22px rgba(0, 0, 0, 0.65), 0 0 34px -8px rgba(125, 211, 252, 0.42);
border-color: rgba(125, 211, 252, 0.32);
}
}
.dash-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 18px;
}
.live-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.6);
animation: livePulse 1.8s ease-out infinite;
}
@keyframes livePulse {
0% {
box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.55);
}
70% {
box-shadow: 0 0 0 9px rgba(74, 222, 128, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(74, 222, 128, 0);
}
}
.dash-title {
font-size: 13.5px;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.88);
}
.dash-tag {
margin-left: auto;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
letter-spacing: 1px;
color: #7dd3fc;
background: rgba(125, 211, 252, 0.14);
border: 1px solid rgba(125, 211, 252, 0.25);
}
/* 流程节点行 */
.flow-row {
display: flex;
align-items: stretch;
gap: 4px;
padding: 14px 6px 16px;
border-radius: 11px;
background: rgba(9, 20, 56, 0.42);
border: 1px solid rgba(125, 211, 252, 0.14);
margin-bottom: 16px;
}
.flow-node {
flex: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
width: 76px;
}
.node-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 50%;
color: #7dd3fc;
background: rgba(125, 211, 252, 0.1);
border: 1px solid rgba(125, 211, 252, 0.32);
transition: box-shadow 0.3s ease;
svg {
width: 20px;
height: 20px;
}
}
.flow-node.active .node-icon {
color: #e8f4ff;
background: linear-gradient(135deg, #38bdf8 0%, #6366f1 100%);
border-color: transparent;
animation: nodeGlow 2.4s ease-in-out infinite;
}
@keyframes nodeGlow {
0%,
100% {
box-shadow: 0 0 0 0 rgba(125, 211, 252, 0.45);
}
50% {
box-shadow: 0 0 0 7px rgba(125, 211, 252, 0);
}
}
.node-label {
font-size: 12px;
line-height: 1.3;
color: rgba(255, 255, 255, 0.82);
text-align: center; text-align: center;
color: rgb(48, 50, 52);
font-size: var(--student-fs-h2);
font-weight: 500;
} }
.login-main {
width: 85%; .node-index {
height: 100%; font-size: 10px;
letter-spacing: 1px;
color: rgba(125, 211, 252, 0.6);
}
.flow-node.active .node-label {
color: #ffffff;
font-weight: 600;
}
/* 流动连接线 */
.flow-link {
flex: 1;
align-self: flex-start;
margin-top: 20px;
height: 2px;
border-radius: 2px;
background: repeating-linear-gradient(90deg, rgba(125, 211, 252, 0.65) 0 7px, transparent 7px 14px);
background-size: 14px 2px;
animation: dashFlow 1.1s linear infinite;
}
@keyframes dashFlow {
to {
background-position: 14px 0;
}
}
/* 模块网格 */
.module-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.module-item {
display: flex; display: flex;
align-items: flex-start;
gap: 11px;
padding: 13px 14px;
border-radius: 11px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.25s ease, border-color 0.25s ease;
&:hover {
background: rgba(125, 211, 252, 0.1);
border-color: rgba(125, 211, 252, 0.3);
}
b {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 3px;
}
i {
font-style: normal;
font-size: 12px;
color: rgba(255, 255, 255, 0.55);
line-height: 1.5;
}
}
.module-icon {
flex: none;
display: inline-flex;
align-items: center; align-items: center;
justify-content: end; justify-content: center;
width: 34px;
height: 34px;
border-radius: 9px;
color: #7dd3fc;
background: rgba(125, 211, 252, 0.12);
border: 1px solid rgba(125, 211, 252, 0.22);
svg {
width: 17px;
height: 17px;
} }
.login-form { }
border-radius: 6px;
/* ---------- 右侧表单区 ---------- */
.form-side {
flex: 1 1 45%;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff; background: #ffffff;
width: 400px; padding: 40px 32px;
padding: 55px 25px 33px 25px; }
.form-wrapper {
width: 100%;
max-width: 390px;
animation: riseIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.15s both;
}
.title {
margin: 0;
color: #1d2129;
font-size: 26px;
font-weight: 600;
}
.subtitle {
margin: 10px 0 34px;
font-size: 14px;
color: #86909c;
}
.login-form {
width: 100%;
.el-form-item {
margin-bottom: 22px;
}
.el-input { .el-input {
height: 40px; height: 46px;
input { input {
height: 40px; height: 46px;
} }
} }
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
border-radius: 10px;
background-color: #f5f7fa !important;
box-shadow: 0 0 0 1px #f5f7fa inset;
padding-left: 14px;
transition: box-shadow 0.25s ease, background-color 0.25s ease;
&:hover {
background-color: #f0f2f7 !important;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
&.is-focus {
background-color: #ffffff !important; background-color: #ffffff !important;
box-shadow: 0 0 0 2px #2b5aed inset, 0 0 0 4px rgba(43, 90, 237, 0.12) !important;
}
} }
:deep(.el-input__inner), :deep(.el-input__inner),
:deep(input) { :deep(input) {
color: #303133; color: #1d2129;
background-color: transparent !important; background-color: transparent !important;
-webkit-text-fill-color: #303133; -webkit-text-fill-color: #1d2129;
} }
:deep(.el-input__inner::placeholder), :deep(.el-input__inner::placeholder),
:deep(input::placeholder) { :deep(input::placeholder) {
color: #b8c0cc; color: #a8abb2;
-webkit-text-fill-color: #b8c0cc; -webkit-text-fill-color: #a8abb2;
opacity: 1; opacity: 1;
} }
@ -194,9 +734,9 @@ getCookie();
:deep(input:-webkit-autofill:hover), :deep(input:-webkit-autofill:hover),
:deep(input:-webkit-autofill:focus), :deep(input:-webkit-autofill:focus),
:deep(input:-webkit-autofill:active) { :deep(input:-webkit-autofill:active) {
caret-color: #303133 !important; caret-color: #1d2129 !important;
-webkit-text-fill-color: #303133 !important; -webkit-text-fill-color: #1d2129 !important;
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important; -webkit-box-shadow: 0 0 0 1000px #f5f7fa inset !important;
transition: background-color 9999s ease-out 0s !important; transition: background-color 9999s ease-out 0s !important;
} }
@ -204,36 +744,114 @@ getCookie();
height: 39px; height: 39px;
width: 14px; width: 14px;
margin-left: 0px; margin-left: 0px;
color: #86909c;
} }
} }
.login-tip {
font-size: var(--student-fs-body-sm); .form-options {
text-align: center; display: flex;
color: #bfbfbf; align-items: center;
} justify-content: space-between;
.login-code { margin: 0 0 22px;
width: 33%;
height: 40px; .link-type {
float: right; font-size: 13px;
img { color: #2b5aed;
cursor: pointer; text-decoration: none;
vertical-align: middle;
} &:hover {
} text-decoration: underline;
.el-login-footer { }
height: 40px; }
line-height: 40px; }
position: fixed;
bottom: 0; .login-btn {
position: relative;
overflow: hidden;
width: 100%; width: 100%;
text-align: center; height: 46px;
color: #fff; border: none;
font-family: var(--student-font-base); border-radius: 10px;
font-size: var(--student-fs-caption); font-size: 16px;
letter-spacing: 1px; letter-spacing: 6px;
background: linear-gradient(135deg, #1e3fae 0%, #2b5aed 55%, #5b7cf5 100%);
box-shadow: 0 8px 22px -6px rgba(43, 90, 237, 0.55);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&::after {
content: "";
position: absolute;
top: 0;
left: -60%;
width: 40%;
height: 100%;
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.32) 50%, rgba(255, 255, 255, 0) 100%);
transform: skewX(-20deg);
animation: shine 3.6s ease-in-out infinite;
}
&:hover {
transform: translateY(-1px);
box-shadow: 0 12px 26px -6px rgba(43, 90, 237, 0.6);
}
&:active {
transform: translateY(0);
}
}
@keyframes shine {
0%,
60% {
left: -60%;
}
100% {
left: 130%;
}
}
/* ---------- 响应式 ---------- */
@media (max-width: 1200px) {
.brand-side {
padding: 40px 44px;
}
.brand-title {
font-size: 29px;
}
.flow-node {
width: 62px;
}
.node-label {
font-size: 11px;
}
.module-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.brand-side {
display: none;
}
.form-side {
flex: 1 1 100%;
}
}
@media (max-width: 480px) {
.form-side {
padding: 24px 18px;
}
.title {
font-size: 23px;
} }
.login-code-img {
height: 40px;
padding-left: 12px;
} }
</style> </style>

@ -157,6 +157,10 @@
<el-form-item label="班级名称" prop="className"> <el-form-item label="班级名称" prop="className">
<el-input v-model="formInline.className" placeholder="请输入班级名称" class="form-control" /> <el-input v-model="formInline.className" placeholder="请输入班级名称" class="form-control" />
</el-form-item> </el-form-item>
<!-- 2026-10-12 停用:新增班级弹窗的「名单创建」(导入名单创建 + 模板下载)。
教学班改为通过「来源行政班」+ 班级名称直接创建。恢复时打开下面注释即可,
并同步启用 importCreateUploadData / beforeImportCreate / uploadImportCreate /
handleImportCreateSuccess / downloadTeachingClassImportTemplate。
<el-form-item v-if="activeClassType === 'TEACHING' && isCreate" label="名单创建"> <el-form-item v-if="activeClassType === 'TEACHING' && isCreate" label="名单创建">
<div class="import-create-actions"> <div class="import-create-actions">
<el-upload action="/api/user/uploadTeachingClassByStudentMembers" :data="importCreateUploadData" <el-upload action="/api/user/uploadTeachingClassByStudentMembers" :data="importCreateUploadData"
@ -177,6 +181,7 @@
</el-button> </el-button>
</div> </div>
</el-form-item> </el-form-item>
-->
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footers"> <span class="dialog-footers">
@ -362,6 +367,7 @@ const adminClassOptions = computed(() => {
}) })
}) })
/* 2026-10-12 停用:名单创建上传参数(配合弹窗「名单创建」一起下线)
const importCreateUploadData = computed(() => ({ const importCreateUploadData = computed(() => ({
schoolId: currentSchoolId.value, schoolId: currentSchoolId.value,
schoolFacultyId: formInline.value.schoolFacultyId, schoolFacultyId: formInline.value.schoolFacultyId,
@ -370,6 +376,7 @@ const importCreateUploadData = computed(() => ({
operatorId: currentUserId.value, operatorId: currentUserId.value,
sourceClassIds: sourceAdminClassIds.value.join(","), sourceClassIds: sourceAdminClassIds.value.join(","),
})) }))
*/
const memberUploadData = computed(() => ({ const memberUploadData = computed(() => ({
teachingClassId: selectedTeachingClass.value?.schoolClassId || "", teachingClassId: selectedTeachingClass.value?.schoolClassId || "",
@ -545,6 +552,7 @@ function submitForm() {
}) })
} }
/* 2026-10-12 停用:名单创建导入(导入名单创建按钮的配套逻辑,仅该按钮使用)
async function beforeImportCreate(file) { async function beforeImportCreate(file) {
if (activeClassType.value !== "TEACHING") { if (activeClassType.value !== "TEACHING") {
proxy.$message.warning("请切换到教学班") proxy.$message.warning("请切换到教学班")
@ -611,6 +619,7 @@ function handleImportCreateSuccess(res) {
getList() getList()
refreshAllClassList() refreshAllClassList()
} }
*/
function openMemberDialog(schoolClass) { function openMemberDialog(schoolClass) {
selectedTeachingClass.value = schoolClass selectedTeachingClass.value = schoolClass
@ -767,6 +776,7 @@ async function downloadMemberImportTemplate() {
XLSX.writeFile(workbook, "教学班成员导入模板.xlsx") XLSX.writeFile(workbook, "教学班成员导入模板.xlsx")
} }
/* 2026-10-12 停用:名单创建模板下载(仅新增班级弹窗「模板下载」按钮使用)
async function downloadTeachingClassImportTemplate() { async function downloadTeachingClassImportTemplate() {
const XLSX = await import("xlsx") const XLSX = await import("xlsx")
const templateSheet = XLSX.utils.aoa_to_sheet([ const templateSheet = XLSX.utils.aoa_to_sheet([
@ -781,6 +791,7 @@ async function downloadTeachingClassImportTemplate() {
XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明") XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明")
XLSX.writeFile(workbook, "教学班名单创建模板.xlsx") XLSX.writeFile(workbook, "教学班名单创建模板.xlsx")
} }
*/
function deleteClass(id) { function deleteClass(id) {
ElMessageBox.confirm("确定删除该班级吗?", "提示", { ElMessageBox.confirm("确定删除该班级吗?", "提示", {

@ -143,6 +143,8 @@
@close="onClose"> @close="onClose">
<el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px" <el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px"
style="width: 80%" :rules="rules"> style="width: 80%" :rules="rules">
<!-- 2026-10-12 停用:所属院系 / 所属专业的下拉选择(新增教师不再选择院系与专业)。
原始代码保留备用;恢复时打开下面这段注释,并删掉其后两个只读 el-form-item。
<el-form-item v-if="status || isOrganizationManagementEnabled" label="所属院系:" prop="schoolFacultyId"> <el-form-item v-if="status || isOrganizationManagementEnabled" label="所属院系:" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="" clearable @change="getMajorList" <el-select v-model="formInline.schoolFacultyId" placeholder="" clearable @change="getMajorList"
style="width: 20rem" v-if="status"> style="width: 20rem" v-if="status">
@ -160,6 +162,14 @@
<el-input v-else v-model="classInfo.value" placeholder="请输入所属专业:" style="width: 20rem" <el-input v-else v-model="classInfo.value" placeholder="请输入所属专业:" style="width: 20rem"
disabled></el-input> disabled></el-input>
</el-form-item> </el-form-item>
-->
<!-- 新增教师(status=true)不再展示院系/专业;编辑教师(status=false)仍保留只读展示 -->
<el-form-item v-if="!status && isOrganizationManagementEnabled" label="所属院系:" prop="schoolFacultyId">
<el-input v-model="classInfo.name" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item v-if="!status && isOrganizationManagementEnabled" label="所属专业:" prop="schoolMajorId">
<el-input v-model="classInfo.value" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item label="教师姓名:" prop="name"> <el-form-item label="教师姓名:" prop="name">
<el-input v-model="formInline.name" style="width: 20rem" placeholder=""></el-input> <el-input v-model="formInline.name" style="width: 20rem" placeholder=""></el-input>
</el-form-item> </el-form-item>
@ -285,7 +295,9 @@ const editSchool = (row) => {
}) })
} }
const add = () => { const add = () => {
formInline.value.schoolMajorName = majorList.value?.filter((item) => item.schoolMajorId == formInline.value.schoolMajorId)[0].schoolMajorName // 院系/专业下拉停用后 schoolMajorId 可能为空,取不到专业名时不能抛错
const matchedMajor = (majorList.value || []).find((item) => item.schoolMajorId == formInline.value.schoolMajorId)
if (matchedMajor) formInline.value.schoolMajorName = matchedMajor.schoolMajorName
formInline.value.operatorId = currentUserId.value formInline.value.operatorId = currentUserId.value
indexApi.addTeacher(formInline.value).then((res) => { indexApi.addTeacher(formInline.value).then((res) => {
proxy.$message.success("添加成功") proxy.$message.success("添加成功")

Loading…
Cancel
Save