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
###
# 页面标题
VITE_APP_TITLE = 电商互联网产品开发实训系统
VITE_APP_TITLE = 互联网产品开发实训系统
# 开发环境配置
VITE_APP_ENV = 'development'

@ -7,7 +7,7 @@
# @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'

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

1
.gitignore vendored

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

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

@ -1,14 +1,17 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="/favicon.ico">
<title>电商互联网产品开发实训系统</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="icon" href="/favicon.ico" />
<title>互联网产品开发实训系统</title>
<!--[if lt IE 11]>
<script>
window.location.href = "/html/ie.html";
</script>
<![endif]-->
<style>
:root {
--ink: #06101f;
@ -37,13 +40,13 @@
height: 100vh;
color: var(--text);
font-family: "Microsoft YaHei", "PingFang SC", "Source Han Sans CN", sans-serif;
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%);
background-size: 52px 52px, 52px 52px, auto, auto, auto;
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%);
background-size:
52px 52px,
52px 52px,
auto,
auto,
auto;
}
#loading-wrapper::before,
@ -55,9 +58,7 @@
}
#loading-wrapper::before {
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));
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));
opacity: 0.75;
}
@ -77,10 +78,10 @@
padding: 34px 38px;
border: 1px solid var(--line);
border-radius: 8px;
background:
linear-gradient(145deg, rgba(14, 42, 68, 0.92), rgba(6, 20, 38, 0.9)),
var(--panel);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.06);
background: linear-gradient(145deg, rgba(14, 42, 68, 0.92), rgba(6, 20, 38, 0.9)), var(--panel);
box-shadow:
0 24px 70px rgba(0, 0, 0, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
animation: shell-in 0.7s ease both;
}
@ -91,10 +92,7 @@
overflow: hidden;
border: 1px solid rgba(83, 214, 255, 0.28);
border-radius: 8px;
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);
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);
background-size: 25px 25px;
}
@ -124,7 +122,9 @@
height: 5px;
border-radius: 99px;
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 {
@ -146,7 +146,10 @@
height: 10px;
border-radius: 50%;
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 {
@ -322,9 +325,9 @@
}
}
</style>
</head>
</head>
<body>
<body>
<div id="app">
<div id="loading-wrapper">
<div class="loading-shell" role="status" aria-live="polite">
@ -338,15 +341,23 @@
<p class="loading-subtitle">正在同步课程资源、实训数据与学习进度,请稍候。</p>
<div class="progress-track" aria-hidden="true"></div>
<div class="loading-steps" aria-hidden="true">
<div class="loading-step"><span>01</span>课程资源</div>
<div class="loading-step"><span>02</span>实训任务</div>
<div class="loading-step"><span>03</span>学习数据</div>
<div class="loading-step">
<span>01</span>
课程资源
</div>
<div class="loading-step">
<span>02</span>
实训任务
</div>
<div class="loading-step">
<span>03</span>
学习数据
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</body>
</html>

