feat: 初始化实践教学评价平台项目
- 创建Vue3项目结构,使用ElementPlus和ECharts - 实现统一样式系统和可复用组件(BaseCard、BaseButton、PageLayout) - 完成登录页面设计,支持四种用户角色(学生、教师、企业、专家) - 实现登录功能和基于角色的权限控制系统 - 配置路由守卫和状态管理(Pinia) - 创建Mock数据生成器支持演示功能 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
b3d4f5d7c3
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__puppeteer__puppeteer_screenshot",
|
||||
"mcp__puppeteer__puppeteer_click",
|
||||
"mcp__puppeteer__puppeteer_fill",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
81
CLAUDE.md
Normal file
81
CLAUDE.md
Normal file
@ -0,0 +1,81 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个实践教学评价平台演示项目,支持企业、教师、行业协会专家、学生四类角色进行实践成果提交、评价、画像展示、报告生成等功能。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端**: Vue3 + ElementPlus + ECharts
|
||||
- **数据**: Mock数据,无后端
|
||||
- **目标平台**: PC Web端(不支持移动端)
|
||||
|
||||
## 核心架构
|
||||
|
||||
### 用户角色系统
|
||||
- 学校管理员:仅维护账号
|
||||
- 企业:对学生实践成果进行指标打分、上传佐证材料
|
||||
- 教师:评价 + 查看学生画像 + 生成报告
|
||||
- 行业协会专家:输入评价指标与意见
|
||||
- 学生:提交成果、自评、互评
|
||||
|
||||
### 主要功能模块
|
||||
1. **登录鉴权** - 统一登录页,基于角色的权限控制
|
||||
2. **评价流程** - 筛选页 + 角色专属评价页
|
||||
3. **学生画像** - 成绩分布图表、能力矩阵雷达图(仅教师可见)
|
||||
4. **评价报告** - 发展监测、专项分析、报告生成
|
||||
|
||||
### 路由结构
|
||||
- `/` - 首页双入口
|
||||
- `/evaluate` - 评价页面
|
||||
- `/portrait` - 学生能力画像(仅教师)
|
||||
- `/report/{studentId}` - 评价报告页
|
||||
- `/report/{id}/monitor` - 发展监测
|
||||
- `/report/{id}/analysis` - 能力报告分析
|
||||
|
||||
### Mock数据结构设计
|
||||
- 用户信息与角色数据
|
||||
- 项目信息(学期、年级、班级)
|
||||
- 评价数据
|
||||
- 学生画像数据
|
||||
- 成绩分布和能力矩阵数据
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
### 演示项目特点
|
||||
- 纯前端项目,所有数据使用Mock生成
|
||||
- 无后端接口,数据存储在前端内存/LocalStorage
|
||||
- 无复杂加密要求
|
||||
- 重点关注前端交互和界面展示
|
||||
|
||||
### 权限控制
|
||||
- 使用前端路由守卫模拟权限控制
|
||||
- 学生画像页面仅教师角色可见
|
||||
- 不同角色在评价页面显示不同功能按钮
|
||||
|
||||
### 文件上传模拟
|
||||
- 支持类型:pdf/doc/jpg/png
|
||||
- 单文件大小:≤10MB
|
||||
- 前端模拟文件上传,显示文件列表
|
||||
|
||||
### 图表组件
|
||||
- 使用ECharts实现柱状图、雷达图、折线图
|
||||
- 画像页面包含成绩分布柱状图和能力矩阵三角形雷达图
|
||||
- 使用Mock数据生成随机但合理的图表数据
|
||||
|
||||
### 页面交互
|
||||
- 弹框输入评价指标和意见
|
||||
- Toast提示完成状态
|
||||
- 表格展示学生名单和评分
|
||||
- PDF报告下载(可使用静态文件)
|
||||
|
||||
## Mock数据管理
|
||||
- 使用统一的Mock数据生成器
|
||||
- 角色登录状态使用LocalStorage持久化
|
||||
- 评价数据和文件上传记录存储在前端
|
||||
- 图表数据实时生成,确保演示效果
|
||||
|
||||
## 当前状态
|
||||
项目目前只有PRD文档,需要从零开始搭建Vue3纯前端演示项目,所有数据通过Mock生成。
|
||||
158
Readme.md
Normal file
158
Readme.md
Normal file
@ -0,0 +1,158 @@
|
||||
实践教学评价平台
|
||||
产品需求文档(PRD)
|
||||
版本:V1.0
|
||||
撰写日期:2024-05-XX
|
||||
撰写人:XXX
|
||||
|
||||
1. 文档目的
|
||||
本 PRD 用于指导开发、测试、UI/UX、运维团队一次性、零偏差地实现《平台设计.docx》中提出的全部功能。任何需求冲突以本 PRD 为准。
|
||||
|
||||
2. 项目背景
|
||||
学校需要一套线上实践教学评价系统,支持企业、教师、行业协会专家、学生四类角色,完成实践成果提交、评价、画像展示、报告生成等闭环流程。
|
||||
|
||||
3. 产品范围
|
||||
• 范围包含:登录、首页双入口、评价流程、学生能力画像、发展监测、专项分析、报告下载。
|
||||
• 非范围:真实 AI 影像/音频/文本分析算法、真实数据对接、移动端适配(本期仅支持 PC Web)。
|
||||
|
||||
4. 目标用户
|
||||
角色 | 描述
|
||||
-----------|------------------------------
|
||||
学校管理员 | 仅维护账号,不参与业务
|
||||
企业 | 对学生实践成果进行指标打分、上传佐证材料
|
||||
教师 | 同上 + 查看学生画像、生成报告
|
||||
行业协会专家 | 仅输入评价指标与意见
|
||||
学生 | 提交成果、自评、互评
|
||||
|
||||
5. 功能需求
|
||||
以下章节采用「用户故事 + 验收标准」格式,可直接转为测试用例。
|
||||
|
||||
5.1 登录与鉴权
|
||||
US-1 作为任意角色用户,我可以通过学校统一登录页进入系统,以便进行后续操作。
|
||||
• 字段:用户名、密码、角色下拉(学生/教师/企业/行业协会专家)。
|
||||
• 登录后根据角色写 Session / JWT,后续路由守卫校验。
|
||||
• 失败提示:「用户名或密码错误」。
|
||||
|
||||
5.2 首页双入口
|
||||
US-2 登录成功后,我可以看到两个卡片入口:
|
||||
A. 评价页面(所有角色可见)
|
||||
B. 学生能力评价画像(仅教师角色可见,其余角色前端隐藏)
|
||||
|
||||
5.3 评价流程
|
||||
5.3.1 筛选页
|
||||
US-3 点击「评价页面」后,我首先进入筛选页:
|
||||
• 下拉:学期(2023-2024-1…)、年级(2021…)、班级(计科1班…)、项目名称(XX公司实训)。
|
||||
• 「确认」按钮,校验四项全部已选才可点击。
|
||||
|
||||
5.3.2 角色专属评价页
|
||||
路由:/evaluate
|
||||
US-4 企业/教师:
|
||||
• 两个按钮:
|
||||
①「输入评价指标」——弹框输入文本,保存后按钮变「已填写」。
|
||||
②「上传学生材料」——多文件上传(pdf/doc/jpg/png),大小≤10 MB,列表展示文件名、大小、删除图标。
|
||||
|
||||
US-5 行业协会专家:
|
||||
• 两个按钮:
|
||||
①「输入评价指标」——同上。
|
||||
②「输入评价意见」——同上。
|
||||
|
||||
US-6 学生:
|
||||
• 三个按钮:
|
||||
①「提交实践成果」——文件上传同上。
|
||||
②「自评」——弹框打分(1-5 星)+ 文字评语。
|
||||
③「互评」——表格列出同组同学,每人打分+评语。
|
||||
• 全部完成后显示绿色「提交完成」Toast,2 秒后自动返回首页。
|
||||
|
||||
5.4 学生能力评价画像(仅教师)
|
||||
路由:/portrait
|
||||
静态页面,假数据即可。布局如下:
|
||||
┌────────────┬────────────┬────────────┐
|
||||
│ 成绩分布 │ 中上:学生人数 │ 学生名单 │
|
||||
│ 柱状图 │ 文本+图标 │ 姓名/学号/ │
|
||||
│ │ │ 年级/超链 │
|
||||
├────────────┼────────────┼────────────┤
|
||||
│ 能力矩阵 │ 中下:信息块 │ 多维度检测 │
|
||||
│ 三角形雷达 │ │ 三个按钮 │
|
||||
└────────────┴────────────┴────────────┘
|
||||
|
||||
• 成绩分布:ECharts 柱状图,X 轴 5 档成绩,Y 轴人数。
|
||||
• 能力矩阵:三角形雷达,三指标(专业基础、专业综合、数智应用),值随机 60-95。
|
||||
• 学生名单:表格,最后一列「评价报告」为蓝色按钮,点击跳转到 /report/{studentId}。
|
||||
• 多维度检测:影像分析 / 音频分析 / 文本分析(仅 UI 按钮,点击弹 Toast「功能暂未开放」)。
|
||||
• 右下角再置两个按钮:
|
||||
①「评价报告」——同名单里的跳转。
|
||||
②「历史记录查看」——暂留空页面,二期实现。
|
||||
|
||||
5.5 评价报告页(/report/{studentId})
|
||||
布局:左侧垂直导航(平台首页 | 发展监测 | 能力报告分析),右侧动态内容。
|
||||
|
||||
5.5.1 平台首页
|
||||
• 点击直接返回 /portrait。
|
||||
|
||||
5.5.2 发展监测
|
||||
路由:/report/{id}/monitor
|
||||
• 顶部三筛选:学年区间、班级、项目(与评价流程一致)。
|
||||
• 上左:学生基本信息卡片(姓名、学期、年级、参加项目)。
|
||||
• 上右:柱状图,展示企业/行业/教师/学生四类主体评分(假数据)。
|
||||
• 中间:
|
||||
左:大数字「总体分数 88」。
|
||||
右:横向条形图(示例:专业能力 90,团队协作 85…)。
|
||||
|
||||
5.5.3 能力报告分析(专项分析)
|
||||
路由:/report/{id}/analysis
|
||||
• 三栏文本:
|
||||
① 优势分析(占位段落)
|
||||
② 不足分析(占位段落)
|
||||
③ 改进建议(占位段落)
|
||||
• 历史评分:折线图,X 轴学年,Y 轴分数,随机 3 个点。
|
||||
• 底部按钮「生成评价报告」:
|
||||
- 点击调用后端接口 /api/report/generate,返回 PDF URL,浏览器下载。
|
||||
- 若后端未就绪,前端 mock:下载一个静态 PDF(占位)。
|
||||
|
||||
6. 非功能需求
|
||||
• 技术栈:Vue3 + ElementPlus + ECharts + SpringBoot + MySQL。
|
||||
• 兼容性:Chrome 90+、Edge 90+。
|
||||
• 性能:页面首屏 < 2s,接口 95% < 500 ms。
|
||||
• 安全:密码加盐哈希(bcrypt),上传文件类型白名单,XSS/CSRF 防护。
|
||||
• 可用性:上线率 99.9%,数据每日自动备份。
|
||||
|
||||
7. 数据字典(核心表)
|
||||
表 user
|
||||
id | username | password_hash | role(enum)
|
||||
|
||||
表 project
|
||||
id | name | semester | grade | class
|
||||
|
||||
表 evaluation
|
||||
id | project_id | student_id | evaluator_role | evaluator_id | score | comment | file_urls(json)
|
||||
|
||||
表 portrait_data(假数据用)
|
||||
student_id | score_distribution_json | ability_matrix_json
|
||||
|
||||
8. 接口清单(关键)
|
||||
POST /api/login
|
||||
GET /api/projects?semester=&grade=&class=
|
||||
POST /api/evaluation
|
||||
GET /api/portrait/{studentId}
|
||||
GET /api/report/{studentId}/monitor
|
||||
GET /api/report/{studentId}/analysis
|
||||
POST /api/report/generate
|
||||
|
||||
9. 里程碑
|
||||
阶段 | 时间 | 交付物
|
||||
---------|-----------|------------------
|
||||
需求冻结 | T0 | 本 PRD + 原型
|
||||
UI 设计 | T0+3 天 | Figma 高保真
|
||||
前端完成 | T0+2 周 | 静态页面 + 联调
|
||||
后端完成 | T0+3 周 | 接口 + 假数据
|
||||
测试 | T0+4 周 | 测试报告
|
||||
上线 | T0+5 周 | 生产环境
|
||||
|
||||
10. 风险与应对
|
||||
风险:专家角色并发量低,可能遗漏测试。
|
||||
应对:在测试用例中强制覆盖专家流程。
|
||||
|
||||
11. 附录
|
||||
• 原型链接(Figma):https://figma.com/xxx
|
||||
• 设计稿切图命名规范:见《UI规范.pdf》
|
||||
|
||||
—— 以上,如无异议即进入开发。
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>实践教学评价平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1505
package-lock.json
generated
Normal file
1505
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "zhangqipro",
|
||||
"version": "1.0.0",
|
||||
"description": "实践教学评价平台",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.8.8",
|
||||
"pinia": "^2.2.7",
|
||||
"vite": "^7.1.5",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4.5.1"
|
||||
}
|
||||
}
|
||||
30
src/App.vue
Normal file
30
src/App.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './assets/styles/variables.css';
|
||||
@import './assets/styles/common.css';
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
109
src/assets/styles/common.css
Normal file
109
src/assets/styles/common.css
Normal file
@ -0,0 +1,109 @@
|
||||
/* 通用样式类 */
|
||||
.container {
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: var(--spacing-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.mb-sm { margin-bottom: var(--spacing-sm); }
|
||||
.mb-md { margin-bottom: var(--spacing-md); }
|
||||
.mb-lg { margin-bottom: var(--spacing-lg); }
|
||||
.mb-xl { margin-bottom: var(--spacing-xl); }
|
||||
|
||||
.mt-sm { margin-top: var(--spacing-sm); }
|
||||
.mt-md { margin-top: var(--spacing-md); }
|
||||
.mt-lg { margin-top: var(--spacing-lg); }
|
||||
.mt-xl { margin-top: var(--spacing-xl); }
|
||||
|
||||
.p-sm { padding: var(--spacing-sm); }
|
||||
.p-md { padding: var(--spacing-md); }
|
||||
.p-lg { padding: var(--spacing-lg); }
|
||||
.p-xl { padding: var(--spacing-xl); }
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
64
src/assets/styles/variables.css
Normal file
64
src/assets/styles/variables.css
Normal file
@ -0,0 +1,64 @@
|
||||
:root {
|
||||
/* 主题色彩 */
|
||||
--primary-color: #409eff;
|
||||
--primary-light: #79bbff;
|
||||
--primary-dark: #337ecc;
|
||||
--success-color: #67c23a;
|
||||
--warning-color: #e6a23c;
|
||||
--danger-color: #f56c6c;
|
||||
--info-color: #909399;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #303133;
|
||||
--text-regular: #606266;
|
||||
--text-secondary: #909399;
|
||||
--text-placeholder: #c0c4cc;
|
||||
|
||||
/* 背景色 */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f7fa;
|
||||
--bg-tertiary: #ebeef5;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-light: #dcdfe6;
|
||||
--border-base: #e4e7ed;
|
||||
--border-dark: #d3d4d6;
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 0.5rem;
|
||||
--spacing-sm: 0.75rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-xxl: 3rem;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* 字体 */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-md: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-xxl: 1.5rem;
|
||||
|
||||
/* 页面布局 */
|
||||
--header-height: 60px;
|
||||
--sidebar-width: 240px;
|
||||
--container-max-width: 1200px;
|
||||
|
||||
/* 渐变背景 */
|
||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
83
src/components/BaseButton.vue
Normal file
83
src/components/BaseButton.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<el-button
|
||||
:type="type"
|
||||
:size="size"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:icon="icon"
|
||||
:class="['base-button', `btn-${variant}`]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BaseButton',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['default', 'gradient', 'outline'].includes(value)
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
if (!this.loading && !this.disabled) {
|
||||
this.$emit('click', event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.base-button {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
background: var(--gradient-primary);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
80
src/components/BaseCard.vue
Normal file
80
src/components/BaseCard.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="base-card" :class="[`card-${size}`, { 'card-shadow': shadow }]">
|
||||
<div v-if="title || $slots.header" class="card-header">
|
||||
<slot name="header">
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="$slots.footer" class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BaseCard',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: (value) => ['sm', 'md', 'lg'].includes(value)
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.base-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-sm {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-md {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.card-lg {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
</style>
|
||||
91
src/components/PageLayout.vue
Normal file
91
src/components/PageLayout.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="page-layout">
|
||||
<div v-if="showHeader" class="page-header">
|
||||
<slot name="header">
|
||||
<div class="header-content">
|
||||
<h2 class="page-title">{{ title }}</h2>
|
||||
<div v-if="$slots.actions" class="page-actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="page-main">
|
||||
<div class="page-content" :class="[`content-${layout}`]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageLayout',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['default', 'centered', 'fluid'].includes(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-layout {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: var(--spacing-lg) 0;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-main {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.content-default {
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content-centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - var(--header-height) - var(--spacing-lg) * 2);
|
||||
}
|
||||
|
||||
.content-fluid {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
18
src/components/index.js
Normal file
18
src/components/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
import BaseCard from './BaseCard.vue'
|
||||
import BaseButton from './BaseButton.vue'
|
||||
import PageLayout from './PageLayout.vue'
|
||||
|
||||
// 全局注册组件
|
||||
export default {
|
||||
install(app) {
|
||||
app.component('BaseCard', BaseCard)
|
||||
app.component('BaseButton', BaseButton)
|
||||
app.component('PageLayout', PageLayout)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
BaseCard,
|
||||
BaseButton,
|
||||
PageLayout
|
||||
}
|
||||
23
src/main.js
Normal file
23
src/main.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import GlobalComponents from './components'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册所有Element Plus图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(GlobalComponents)
|
||||
|
||||
app.mount('#app')
|
||||
71
src/router/index.js
Normal file
71
src/router/index.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/evaluate',
|
||||
name: 'Evaluate',
|
||||
component: () => import('@/views/Evaluate.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/portrait',
|
||||
name: 'Portrait',
|
||||
component: () => import('@/views/Portrait.vue'),
|
||||
meta: { requiresAuth: true, role: 'teacher' }
|
||||
},
|
||||
{
|
||||
path: '/report/:studentId',
|
||||
name: 'Report',
|
||||
component: () => import('@/views/Report.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 检查登录状态
|
||||
authStore.checkAuth()
|
||||
|
||||
// 如果访问登录页且已登录,重定向到首页
|
||||
if (to.name === 'Login' && authStore.isLoggedIn) {
|
||||
return next('/home')
|
||||
}
|
||||
|
||||
// 如果路由需要认证
|
||||
if (to.meta.requiresAuth) {
|
||||
if (!authStore.isLoggedIn) {
|
||||
ElMessage.error('请先登录')
|
||||
return next('/')
|
||||
}
|
||||
|
||||
// 如果路由需要特定角色
|
||||
if (to.meta.role && authStore.user?.role !== to.meta.role) {
|
||||
ElMessage.error('您没有权限访问该页面')
|
||||
return next('/home')
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
30
src/stores/auth.js
Normal file
30
src/stores/auth.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: null,
|
||||
isLoggedIn: false
|
||||
}),
|
||||
|
||||
actions: {
|
||||
login(userInfo) {
|
||||
this.user = userInfo
|
||||
this.isLoggedIn = true
|
||||
localStorage.setItem('user', JSON.stringify(userInfo))
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.user = null
|
||||
this.isLoggedIn = false
|
||||
localStorage.removeItem('user')
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
const stored = localStorage.getItem('user')
|
||||
if (stored) {
|
||||
this.user = JSON.parse(stored)
|
||||
this.isLoggedIn = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
41
src/utils/mockData.js
Normal file
41
src/utils/mockData.js
Normal file
@ -0,0 +1,41 @@
|
||||
// Mock用户数据
|
||||
export const mockUsers = [
|
||||
{ username: 'student1', password: '123456', role: 'student', name: '张三' },
|
||||
{ username: 'teacher1', password: '123456', role: 'teacher', name: '李老师' },
|
||||
{ username: 'company1', password: '123456', role: 'company', name: '企业代表' },
|
||||
{ username: 'expert1', password: '123456', role: 'expert', name: '行业专家' }
|
||||
]
|
||||
|
||||
// Mock项目选择数据
|
||||
export const mockOptions = {
|
||||
semesters: ['2023-2024-1', '2023-2024-2', '2024-2025-1'],
|
||||
grades: ['2021级', '2022级', '2023级'],
|
||||
classes: ['计科1班', '计科2班', '软工1班', '软工2班'],
|
||||
projects: ['XX公司实训', 'YY企业项目', 'ZZ技术实习']
|
||||
}
|
||||
|
||||
// Mock学生数据
|
||||
export const mockStudents = [
|
||||
{ id: 1, name: '张三', studentId: '202101001', grade: '2021级', class: '计科1班' },
|
||||
{ id: 2, name: '李四', studentId: '202101002', grade: '2021级', class: '计科1班' },
|
||||
{ id: 3, name: '王五', studentId: '202101003', grade: '2021级', class: '计科1班' },
|
||||
{ id: 4, name: '赵六', studentId: '202101004', grade: '2021级', class: '计科1班' }
|
||||
]
|
||||
|
||||
// Mock图表数据生成器
|
||||
export const generateChartData = () => {
|
||||
return {
|
||||
scoreDistribution: [
|
||||
{ grade: '优秀', count: Math.floor(Math.random() * 10) + 5 },
|
||||
{ grade: '良好', count: Math.floor(Math.random() * 15) + 10 },
|
||||
{ grade: '中等', count: Math.floor(Math.random() * 20) + 15 },
|
||||
{ grade: '及格', count: Math.floor(Math.random() * 10) + 5 },
|
||||
{ grade: '不及格', count: Math.floor(Math.random() * 5) + 1 }
|
||||
],
|
||||
abilityMatrix: {
|
||||
professional: Math.floor(Math.random() * 35) + 60,
|
||||
comprehensive: Math.floor(Math.random() * 35) + 60,
|
||||
digital: Math.floor(Math.random() * 35) + 60
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/views/Evaluate.vue
Normal file
9
src/views/Evaluate.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>评价页面 - 开发中</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Evaluate'
|
||||
}
|
||||
</script>
|
||||
158
src/views/Home.vue
Normal file
158
src/views/Home.vue
Normal file
@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<PageLayout title="实践教学评价平台">
|
||||
<div class="home-container">
|
||||
<div class="welcome-section">
|
||||
<h2 class="welcome-text">欢迎,{{ user?.name }}</h2>
|
||||
<p class="role-text">当前角色:{{ getRoleText(user?.role) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="entry-cards">
|
||||
<BaseCard class="entry-card" @click="goToEvaluate">
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<EditPen :size="48" />
|
||||
</div>
|
||||
<h3 class="card-title">评价页面</h3>
|
||||
<p class="card-description">进行实践成果评价</p>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard
|
||||
v-if="user?.role === 'teacher'"
|
||||
class="entry-card"
|
||||
@click="goToPortrait"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<User :size="48" />
|
||||
</div>
|
||||
<h3 class="card-title">学生能力评价画像</h3>
|
||||
<p class="card-description">查看学生能力分析</p>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</div>
|
||||
|
||||
<div class="logout-section">
|
||||
<BaseButton @click="handleLogout" variant="outline">
|
||||
退出登录
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
|
||||
const getRoleText = (role) => {
|
||||
const roleMap = {
|
||||
student: '学生',
|
||||
teacher: '教师',
|
||||
company: '企业',
|
||||
expert: '行业协会专家'
|
||||
}
|
||||
return roleMap[role] || role
|
||||
}
|
||||
|
||||
const goToEvaluate = () => {
|
||||
router.push('/evaluate')
|
||||
}
|
||||
|
||||
const goToPortrait = () => {
|
||||
router.push('/portrait')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
getRoleText,
|
||||
goToEvaluate,
|
||||
goToPortrait,
|
||||
handleLogout
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xxl);
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.role-text {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.entry-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-xxl);
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.entry-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
415
src/views/Login.vue
Normal file
415
src/views/Login.vue
Normal file
@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="homepage">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
<School class="logo-icon" />
|
||||
<span class="platform-title">实践教学评价平台</span>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<span class="version">V1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 左侧功能介绍 -->
|
||||
<div class="intro-section">
|
||||
<div class="intro-content">
|
||||
<h1 class="main-title">智能化实践教学评价系统</h1>
|
||||
<p class="main-subtitle">支持多角色协同评价的综合性教学管理平台</p>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<User />
|
||||
</div>
|
||||
<h3>多角色支持</h3>
|
||||
<p>学生、教师、企业、专家四类用户</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<TrendCharts />
|
||||
</div>
|
||||
<h3>智能分析</h3>
|
||||
<p>能力画像与发展监测</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<Document />
|
||||
</div>
|
||||
<h3>报告生成</h3>
|
||||
<p>自动生成评价报告</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<DataAnalysis />
|
||||
</div>
|
||||
<h3>数据可视化</h3>
|
||||
<p>图表展示评价结果</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧登录区域 -->
|
||||
<div class="login-section">
|
||||
<div class="login-card">
|
||||
<div class="card-header">
|
||||
<h2>系统登录</h2>
|
||||
<p>请选择您的身份登录系统</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="loginForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
placeholder="用户名"
|
||||
size="large"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<User class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
size="large"
|
||||
clearable
|
||||
show-password
|
||||
>
|
||||
<template #prefix>
|
||||
<Lock class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="role">
|
||||
<el-select
|
||||
v-model="form.role"
|
||||
placeholder="选择角色"
|
||||
size="large"
|
||||
class="full-width"
|
||||
>
|
||||
<el-option label="学生" value="student" />
|
||||
<el-option label="教师" value="teacher" />
|
||||
<el-option label="企业" value="company" />
|
||||
<el-option label="行业协会专家" value="expert" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
class="login-btn"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="demo-accounts">
|
||||
<el-divider><span class="demo-title">快速体验</span></el-divider>
|
||||
<div class="demo-buttons">
|
||||
<el-button size="small" @click="quickLogin('student')">学生演示</el-button>
|
||||
<el-button size="small" @click="quickLogin('teacher')">教师演示</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<p>© 2024 实践教学评价平台 - 演示版本</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { mockUsers } from '@/utils/mockData'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
role: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const loginForm = ref(null)
|
||||
|
||||
const handleLogin = async () => {
|
||||
await loginForm.value.validate()
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 模拟登录验证
|
||||
const user = mockUsers.find(u =>
|
||||
u.username === form.username &&
|
||||
u.password === form.password &&
|
||||
u.role === form.role
|
||||
)
|
||||
|
||||
if (user) {
|
||||
authStore.login(user)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/home')
|
||||
} else {
|
||||
ElMessage.error('用户名或密码错误')
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const quickLogin = (role) => {
|
||||
if (role === 'student') {
|
||||
form.username = 'student1'
|
||||
form.password = '123456'
|
||||
form.role = 'student'
|
||||
} else if (role === 'teacher') {
|
||||
form.username = 'teacher1'
|
||||
form.password = '123456'
|
||||
form.role = 'teacher'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
rules,
|
||||
loading,
|
||||
loginForm,
|
||||
handleLogin,
|
||||
quickLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.homepage {
|
||||
height: 100vh;
|
||||
background: var(--gradient-primary);
|
||||
display: grid;
|
||||
grid-template-rows: 80px 1fr 60px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-xl);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.platform-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--text-placeholder);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
gap: var(--spacing-xl);
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
color: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: calc(var(--font-size-xxl) * 1.5);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.main-subtitle {
|
||||
font-size: var(--font-size-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-sm);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: calc(var(--font-size-xxl) * 1.5);
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
opacity: 0.8;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.login-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-xl);
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.login-form .el-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.demo-accounts {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.demo-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
justify-content: center;
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-content p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
9
src/views/Portrait.vue
Normal file
9
src/views/Portrait.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>学生画像页面 - 开发中</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Portrait'
|
||||
}
|
||||
</script>
|
||||
9
src/views/Report.vue
Normal file
9
src/views/Report.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>评价报告页面 - 开发中</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Report'
|
||||
}
|
||||
</script>
|
||||
16
vite.config.js
Normal file
16
vite.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user