@ -1,7 +1,8 @@
<template>
<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>
<linearGradient id="navGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#001529;stop-opacity:0.95" />
@ -47,26 +48,32 @@
<div class="logo-area">
<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>
<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>
</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">
<div :class="['el-ment-item', { 'el-ment-item-active': isActive(item) }]" @click="go(item)">
<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>
</div>
</template>
@ -75,13 +82,14 @@
<div class="right-menu">
<div class="search-box">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
</svg>
</div>
<div class="divider"></div>
<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-ring">
<img src="../../assets/images/头像.webp" class="user-avatar" />
@ -99,17 +107,19 @@
<div class="dropdown-divider"></div>
<el-dropdown-menu class="dropdown-menu-custom">
<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">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
<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" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
<span>修改密码</span>
</el-dropdown-item>
<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">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
<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" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
<span>退出登录</span>
</el-dropdown-item>
@ -124,37 +134,37 @@
</template>
<script setup>
import { ElMessageBox } from "element-plus";
import { HomeFilled, Reading, Document, TrendCharts, DataLine } from "@element-plus/icons-vue";
import Breadcrumb from "@/components/Breadcrumb";
import TopNav from "@/components/TopNav";
import Hamburger from "@/components/Hamburger";
import Screenfull from "@/components/Screenfull";
import SizeSelect from "@/components/SizeSelect";
import HeaderSearch from "@/components/HeaderSearch";
import RuoYiGit from "@/components/RuoYi/Git";
import RuoYiDoc from "@/components/RuoYi/Doc";
import ChangePasswordDialog from "@/views/profile/ChangePasswordDialog.vue";
import useAppStore from "@/store/modules/app";
import useUserStore from "@/store/modules/user";
import useSettingsStore from "@/store/modules/settings";
import { getCachedStudentTrainingTasks, loadStudentTrainingTasks } from "@/api/trainingTask";
import { getComprehensiveTopTask } from "@/utils/studentTrainingNavigation";
const { proxy } = getCurrentInstance();
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const role = computed(() => Number(userStore.userInfo.roleId));
import { ElMessageBox } from "element-plus"
import { HomeFilled, Reading, Document, TrendCharts, DataLine } from "@element-plus/icons-vue"
import Breadcrumb from "@/components/Breadcrumb"
import TopNav from "@/components/TopNav"
import Hamburger from "@/components/Hamburger"
import Screenfull from "@/components/Screenfull"
import SizeSelect from "@/components/SizeSelect"
import HeaderSearch from "@/components/HeaderSearch"
import RuoYiGit from "@/components/RuoYi/Git"
import RuoYiDoc from "@/components/RuoYi/Doc"
import ChangePasswordDialog from "@/views/profile/ChangePasswordDialog.vue"
import useAppStore from "@/store/modules/app"
import useUserStore from "@/store/modules/user"
import useSettingsStore from "@/store/modules/settings"
import { getCachedStudentTrainingTasks, loadStudentTrainingTasks } from "@/api/trainingTask"
import { getComprehensiveTopTask } from "@/utils/studentTrainingNavigation"
const { proxy } = getCurrentInstance()
const router = useRouter()
const appStore = useAppStore()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const role = computed(() => Number(userStore.userInfo.roleId))
const roleText = computed(() => {
const roleMap = { 1: '平台管理员', 2: '学校管理员', 3: '教师', 4: '学生' };
return roleMap[role.value] || '用户';
});
const roleMap = { 1: '平台管理员', 2: '学校管理员', 3: '教师', 4: '学生' }
return roleMap[role.value] || '用户'
})
const studentTopTask = ref(null);
const changePasswordVisible = ref(false);
const studentTopTask = ref(null)
const changePasswordVisible = ref(false)
const mentList = ref([
{ name: "首页", path: "/index", icon: HomeFilled },
@ -179,23 +189,23 @@ const mentList = ref([
{ name: "学生管理", path: "/student/index", role: 3, teacherMenu: true, activePrefix: "/student" },
{ name: "任务分配", path: "/task/index", role: 3, teacherMenu: true, activePrefix: "/task" },
{ name: "成绩中心", path: "/score/index", role: 3, teacherMenu: true, activePrefix: "/score" },
]);
const activement = ref(mentList.value[0]);
])
const activement = ref(mentList.value[0])
function toggleSideBar() {
appStore.toggleSideBar();
appStore.toggleSideBar()
}
function handleCommand(command) {
switch (command) {
case "changePassword":
changePasswordVisible.value = true;
break;
changePasswordVisible.value = true
break
case "logout":
logout();
break;
logout()
break
default:
break;
break
}
}
@ -213,11 +223,11 @@ function onPasswordChanged() {
)
.then(() => {
userStore.logOut().then(() => {
location.href = "/index";
});
location.href = "/index"
})
})
.catch(() => {});
}, 200);
.catch(() => { })
}, 200)
}
function logout() {
@ -228,71 +238,71 @@ function logout() {
})
.then(() => {
userStore.logOut().then(() => {
location.href = "/index";
});
location.href = "/index"
})
})
.catch(() => {});
.catch(() => { })
}
const go = (item) => {
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) => {
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 currentRole = role.value;
const currentRole = role.value
if (currentRole === 3) {
const teacherOrder = ["/index", "/analysis/index", "/ai/teaching-plan"];
return teacherOrder.map((path) => mentList.value.find((item) => item.path === path)).filter(Boolean);
const teacherOrder = ["/index", "/analysis/index", "/ai/teaching-plan"]
return teacherOrder.map((path) => mentList.value.find((item) => item.path === path)).filter(Boolean)
}
const items = mentList.value.filter((item) => {
if (currentRole === 1) {
return item.path === "/index" || item.role === 1;
return item.path === "/index" || item.role === 1
}
if (currentRole === 2) {
return item.path === "/index";
return item.path === "/index"
}
if (!item.role || item.path === "/index" || item.path === "/analysis/index") return true;
return item.role == currentRole;
});
if (currentRole !== 4 || !studentTopTask.value) return items;
const homeIndex = items.findIndex((item) => item.path === "/index");
if (!item.role || item.path === "/index" || item.path === "/analysis/index") return true
return item.role == currentRole
})
if (currentRole !== 4 || !studentTopTask.value) return items
const homeIndex = items.findIndex((item) => item.path === "/index")
return [
...items.slice(0, homeIndex + 1),
{ ...studentTopTask.value, name: studentTopTask.value.title, icon: Reading },
...items.slice(homeIndex + 1),
];
});
]
})
async function loadStudentTopTask() {
if (role.value !== 4) {
studentTopTask.value = null;
return;
studentTopTask.value = null
return
}
const cachedTasks = getCachedStudentTrainingTasks();
const cachedTasks = getCachedStudentTrainingTasks()
if (cachedTasks.length) {
studentTopTask.value = getComprehensiveTopTask(cachedTasks);
studentTopTask.value = getComprehensiveTopTask(cachedTasks)
}
try {
const res = await loadStudentTrainingTasks();
studentTopTask.value = getComprehensiveTopTask(res.data || []);
const res = await loadStudentTrainingTasks()
studentTopTask.value = getComprehensiveTopTask(res.data || [])
} catch (error) {
if (!cachedTasks.length) {
studentTopTask.value = null;
studentTopTask.value = null
}
}
}
onMounted(() => {
loadStudentTopTask();
});
loadStudentTopTask()
})
</script>
<style lang="scss" scoped>
@ -606,7 +616,9 @@ onMounted(() => {
backdrop-filter: none;
// 蓝色光点装饰与极简白底不匹配,移除
.nav-bg-decoration { display: none; }
.nav-bg-decoration {
display: none;
}
// 品牌区:去渐变去模糊,logo 描边收敛为教育蓝
// 超管端(role=1)品牌区使用 .navTitle,教师端(role=3)使用 .TnavTitle,统一覆盖
@ -619,8 +631,14 @@ onMounted(() => {
.logo-icon {
filter: none;
path, circle { stroke: #3b82f6; }
path:first-child { fill: rgba(59, 130, 246, 0.1); }
path,
circle {
stroke: #3b82f6;
}
path:first-child {
fill: rgba(59, 130, 246, 0.1);
}
}
span,
@ -672,7 +690,9 @@ onMounted(() => {
background: #3b82f6;
}
&::before { display: none; }
&::before {
display: none;
}
}
// teacher-nav 定位规则保留,胶囊样式全部清除
@ -694,7 +714,9 @@ onMounted(() => {
border: 0;
box-shadow: none;
.menu-indicator { background: #3b82f6; }
.menu-indicator {
background: #3b82f6;
}
}
}
}
@ -752,7 +774,9 @@ onMounted(() => {
border-bottom: 1px solid #e5e7eb;
box-shadow: none;
.nav-bg-decoration { display: none; }
.nav-bg-decoration {
display: none;
}
// 品牌区:去渐变去发光,logo 描边收敛为教育蓝
.navTitle {
@ -762,10 +786,14 @@ onMounted(() => {
.logo-icon {
filter: none;
path, circle {
path,
circle {
stroke: #2563eb;
}
path:first-child { fill: rgba(37, 99, 235, 0.1); }
path:first-child {
fill: rgba(37, 99, 235, 0.1);
}
}
.nacber-name {
@ -794,7 +822,10 @@ onMounted(() => {
background: #2563eb;
}
.menu-ico { font-size: 15px; color: inherit; }
.menu-ico {
font-size: 15px;
color: inherit;
}
&:hover {
color: #1f2937;
@ -819,7 +850,9 @@ onMounted(() => {
background: #2563eb;
}
&::before { display: none; }
&::before {
display: none;
}
}
}
@ -841,7 +874,9 @@ onMounted(() => {
}
}
.divider { background: #e5e7eb; }
.divider {
background: #e5e7eb;
}
.avatar-container .avatar-wrapper {
background: #ffffff;
@ -863,7 +898,9 @@ onMounted(() => {
}
}
.dropdown-icon { color: #6b7280; }
.dropdown-icon {
color: #6b7280;
}
}
}
}
@ -976,7 +1013,9 @@ onMounted(() => {
@media (max-width: 768px) {
.navbar {
.navTitle, .TnavTitle {
.navTitle,
.TnavTitle {
.logo-area {
.nacber-name {
font-size: 13px;
@ -1069,7 +1108,9 @@ onMounted(() => {
/* ===== 学生端用户下拉:白底墨黑 ===== */
.student-dropdown-popper {
.el-popper__arrow { display: none !important; }
.el-popper__arrow {
display: none !important;
}
.dropdown-panel {
margin: 0 !important;
@ -1094,6 +1135,7 @@ onMounted(() => {
font-weight: 600;
letter-spacing: 0;
}
.user-role {
color: #6b7280 !important;
font-size: var(--student-fs-caption);
@ -1141,7 +1183,9 @@ onMounted(() => {
outline: none;
transform: none !important;
svg { color: #1f2937; }
svg {
color: #1f2937;
}
}
&.logout,
@ -1150,14 +1194,19 @@ onMounted(() => {
&.logout:focus-visible {
color: #d9554c !important;
svg { color: #d9554c; }
svg {
color: #d9554c;
}
}
&.logout:hover,
&.logout:focus,
&.logout:focus-visible {
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) {
let arr = ["课程知识点", "核心知识点", "最难的部分", "实务知识概述", "学习建议", "业务现状", "理论研究", "发展趋势"]
if (arr.includes(type.name)) {
inputMessage.value = type.com.replace("xxx", "电商互联网产品开发实训系统")
inputMessage.value = type.com.replace("xxx", "互联网产品开发实训系统")
} else {
inputMessage.value = type.com
}
@ -908,7 +908,7 @@ onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
});
})
</script>
<style lang="scss">
.index {

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

@ -1,188 +1,728 @@
<template>
<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">
<h3 class="title">电商互联网产品开发实训系统</h3>
<h3 class="title">欢迎回来</h3>
<p class="subtitle">请登录你的账号,继续今天的实训</p>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" autocomplete="off" placeholder="账号">
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
<el-input v-model="loginForm.username" type="text" size="large" autocomplete="off"
placeholder="账号">
<template #prefix><svg-icon icon-class="user"
class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<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>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
<el-input v-model="loginForm.password" class="password-input" type="password" size="large"
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-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-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-else>登 录 中...</span>
</el-button>
<div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
</div>
<!-- 底部 -->
<div class="el-login-footer" style="display: none">
<span>Copyright © 2018-2023 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</div>
</template>
<script setup>
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from "@/store/modules/user";
import { getUserInfo } from "@/utils/auth";
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from "@/store/modules/user"
import { getUserInfo } from "@/utils/auth"
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const loginForm = ref({
username: "",
password: "",
rememberMe: false,
TOKEN: "",
});
})
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
code: [{ required: true, trigger: "change", message: "请输入验证码" }],
};
}
const loading = ref(false);
const loadpage = ref(false);
const loading = ref(false)
const loadpage = ref(false)
// 验证码开关
const captchaEnabled = ref(true);
const captchaEnabled = ref(true)
// 注册开关
const register = ref(false);
const redirect = ref(undefined);
const register = ref(false)
const redirect = ref(undefined)
watch(
route,
(newRoute) => {
if (newRoute.query.token) {
loadpage.value = true;
loadpage.value = true
setTimeout(() => {
loginForm.value.TOKEN = newRoute.query.token;
return handleLogin();
}, 1000);
loginForm.value.TOKEN = newRoute.query.token
return handleLogin()
}, 1000)
}
},
{ immediate: true }
);
)
function handleLogin() {
// proxy.$refs.loginRef.validate(valid => {
// if (valid) {
loading.value = true;
loadpage.value = true;
loading.value = true
loadpage.value = true
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 });
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
Cookies.set("username", loginForm.value.username, { expires: 30 })
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 })
} else {
// 否则移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
Cookies.remove("username")
Cookies.remove("password")
Cookies.remove("rememberMe")
}
//登录
const params = {
userName: loginForm.value.username,
passwordEncode: loginForm.value.password !== "" ? encrypt(loginForm.value.password) : loginForm.value.password,
TOKEN: loginForm.value.token,
};
}
if (loginForm.value.TOKEN !== "") {
params.token = loginForm.value.TOKEN;
params.token = loginForm.value.TOKEN
}
userStore
.login(params)
.then((res) => {
if (params.token) {
setTimeout(() => {
loading.value = false;
loadpage.value = false;
router.push({ path: redirect.value || "/index" });
}, 2000);
loading.value = false
loadpage.value = false
router.push({ path: redirect.value || "/index" })
}, 2000)
} else {
loading.value = false;
router.push({ path: redirect.value || "/index" });
loading.value = false
router.push({ path: redirect.value || "/index" })
}
})
.catch(() => {
loadpage.value = false;
loading.value = false;
});
loadpage.value = false
loading.value = false
})
}
function getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
const username = Cookies.get("username")
const password = Cookies.get("password")
const rememberMe = Cookies.get("rememberMe")
loginForm.value = {
username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
};
}
}
// 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>
<style lang="scss" scoped>
.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;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("@/assets/images/login-background.0dded3b4.webp");
background-size: 100% 100%;
overflow: hidden;
padding: 52px 68px;
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;
color: rgb(48, 50, 52);
font-size: var(--student-fs-h2);
font-weight: 500;
}
.login-main {
width: 85%;
height: 100%;
.node-index {
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;
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;
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;
width: 400px;
padding: 55px 25px 33px 25px;
padding: 40px 32px;
}
.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 {
height: 40px;
height: 46px;
input {
height: 40px;
height: 46px;
}
}
: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;
box-shadow: 0 0 0 2px #2b5aed inset, 0 0 0 4px rgba(43, 90, 237, 0.12) !important;
}
}
:deep(.el-input__inner),
:deep(input) {
color: #303133;
color: #1d2129;
background-color: transparent !important;
-webkit-text-fill-color: #303133;
-webkit-text-fill-color: #1d2129;
}
:deep(.el-input__inner::placeholder),
:deep(input::placeholder) {
color: #b8c0cc;
-webkit-text-fill-color: #b8c0cc;
color: #a8abb2;
-webkit-text-fill-color: #a8abb2;
opacity: 1;
}
@ -194,9 +734,9 @@ getCookie();
:deep(input:-webkit-autofill:hover),
:deep(input:-webkit-autofill:focus),
:deep(input:-webkit-autofill:active) {
caret-color: #303133 !important;
-webkit-text-fill-color: #303133 !important;
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
caret-color: #1d2129 !important;
-webkit-text-fill-color: #1d2129 !important;
-webkit-box-shadow: 0 0 0 1000px #f5f7fa inset !important;
transition: background-color 9999s ease-out 0s !important;
}
@ -204,36 +744,114 @@ getCookie();
height: 39px;
width: 14px;
margin-left: 0px;
color: #86909c;
}
}
.login-tip {
font-size: var(--student-fs-body-sm);
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 40px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
.form-options {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 22px;
.link-type {
font-size: 13px;
color: #2b5aed;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.login-btn {
position: relative;
overflow: hidden;
width: 100%;
text-align: center;
color: #fff;
font-family: var(--student-font-base);
font-size: var(--student-fs-caption);
letter-spacing: 1px;
height: 46px;
border: none;
border-radius: 10px;
font-size: 16px;
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;
}
}
.login-code-img {
height: 40px;
padding-left: 12px;
@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;
}
}
</style>

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

@ -143,6 +143,8 @@
@close="onClose">
<el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px"
style="width: 80%" :rules="rules">
<!-- 2026-10-12 停用:所属院系 / 所属专业的下拉选择(新增教师不再选择院系与专业)。
原始代码保留备用;恢复时打开下面这段注释,并删掉其后两个只读 el-form-item。
<el-form-item v-if="status || isOrganizationManagementEnabled" label="所属院系:" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="" clearable @change="getMajorList"
style="width: 20rem" v-if="status">
@ -160,6 +162,14 @@
<el-input v-else v-model="classInfo.value" placeholder="请输入所属专业:" style="width: 20rem"
disabled></el-input>
</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-input v-model="formInline.name" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
@ -285,7 +295,9 @@ const editSchool = (row) => {
})
}
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
indexApi.addTeacher(formInline.value).then((res) => {
proxy.$message.success("添加成功")

Loading…
Cancel
Save