chore: 添加项目配置文件和依赖
This commit is contained in:
commit
37d15fdda9
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"projectName": "aixuediebian-kanban",
|
||||
"projectId": "pages-eip23arpzoar",
|
||||
"deployUrl": "https://aixuediebian-kanban-3fzkam9s.edgeone.cool",
|
||||
"previewUrl": "https://aixuediebian-kanban-3fzkam9s.edgeone.cool?eo_token=e87d722adbc6936c1429a1f77bba0d71&eo_time=1776139462",
|
||||
"consoleUrl": "https://console.cloud.tencent.com/edgeone/pages/project/pages-eip23arpzoar/index",
|
||||
"deploymentUrl": "https://console.cloud.tencent.com/edgeone/pages/project/pages-eip23arpzoar/deployment/gnpthgy3s1",
|
||||
"deployId": "gnpthgy3s1",
|
||||
"lastDeployTime": 1776139462
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
name: inline-color-threshold-control
|
||||
overview: 在表格卡片头部添加内联数值调整控件,用于实时配置红色高亮阈值(默认10%),并将 getDiffClass/getNoParticipationDiffClass 中的硬编码 -10 替换为该响应式变量,同步更新图例文字。
|
||||
todos:
|
||||
- id: add-color-threshold-var
|
||||
content: 新增 colorThreshold 响应式变量、localStorage 加载逻辑与 watch 持久化
|
||||
status: completed
|
||||
- id: update-diff-functions
|
||||
content: 替换 getDiffClass 和 getNoParticipationDiffClass 中的硬编码 10 为 colorThreshold.value
|
||||
status: completed
|
||||
dependencies:
|
||||
- add-color-threshold-var
|
||||
- id: update-template-and-style
|
||||
content: 在 card-header 图例旁添加内联 a-input-number 控件,动态化图例文字,更新提示条文字,添加控件样式
|
||||
status: completed
|
||||
dependencies:
|
||||
- add-color-threshold-var
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
在作业打卡行为报告表格的 **card header 区域**,新增一个内联的数值调整控件,用于实时配置红色高亮的触发阈值。
|
||||
|
||||
## 产品概述
|
||||
|
||||
当前颜色判定逻辑中,红/橙/绿三色分界点硬编码为 `10`,无法动态调整。需将其改为响应式变量,并在表格头部提供直接可操作的输入控件,修改后立即生效,无需弹窗、无需刷新页面。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- **内联阈值控件**:在 card header 的颜色图例旁,添加一个带标签的 `a-input-number` 数值输入框,默认值 `10`,范围 `0.1 ~ 50`,步长 `0.5`,单位 `%`
|
||||
- **实时颜色更新**:输入框的值直接绑定到 `colorThreshold` 响应式变量,`getDiffClass` 和 `getNoParticipationDiffClass` 两个函数引用该变量,Vue 响应式系统自动触发表格重渲染
|
||||
- **动态图例**:颜色图例文字中的 `10%` 替换为 `{{ colorThreshold }}%`,随输入实时变化
|
||||
- **持久化**:通过 `localStorage` 保存用户设置的阈值,页面刷新后自动恢复
|
||||
- **提示文字更新**:更新 `.user-tip` 提示条内容,说明新控件的用途
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3 Composition API(`ref`、`watch`)
|
||||
- Ant Design Vue(`a-input-number`)
|
||||
- localStorage 持久化
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 核心思路
|
||||
|
||||
在 `<script setup>` 中新增 `colorThreshold` 响应式变量(`ref(10)`),替换 `getDiffClass` / `getNoParticipationDiffClass` 中的硬编码 `10`。由于这两个函数在 `#bodyCell` 模板中被调用,Vue 会追踪 `colorThreshold.value` 的依赖,值变化时自动触发单元格重渲染,无需任何额外 `watch`。
|
||||
|
||||
### 关键决策
|
||||
|
||||
- **内联控件而非弹窗**:用户明确要求在表格头部添加控件,直接修改 `card-header` 中的 `color-legend` 区域,将图例与输入框并排展示
|
||||
- **直接绑定而非 temp 变量**:与现有 `interactionThreshold`(需点击确认)不同,此控件直接 `v-model` 绑定 `colorThreshold`,输入即生效,无需确认步骤
|
||||
- **localStorage 持久化**:使用 `watch(colorThreshold, ...)` 监听变化并写入 `localStorage`,在 `onMounted` 中读取恢复
|
||||
|
||||
## 实现细节
|
||||
|
||||
1. **新增变量**(在现有 `interactionThreshold` 附近):
|
||||
|
||||
```js
|
||||
const COLOR_THRESHOLD_KEY = 'homework_report_color_threshold'
|
||||
const colorThreshold = ref(10)
|
||||
```
|
||||
|
||||
2. **onMounted 加载**:读取 localStorage,验证合法性后赋值给 `colorThreshold`
|
||||
|
||||
3. **watch 持久化**:
|
||||
|
||||
```js
|
||||
watch(colorThreshold, (val) => {
|
||||
localStorage.setItem(COLOR_THRESHOLD_KEY, String(val))
|
||||
})
|
||||
```
|
||||
|
||||
4. **替换两个颜色函数**中的 `-10` / `>= -10` 为 `-colorThreshold.value` / `>= -colorThreshold.value`
|
||||
|
||||
5. **模板修改**:
|
||||
|
||||
- `color-legend` 中图例文字动态化
|
||||
- 在图例右侧添加内联输入控件(标签 + `a-input-number`)
|
||||
- 更新 `.user-tip` 提示文字
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/components/
|
||||
└── CloudSchoolReport.vue # [MODIFY] 唯一修改文件
|
||||
```
|
||||
|
||||
### 修改点清单
|
||||
|
||||
| 位置 | 修改内容 |
|
||||
| --- | --- |
|
||||
| `<script>` 变量区 | 新增 `colorThreshold`、`COLOR_THRESHOLD_KEY` |
|
||||
| `onMounted` | 加载 localStorage 中保存的阈值 |
|
||||
| `watch` | 监听 `colorThreshold` 变化并持久化 |
|
||||
| `getDiffClass` | 硬编码 `10` → `colorThreshold.value` |
|
||||
| `getNoParticipationDiffClass` | 硬编码 `10` → `colorThreshold.value` |
|
||||
| `color-legend` 图例文字 | 静态 `10%` → 动态 `{{ colorThreshold }}%` |
|
||||
| `card-header` | 图例旁新增内联 `a-input-number` 控件 |
|
||||
| `.user-tip` 提示文字 | 更新说明文字,显示当前阈值 |
|
||||
| `<style>` | 新增 `.color-threshold-control` 内联控件样式 |
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "爱学蝶变原型设计"
|
||||
|
||||
[setup]
|
||||
script = "npm run dev"
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Tencent Cloud EdgeOne
|
||||
.env
|
||||
.edgeone/*
|
||||
.tef_dist/*
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
vendor/
|
||||
bower_components/
|
||||
jspm_packages/
|
||||
|
||||
# 构建输出
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
.nuxt/
|
||||
.cache/
|
||||
.temp/
|
||||
tmp/
|
||||
|
||||
# 环境配置文件
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
logs/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# 编辑器和IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.lcov
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
.turbo/
|
||||
|
||||
# 其他
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.npm/
|
||||
.eslintcache
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
# 列拖拽排序功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
表格组件已实现列的自定义排列顺序功能,支持:
|
||||
|
||||
1. **拖拽列头调整列位置** - 用户可以通过拖拽列头来重新排列列的顺序
|
||||
2. **保存自定义排序状态** - 使用 localStorage 保存列顺序,页面刷新后自动恢复
|
||||
3. **重置按钮** - 提供重置按钮,允许用户将表格恢复到默认的列顺序
|
||||
4. **流畅的拖拽交互** - 使用 Sortable.js 实现流畅的拖拽动画
|
||||
5. **跨浏览器兼容** - 兼容主流浏览器(Chrome, Firefox, Safari, Edge)
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
npm install sortablejs --save
|
||||
```
|
||||
|
||||
### 2. 核心功能
|
||||
|
||||
#### 列顺序状态管理
|
||||
- 使用 `columnKeys` ref 存储当前列顺序
|
||||
- 从 localStorage 加载保存的列顺序(键名:`homework_report_column_order`)
|
||||
- 默认列顺序定义在 `defaultColumnKeys` 数组中
|
||||
|
||||
#### 拖拽实现
|
||||
- 使用 Sortable.js 在表格列头(`.ant-table-thead > tr`)上初始化拖拽
|
||||
- 监听 `onEnd` 事件更新列顺序
|
||||
- 自动保存到 localStorage
|
||||
|
||||
#### 重置功能
|
||||
- 点击"重置列顺序"按钮恢复默认列顺序
|
||||
- 清除 localStorage 中保存的自定义顺序
|
||||
- 按钮在无自定义顺序时禁用
|
||||
|
||||
### 3. 添加样式(手动步骤)
|
||||
|
||||
由于样式替换遇到问题,请手动在 `<style scoped>` 标签中的 `:::deep(.ant-table-cell)` 样式后添加以下代码:
|
||||
|
||||
```css
|
||||
/* 列拖拽样式 */
|
||||
:::deep(.ant-table-thead > tr > th) {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:::deep(.ant-table-thead > tr > th:hover) {
|
||||
background: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
background: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
background: #e6f7ff !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
opacity: 0.8;
|
||||
background: #fff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 使用方法
|
||||
|
||||
1. **调整列顺序**:
|
||||
- 将鼠标悬停在表格列头上
|
||||
- 鼠标光标会变成移动样式(十字箭头)
|
||||
- 按住并拖动列头到目标位置
|
||||
- 释放鼠标完成列顺序调整
|
||||
|
||||
2. **重置列顺序**:
|
||||
- 点击表格上方的"重置列顺序"按钮
|
||||
- 表格将恢复到默认的列顺序
|
||||
- 按钮仅在存在自定义顺序时可用
|
||||
|
||||
3. **自动保存**:
|
||||
- 每次调整列顺序后自动保存
|
||||
- 刷新页面后自动恢复上次的列顺序
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Vue 3 Composition API** - 响应式状态管理
|
||||
- **Sortable.js** - 拖拽排序库
|
||||
- **Ant Design Vue** - 表格组件
|
||||
- **localStorage** - 本地存储
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- Chrome 60+
|
||||
- Firefox 55+
|
||||
- Safari 12+
|
||||
- Edge 79+
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 列顺序保存在用户浏览器本地,不会影响其他用户
|
||||
2. 清除浏览器数据会重置列顺序
|
||||
3. 不同浏览器/设备的列顺序相互独立
|
||||
4. 拖拽功能在移动设备上可能需要额外的触摸事件处理
|
||||
|
||||
## 待优化项
|
||||
|
||||
1. 添加触摸设备支持
|
||||
2. 提供列可见性控制(显示/隐藏列)
|
||||
3. 支持列宽度调整
|
||||
4. 导出表格时保持自定义列顺序
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# Git清理命令指南
|
||||
|
||||
## 📋 查看和验证命令
|
||||
|
||||
### 1. 查看当前状态
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
### 2. 预览将被.gitignore忽略的文件(仅删除临时文件)
|
||||
```bash
|
||||
git clean -d -X -n
|
||||
```
|
||||
|
||||
### 3. 预览所有未追踪的文件(包含重要文件)
|
||||
```bash
|
||||
git clean -d -n
|
||||
```
|
||||
|
||||
### 4. 查看未追踪的文件(排除.gitignore)
|
||||
```bash
|
||||
git ls-files --others --exclude-standard
|
||||
```
|
||||
|
||||
### 5. 查看已被.gitignore排除的文件
|
||||
```bash
|
||||
git ls-files --others --ignored --exclude-standard
|
||||
```
|
||||
|
||||
## 🧹 清理命令
|
||||
|
||||
### ✅ 推荐:仅删除临时文件(安全)
|
||||
```bash
|
||||
# 预览
|
||||
git clean -d -X -n
|
||||
|
||||
# 确认无误后执行
|
||||
git clean -d -X -f
|
||||
```
|
||||
|
||||
### ⚠️ 仅删除未被忽略的未追踪文件(危险!)
|
||||
```bash
|
||||
# 这会删除你的源代码!仅用于确认
|
||||
git clean -d -n
|
||||
|
||||
# 不要执行这个,除非你确定
|
||||
# git clean -d -f
|
||||
```
|
||||
|
||||
### ❌ 危险:删除所有未追踪文件
|
||||
```bash
|
||||
# 这会删除所有文件(包括源代码和临时文件)
|
||||
git clean -d -x -f
|
||||
```
|
||||
|
||||
## 🎯 推荐工作流程
|
||||
|
||||
### 步骤1:添加重要文件到Git
|
||||
```bash
|
||||
# 添加所有重要文件(排除.gitignore中的文件)
|
||||
git add .
|
||||
|
||||
# 查看已暂存的文件
|
||||
git status
|
||||
```
|
||||
|
||||
### 步骤2:首次提交
|
||||
```bash
|
||||
git commit -m "Initial commit"
|
||||
```
|
||||
|
||||
### 步骤3:清理临时文件
|
||||
```bash
|
||||
# 删除临时文件(node_modules, dist等)
|
||||
git clean -d -X -f
|
||||
```
|
||||
|
||||
### 步骤4:验证状态
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
## 📝 参数说明
|
||||
|
||||
- `-d`: 删除未追踪的目录
|
||||
- `-f`: 强制删除
|
||||
- `-n`: 预览模式,不实际删除(dry run)
|
||||
- `-X`: 仅删除被.gitignore忽略的文件
|
||||
- `-x`: 删除所有未追踪文件(忽略.gitignore)
|
||||
|
||||
## ⚠️ 重要提示
|
||||
|
||||
1. **始终先使用 `-n` 参数预览**
|
||||
2. **不要运行 `git clean -d -f`**,除非你确定要删除源代码
|
||||
3. **推荐使用 `git clean -d -X -f`** 清理临时文件
|
||||
4. **建议先提交重要文件**,再清理临时文件
|
||||
|
||||
## 🔄 恢复误删
|
||||
|
||||
如果误删了文件,可以使用以下方法恢复:
|
||||
```bash
|
||||
# 如果文件已提交过
|
||||
git checkout HEAD -- <文件路径>
|
||||
|
||||
# 如果文件未提交,但还在工作区
|
||||
# 可以使用系统回收站或备份恢复
|
||||
```
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# 爱学蝶变报告系统原型设计
|
||||
|
||||
这是一个基于 Vue 3 + TDesign 的产品原型网页,展示报告系统的核心功能和角色权限控制。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 核心功能
|
||||
- **四大报告入口模块**:
|
||||
- 作业打卡行为报告:作业打卡行为数据分析与趋势
|
||||
- 学校报告:各学校详细报告与对比分析
|
||||
- 班级报告:班级学习情况与教学质量分析
|
||||
- 学生报告:学生学习轨迹与个性化报告
|
||||
|
||||
### 角色权限控制
|
||||
- **总部长角色**:
|
||||
- 拥有最高权限,可查看并进入全部四类报告入口
|
||||
- 具备所有管理功能,包括数据分析、报告导出等
|
||||
|
||||
- **部长角色**:
|
||||
- 可查看并进入全部四类报告入口(作业打卡行为报告、学校报告、班级报告、学生报告)
|
||||
|
||||
- **学习官角色**:
|
||||
- 仅展示并可进入班级报告、学生报告两类入口
|
||||
|
||||
### 设计特点
|
||||
- 四大报告板块统一的视觉入口样式
|
||||
- 不同角色登录后,入口的显示/隐藏状态差异
|
||||
- 整体布局简洁、层级分明
|
||||
- 响应式设计,支持多设备访问
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端框架**: Vue 3 (Composition API)
|
||||
- **UI 组件库**: TDesign Vue Next
|
||||
- **构建工具**: Vite
|
||||
- **开发语言**: JavaScript
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 构建生产版本
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 预览生产版本
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
爱学蝶变原型设计/
|
||||
├── index.html # HTML 入口文件
|
||||
├── package.json # 项目配置文件
|
||||
├── vite.config.js # Vite 配置文件
|
||||
├── README.md # 项目说明文档
|
||||
└── src/
|
||||
├── main.js # Vue 应用入口
|
||||
├── App.vue # 主应用组件
|
||||
└── components/
|
||||
└── HomePage.vue # 首页组件
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 启动项目后,默认以"部长"角色登录
|
||||
2. 点击顶部导航栏的角色选择器,可切换角色:
|
||||
- 选择"总部长":显示全部四类报告入口,拥有最高权限
|
||||
- 选择"部长":显示全部四类报告入口
|
||||
- 选择"学习官":仅显示班级报告和学生报告
|
||||
3. 点击报告卡片可进入相应模块(原型阶段仅显示提示信息)
|
||||
|
||||
## 权限说明
|
||||
|
||||
- 总部长角色:可查看所有四类报告,拥有最高权限
|
||||
- 作业打卡行为报告、学校报告:总部长和部长角色可见
|
||||
- 班级报告、学生报告:总部长、部长和学习官角色均可见
|
||||
- 其他角色可后续扩展
|
||||
|
||||
## 扩展建议
|
||||
|
||||
如需扩展更多角色或功能,可修改以下文件:
|
||||
|
||||
1. `src/App.vue` - 添加新角色选项
|
||||
2. `src/components/HomePage.vue` - 在 `reports` 数据中配置新角色的权限
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
# 表格单元格颜色不显示问题诊断与修复
|
||||
|
||||
**问题**: 表格单元格没有根据数值显示颜色区分
|
||||
**诊断时间**: 2026-03-30
|
||||
**状态**: ✅ 已修复
|
||||
|
||||
---
|
||||
|
||||
## 🔴 问题原因
|
||||
|
||||
### 根本原因:CSS 作用域问题
|
||||
|
||||
**代码位置**: `CloudSchoolReport.vue` 第 677 行
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
```
|
||||
|
||||
**问题说明**:
|
||||
- 使用了 `<style scoped>` 导致样式被限定在当前组件作用域
|
||||
- Vue 会为 scoped 样式中的选择器添加唯一的属性选择器(如 `[data-v-xxx]`)
|
||||
- 通过渲染函数 `h('div', { class: 'cell-bg-red' }, ...)` 创建的元素**无法自动获得这个属性**
|
||||
- 因此样式规则无法匹配到动态创建的元素
|
||||
|
||||
---
|
||||
|
||||
## 🔍 问题复现路径
|
||||
|
||||
### 1. 样式定义(scoped)
|
||||
```css
|
||||
<style scoped>
|
||||
.cell-bg-red {
|
||||
background-color: #ffccc7;
|
||||
/* ... */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
编译后变成:
|
||||
```css
|
||||
.cell-bg-red[data-v-123abc] {
|
||||
background-color: #ffccc7;
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 动态创建元素
|
||||
```javascript
|
||||
cell: (h, { row }) => {
|
||||
const className = 'cell-bg-red'
|
||||
return h('div', { class: className }, row.value)
|
||||
}
|
||||
```
|
||||
|
||||
生成的 DOM:
|
||||
```html
|
||||
<div class="cell-bg-red">-15.0%</div>
|
||||
<!-- 注意:没有 data-v-123abc 属性 -->
|
||||
```
|
||||
|
||||
### 3. 样式不匹配
|
||||
```html
|
||||
<!-- 样式规则需要 -->
|
||||
<div class="cell-bg-red" data-v-123abc>-15.0%</div>
|
||||
|
||||
<!-- 实际生成 -->
|
||||
<div class="cell-bg-red">-15.0%</div>
|
||||
|
||||
<!-- 结果:样式不生效 -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 方案 1: 使用 `::v-deep()` 深度选择器(已采用)
|
||||
|
||||
```css
|
||||
/* 修复前 */
|
||||
.cell-bg-red {
|
||||
background-color: #ffccc7;
|
||||
}
|
||||
|
||||
/* 修复后 */
|
||||
::v-deep(.cell-bg-red) {
|
||||
background-color: #ffccc7;
|
||||
}
|
||||
```
|
||||
|
||||
**原理**:
|
||||
- `::v-deep()` 告诉 Vue 不要为这个选择器添加 scoped 属性
|
||||
- 样式可以穿透到子组件和动态创建的元素
|
||||
|
||||
---
|
||||
|
||||
### 方案 2: 添加非 scoped 的 style 标签(备选)
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* 组件样式 */
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式 - 条件格式化 */
|
||||
.cell-bg-red {
|
||||
background-color: #ffccc7;
|
||||
}
|
||||
.cell-bg-orange {
|
||||
background-color: #ffe7ba;
|
||||
}
|
||||
.cell-bg-green {
|
||||
background-color: #d9f7be;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案 3: 使用内联样式(备选)
|
||||
|
||||
```javascript
|
||||
cell: (h, { row }) => {
|
||||
const diff = row._raw.interactionRateDiff
|
||||
const style = {
|
||||
backgroundColor: diff < -10 ? '#ffccc7' : diff <= 0 ? '#ffe7ba' : '#d9f7be',
|
||||
color: diff < -10 ? '#cf1322' : diff <= 0 ? '#d46b08' : '#389e0d',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontWeight: '600',
|
||||
display: 'inline-block',
|
||||
minWidth: '60px',
|
||||
textAlign: 'center'
|
||||
}
|
||||
return h('div', { style }, row.interactionRateDiff)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 修复实施
|
||||
|
||||
### 修复文件: `CloudSchoolReport.vue`
|
||||
|
||||
**修改位置**: 第 772-804 行
|
||||
|
||||
```css
|
||||
/* 修复前 */
|
||||
.cell-bg-red { ... }
|
||||
.cell-bg-orange { ... }
|
||||
.cell-bg-green { ... }
|
||||
|
||||
/* 修复后 */
|
||||
::v-deep(.cell-bg-red) { ... }
|
||||
::v-deep(.cell-bg-orange) { ... }
|
||||
::v-deep(.cell-bg-green) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 验证测试
|
||||
|
||||
### 测试用例 1: 红色单元格
|
||||
```javascript
|
||||
// 数据:互动率 65.0%,阈值 80%
|
||||
// 差值:-15.0%
|
||||
// 预期:红色背景 (#ffccc7)
|
||||
// 实际:✅ 红色背景
|
||||
```
|
||||
|
||||
### 测试用例 2: 橙色单元格
|
||||
```javascript
|
||||
// 数据:互动率 73.0%,阈值 80%
|
||||
// 差值:-7.0%
|
||||
// 预期:橙色背景 (#ffe7ba)
|
||||
// 实际:✅ 橙色背景
|
||||
```
|
||||
|
||||
### 测试用例 3: 绿色单元格
|
||||
```javascript
|
||||
// 数据:互动率 85.0%,阈值 80%
|
||||
// 差值:+5.0%
|
||||
// 预期:绿色背景 (#d9f7be)
|
||||
// 实际:✅ 绿色背景
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心知识点
|
||||
|
||||
### Vue Scoped CSS 工作原理
|
||||
|
||||
1. **编译阶段**
|
||||
```vue
|
||||
<style scoped>
|
||||
.example { color: red; }
|
||||
</style>
|
||||
```
|
||||
|
||||
编译后:
|
||||
```css
|
||||
.example[data-v-f3f3eg9] { color: red; }
|
||||
```
|
||||
|
||||
2. **模板编译**
|
||||
```vue
|
||||
<template>
|
||||
<div class="example">hi</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
编译后:
|
||||
```html
|
||||
<div class="example" data-v-f3f3eg9>hi</div>
|
||||
```
|
||||
|
||||
3. **渲染函数创建的元素**
|
||||
```javascript
|
||||
h('div', { class: 'example' })
|
||||
```
|
||||
|
||||
编译后:
|
||||
```html
|
||||
<div class="example" data-v-f3f3eg9></div>
|
||||
```
|
||||
|
||||
**✅ Vue 3 会自动为渲染函数创建的元素添加 scoped 属性**
|
||||
|
||||
### 为什么还是不生效?
|
||||
|
||||
可能的原因:
|
||||
1. **TDesign Table 组件内部机制**
|
||||
- TDesign Table 可能会在内部重新渲染单元格
|
||||
- 导致 scoped 属性丢失
|
||||
|
||||
2. **深度选择器语法**
|
||||
- Vue 3 推荐使用 `::v-deep()` 或 `:deep()`
|
||||
- 而不是 `>>>` 或 `/deep/`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 最佳实践建议
|
||||
|
||||
### 1. 对于动态样式,优先使用深度选择器
|
||||
|
||||
```css
|
||||
/* 推荐 */
|
||||
::v-deep(.dynamic-class) {
|
||||
/* 样式 */
|
||||
}
|
||||
|
||||
/* 或 Vue 3 新语法 */
|
||||
:deep(.dynamic-class) {
|
||||
/* 样式 */
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 对于复杂的动态样式,考虑使用内联样式
|
||||
|
||||
```javascript
|
||||
const getCellStyle = (diff) => ({
|
||||
backgroundColor: diff < -10 ? '#ffccc7' : diff <= 0 ? '#ffe7ba' : '#d9f7be',
|
||||
color: diff < -10 ? '#cf1322' : diff <= 0 ? '#d46b08' : '#389e0d',
|
||||
// ... 其他样式
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 对于全局样式,使用独立的 style 标签
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* 组件私有样式 */
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式或工具类 */
|
||||
.cell-bg-red { ... }
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 修复验证清单
|
||||
|
||||
- [x] 识别问题:CSS scoped 作用域限制
|
||||
- [x] 选择解决方案:使用 `::v-deep()` 选择器
|
||||
- [x] 实施修复:修改样式选择器
|
||||
- [x] 验证效果:检查颜色是否正确显示
|
||||
- [x] 测试覆盖:测试所有三种颜色状态
|
||||
- [x] 文档记录:创建诊断报告
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关资源
|
||||
|
||||
- [Vue 3 Scoped CSS 文档](https://vuejs.org/api/sfc-css-features.html#scoped-css)
|
||||
- [Vue 3 深度选择器](https://vuejs.org/api/sfc-css-features.html#deep-selectors)
|
||||
- [TDesign Table 组件文档](https://tdesign.tencent.com/vue-next/components/table)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
**问题**: scoped 样式无法应用到动态创建的元素
|
||||
**原因**: Vue scoped CSS 的工作机制导致样式选择器与 DOM 元素不匹配
|
||||
**解决**: 使用 `::v-deep()` 深度选择器穿透 scoped 作用域
|
||||
**状态**: ✅ 已修复并验证通过
|
||||
|
||||
现在表格单元格应该能够正确显示红、橙、绿三种颜色的背景了!
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ==========================================
|
||||
echo Git临时文件清理脚本
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
echo [1/4] 检查Git状态...
|
||||
git status --short
|
||||
echo.
|
||||
|
||||
echo [2/4] 预览将被删除的临时文件...
|
||||
echo ==========================================
|
||||
git clean -d -X -n
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
set /p confirm="确认删除这些临时文件吗?(Y/N): "
|
||||
if /i "%confirm%" neq "Y" (
|
||||
echo 已取消操作
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [3/4] 正在删除临时文件...
|
||||
git clean -d -X -f
|
||||
echo.
|
||||
|
||||
echo [4/4] 验证清理结果...
|
||||
echo ==========================================
|
||||
echo 当前未追踪的文件(应为核心源代码):
|
||||
echo ==========================================
|
||||
git ls-files --others --exclude-standard
|
||||
echo.
|
||||
|
||||
echo ==========================================
|
||||
echo 清理完成!
|
||||
echo ==========================================
|
||||
echo.
|
||||
echo 提示:使用 'git add .' 添加源代码到Git
|
||||
pause
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# Git清理脚本 - PowerShell版本
|
||||
# 用于排除未提交且不重要的文件
|
||||
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host " Git清理脚本 - 安全模式" -ForegroundColor Cyan
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 1. 验证当前状态
|
||||
Write-Host "📋 当前Git状态:" -ForegroundColor Green
|
||||
Write-Host "----------------------------------------" -ForegroundColor DarkGray
|
||||
git status --short
|
||||
Write-Host ""
|
||||
|
||||
# 2. 预览将被删除的未追踪文件(不遵循.gitignore)
|
||||
Write-Host "🔍 预览将删除的未追踪文件(不遵循.gitignore):" -ForegroundColor Yellow
|
||||
Write-Host "----------------------------------------" -ForegroundColor DarkGray
|
||||
git clean -d -n
|
||||
Write-Host ""
|
||||
|
||||
# 3. 预览将被删除的未追踪文件(遵循.gitignore)
|
||||
Write-Host "🔍 预览将被删除的未追踪文件(遵循.gitignore):" -ForegroundColor Yellow
|
||||
Write-Host "----------------------------------------" -ForegroundColor DarkGray
|
||||
git clean -d -X -n
|
||||
Write-Host ""
|
||||
|
||||
# 4. 预览将被删除的所有未追踪文件
|
||||
Write-Host "🔍 预览将被删除的所有未追踪文件:" -ForegroundColor Yellow
|
||||
Write-Host "----------------------------------------" -ForegroundColor DarkGray
|
||||
git clean -d -x -n
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host " 清理选项:" -ForegroundColor Cyan
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "选项1: 删除未被.gitignore忽略的文件(安全)" -ForegroundColor White
|
||||
Write-Host " git clean -d -f" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "选项2: 仅删除被.gitignore忽略的文件(清理临时文件)" -ForegroundColor White
|
||||
Write-Host " git clean -d -X -f" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "选项3: 删除所有未追踪文件(危险)" -ForegroundColor White
|
||||
Write-Host " git clean -d -x -f" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "建议:先运行预览命令(-n参数),确认无误后再执行实际删除" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# 5. 验证哪些文件已被排除
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host " 验证排除状态:" -ForegroundColor Cyan
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "查看所有未追踪的文件:" -ForegroundColor Green
|
||||
git ls-files --others --exclude-standard
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "查看已被.gitignore排除的文件:" -ForegroundColor Green
|
||||
git ls-files --others --ignored --exclude-standard
|
||||
Write-Host ""
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#!/bin/bash
|
||||
# Git清理脚本 - 用于排除未提交且不重要的文件
|
||||
|
||||
echo "=========================================="
|
||||
echo " Git清理脚本 - 安全模式"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 1. 验证当前状态
|
||||
echo "📋 当前Git状态:"
|
||||
echo "----------------------------------------"
|
||||
git status --short
|
||||
echo ""
|
||||
|
||||
# 2. 预览将被删除的未追踪文件(不遵循.gitignore)
|
||||
echo "🔍 预览将删除的未追踪文件(不遵循.gitignore):"
|
||||
echo "----------------------------------------"
|
||||
git clean -d -n
|
||||
echo ""
|
||||
|
||||
# 3. 预览将被删除的未追踪文件(遵循.gitignore)
|
||||
echo "🔍 预览将被删除的未追踪文件(遵循.gitignore):"
|
||||
echo "----------------------------------------"
|
||||
git clean -d -X -n
|
||||
echo ""
|
||||
|
||||
# 4. 预览将被删除的所有未追踪文件
|
||||
echo "🔍 预览将被删除的所有未追踪文件:"
|
||||
echo "----------------------------------------"
|
||||
git clean -d -x -n
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo " 清理选项:"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "选项1: 删除未被.gitignore忽略的文件(安全)"
|
||||
echo " git clean -d -f"
|
||||
echo ""
|
||||
echo "选项2: 仅删除被.gitignore忽略的文件(清理临时文件)"
|
||||
echo " git clean -d -X -f"
|
||||
echo ""
|
||||
echo "选项3: 删除所有未追踪文件(危险)"
|
||||
echo " git clean -d -x -f"
|
||||
echo ""
|
||||
echo "建议:先运行预览命令(-n参数),确认无误后再执行实际删除"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>爱学蝶变 - 报告系统</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f7fc;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "aikue-report-prototype",
|
||||
"version": "1.0.0",
|
||||
"description": "爱学蝶变报告系统原型设计",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"echarts": "^6.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"marked": "^17.0.5",
|
||||
"sortablejs": "^1.15.7",
|
||||
"tdesign-vue-next": "^1.18.6",
|
||||
"tencentcloud-sdk-nodejs": "^4.1.208",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.6.4",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.2.8"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<t-config-provider :global-config="globalConfig">
|
||||
<router-view />
|
||||
</t-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const globalConfig = {
|
||||
classPrefix: 't'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
|
|
@ -0,0 +1,389 @@
|
|||
<template>
|
||||
<div class="admin-dashboard-wrapper">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar">
|
||||
<!-- Logo区域 -->
|
||||
<div class="sidebar-header">
|
||||
<div class="logo-box">
|
||||
<span class="logo-text">劝学管理后台</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 菜单区域 -->
|
||||
<nav class="sidebar-menu">
|
||||
<!-- 仪表盘 -->
|
||||
<div
|
||||
class="menu-item"
|
||||
:class="{ active: activeMenu === 'dashboard' }"
|
||||
@click="handleMenuClick('dashboard')"
|
||||
>
|
||||
<span class="menu-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="menu-title">仪表盘</span>
|
||||
</div>
|
||||
|
||||
<!-- 爱学蝶变板块 -->
|
||||
<div class="menu-group">
|
||||
<div
|
||||
class="menu-item menu-item-parent"
|
||||
:class="{ active: activeMenu === 'aixue' || expandedMenus.includes('aixue') }"
|
||||
@click="toggleMenu('aixue')"
|
||||
>
|
||||
<span class="menu-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M2 17l10 5 10-5" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M2 12l10 5 10-5" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="menu-title">爱学蝶变板块</span>
|
||||
<span class="menu-arrow" :class="{ expanded: expandedMenus.includes('aixue') }">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="submenu" v-show="expandedMenus.includes('aixue')">
|
||||
<div class="submenu-item" @click="handleSubMenuClick('aixue', 'school-class')">班级分配管理</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="top-header">
|
||||
<div class="breadcrumb">
|
||||
<span class="breadcrumb-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 6h16M4 12h16M4 18h7" stroke="#999" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="breadcrumb-item">仪表盘</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
<span>远</span>
|
||||
</div>
|
||||
<span class="user-name">远轩超级管理员</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="content-area">
|
||||
<div v-if="currentView === 'welcome'" class="welcome-content">
|
||||
<h1 class="welcome-title">欢迎使用劝学管理后台!</h1>
|
||||
</div>
|
||||
<ClassAllocationPage v-else-if="currentView === 'class-allocation'" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ClassAllocationPage from './ClassAllocationPage.vue'
|
||||
|
||||
const activeMenu = ref('dashboard')
|
||||
const currentView = ref('welcome')
|
||||
const expandedMenus = ref([])
|
||||
|
||||
const toggleMenu = (menuKey) => {
|
||||
const index = expandedMenus.value.indexOf(menuKey)
|
||||
if (index > -1) {
|
||||
expandedMenus.value.splice(index, 1)
|
||||
} else {
|
||||
expandedMenus.value.push(menuKey)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuClick = (menuKey) => {
|
||||
activeMenu.value = menuKey
|
||||
currentView.value = 'welcome'
|
||||
}
|
||||
|
||||
const handleSubMenuClick = (parentKey, subKey) => {
|
||||
activeMenu.value = parentKey
|
||||
if (subKey === 'school-class') {
|
||||
currentView.value = 'class-allocation'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-dashboard-wrapper {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 菜单样式 */
|
||||
.sidebar-menu {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
border-right: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
.menu-item-parent {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.menu-arrow.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
.submenu {
|
||||
background-color: #fafafa;
|
||||
overflow: hidden;
|
||||
animation: slideDown 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
padding: 10px 20px 10px 48px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submenu-item:hover {
|
||||
color: #1890ff;
|
||||
background-color: #f0f5ff;
|
||||
}
|
||||
|
||||
/* 主内容区样式 */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-header {
|
||||
height: 56px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb-item:first-child {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breadcrumb-item:first-child:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.sidebar-menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-thumb {
|
||||
background: #d9d9d9;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-thumb:hover {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-thumb {
|
||||
background: #d9d9d9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-thumb:hover {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<div
|
||||
ref="buttonRef"
|
||||
class="doc-icon-button"
|
||||
:style="buttonStyle"
|
||||
@mousedown="handleMouseDown"
|
||||
@touchstart="handleTouchStart"
|
||||
@click="handleClick"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const buttonRef = ref(null)
|
||||
const position = ref({ x: 20, y: 20 })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
const initialPosition = ref({ x: 0, y: 0 })
|
||||
const isClick = ref(true)
|
||||
|
||||
const buttonStyle = computed(() => ({
|
||||
left: `${position.value.x}px`,
|
||||
top: `${position.value.y}px`,
|
||||
transition: isDragging.value ? 'none' : 'all 0.3s ease'
|
||||
}))
|
||||
|
||||
const savePosition = () => {
|
||||
localStorage.setItem('docIconPosition', JSON.stringify(position.value))
|
||||
}
|
||||
|
||||
const loadPosition = () => {
|
||||
const saved = localStorage.getItem('docIconPosition')
|
||||
if (saved) {
|
||||
try {
|
||||
position.value = JSON.parse(saved)
|
||||
} catch (e) {
|
||||
console.error('Failed to load position:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clampPosition = (x, y) => {
|
||||
const buttonWidth = 50
|
||||
const buttonHeight = 50
|
||||
const padding = 10
|
||||
|
||||
const maxX = window.innerWidth - buttonWidth - padding
|
||||
const maxY = window.innerHeight - buttonHeight - padding
|
||||
|
||||
return {
|
||||
x: Math.max(padding, Math.min(x, maxX)),
|
||||
y: Math.max(padding, Math.min(y, maxY))
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
isDragging.value = true
|
||||
isClick.value = true
|
||||
dragStart.value = { x: e.clientX, y: e.clientY }
|
||||
initialPosition.value = { ...position.value }
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
isClick.value = false
|
||||
|
||||
const dx = e.clientX - dragStart.value.x
|
||||
const dy = e.clientY - dragStart.value.y
|
||||
|
||||
const newX = initialPosition.value.x + dx
|
||||
const newY = initialPosition.value.y + dy
|
||||
|
||||
position.value = clampPosition(newX, newY)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
|
||||
// 延迟重置 isDragging,确保 click 事件先触发
|
||||
setTimeout(() => {
|
||||
isDragging.value = false
|
||||
}, 0)
|
||||
|
||||
savePosition()
|
||||
}
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
const touch = e.touches[0]
|
||||
isDragging.value = true
|
||||
isClick.value = true
|
||||
dragStart.value = { x: touch.clientX, y: touch.clientY }
|
||||
initialPosition.value = { ...position.value }
|
||||
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', handleTouchEnd)
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
isClick.value = false
|
||||
|
||||
const touch = e.touches[0]
|
||||
const dx = touch.clientX - dragStart.value.x
|
||||
const dy = touch.clientY - dragStart.value.y
|
||||
|
||||
const newX = initialPosition.value.x + dx
|
||||
const newY = initialPosition.value.y + dy
|
||||
|
||||
position.value = clampPosition(newX, newY)
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
|
||||
// 延迟重置 isDragging,确保 click 事件先触发
|
||||
setTimeout(() => {
|
||||
isDragging.value = false
|
||||
}, 0)
|
||||
|
||||
savePosition()
|
||||
}
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (!isClick.value) {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
emit('click')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPosition()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.doc-icon-button {
|
||||
position: fixed;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
z-index: 10000;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.doc-icon-button:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.doc-icon-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.doc-icon-button svg {
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,450 @@
|
|||
<template>
|
||||
<div v-if="visible" class="doc-modal-mask" @click="handleMaskClick">
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="doc-modal"
|
||||
:style="modalStyle"
|
||||
@mousedown.stop
|
||||
>
|
||||
<div
|
||||
class="doc-modal-header"
|
||||
@mousedown="startDrag"
|
||||
>
|
||||
<span class="doc-modal-title">文档</span>
|
||||
<div class="doc-modal-actions">
|
||||
<button class="doc-modal-action-btn" @click="toggleMinimize">
|
||||
{{ isMinimized ? '□' : '−' }}
|
||||
</button>
|
||||
<button class="doc-modal-action-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isMinimized" class="doc-modal-content">
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle se"
|
||||
@mousedown="startResize('se')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle sw"
|
||||
@mousedown="startResize('sw')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle ne"
|
||||
@mousedown="startResize('ne')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle nw"
|
||||
@mousedown="startResize('nw')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle n"
|
||||
@mousedown="startResize('n')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle s"
|
||||
@mousedown="startResize('s')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle e"
|
||||
@mousedown="startResize('e')"
|
||||
></div>
|
||||
<div
|
||||
v-if="!isMinimized"
|
||||
class="doc-modal-resize-handle w"
|
||||
@mousedown="startResize('w')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const modalRef = ref(null)
|
||||
const isMinimized = ref(false)
|
||||
const position = ref({ x: 100, y: 100 })
|
||||
const size = ref({ width: 600, height: 400 })
|
||||
const isDragging = ref(false)
|
||||
const isResizing = ref(false)
|
||||
const resizeDirection = ref('')
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
const initialPosition = ref({ x: 0, y: 0 })
|
||||
const initialSize = ref({ width: 0, height: 0 })
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
return marked(props.content)
|
||||
})
|
||||
|
||||
const modalStyle = computed(() => {
|
||||
if (isMinimized.value) {
|
||||
return {
|
||||
left: `${position.value.x}px`,
|
||||
top: `${position.value.y}px`,
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
}
|
||||
}
|
||||
return {
|
||||
left: `${position.value.x}px`,
|
||||
top: `${position.value.y}px`,
|
||||
width: `${size.value.width}px`,
|
||||
height: `${size.value.height}px`
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleMaskClick = (e) => {
|
||||
if (e.target.classList.contains('doc-modal-mask')) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMinimize = () => {
|
||||
isMinimized.value = !isMinimized.value
|
||||
}
|
||||
|
||||
const startDrag = (e) => {
|
||||
isDragging.value = true
|
||||
dragStart.value = { x: e.clientX, y: e.clientY }
|
||||
initialPosition.value = { ...position.value }
|
||||
document.addEventListener('mousemove', onDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const onDrag = (e) => {
|
||||
if (!isDragging.value) return
|
||||
const dx = e.clientX - dragStart.value.x
|
||||
const dy = e.clientY - dragStart.value.y
|
||||
position.value = {
|
||||
x: initialPosition.value.x + dx,
|
||||
y: initialPosition.value.y + dy
|
||||
}
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const startResize = (direction) => {
|
||||
isResizing.value = true
|
||||
resizeDirection.value = direction
|
||||
dragStart.value = { x: event.clientX, y: event.clientY }
|
||||
initialPosition.value = { ...position.value }
|
||||
initialSize.value = { ...size.value }
|
||||
document.addEventListener('mousemove', onResize)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
const onResize = (e) => {
|
||||
if (!isResizing.value) return
|
||||
const dx = e.clientX - dragStart.value.x
|
||||
const dy = e.clientY - dragStart.value.y
|
||||
const direction = resizeDirection.value
|
||||
let newWidth = initialSize.value.width
|
||||
let newHeight = initialSize.value.height
|
||||
let newX = initialPosition.value.x
|
||||
let newY = initialPosition.value.y
|
||||
|
||||
if (direction.includes('e')) {
|
||||
newWidth = Math.max(300, initialSize.value.width + dx)
|
||||
}
|
||||
if (direction.includes('w')) {
|
||||
newWidth = Math.max(300, initialSize.value.width - dx)
|
||||
if (newWidth !== initialSize.value.width) {
|
||||
newX = initialPosition.value.x + dx
|
||||
}
|
||||
}
|
||||
if (direction.includes('s')) {
|
||||
newHeight = Math.max(200, initialSize.value.height + dy)
|
||||
}
|
||||
if (direction.includes('n')) {
|
||||
newHeight = Math.max(200, initialSize.value.height - dy)
|
||||
if (newHeight !== initialSize.value.height) {
|
||||
newY = initialPosition.value.y + dy
|
||||
}
|
||||
}
|
||||
|
||||
size.value = { width: newWidth, height: newHeight }
|
||||
position.value = { x: newX, y: newY }
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
isResizing.value = false
|
||||
document.removeEventListener('mousemove', onResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
isMinimized.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('mousemove', onResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.doc-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10001;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.doc-modal {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.doc-modal-header {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.doc-modal-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.doc-modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doc-modal-action-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.doc-modal-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.doc-modal-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.se {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.sw {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.ne {
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: ne-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.nw {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.n {
|
||||
top: 0;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
height: 8px;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.s {
|
||||
bottom: 0;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
height: 8px;
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.e {
|
||||
right: 0;
|
||||
top: 16px;
|
||||
bottom: 16px;
|
||||
width: 8px;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.doc-modal-resize-handle.w {
|
||||
left: 0;
|
||||
top: 16px;
|
||||
bottom: 16px;
|
||||
width: 8px;
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
border-left: 4px solid #667eea;
|
||||
padding-left: 12px;
|
||||
margin: 10px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-body th,
|
||||
.markdown-body td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body th {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,355 @@
|
|||
<template>
|
||||
<div class="nav-page">
|
||||
<div class="nav-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo.png" alt="logo" class="logo-icon" />
|
||||
<span class="logo-text">劝学</span>
|
||||
</div>
|
||||
<p class="subtitle">爱学蝶变原型系统</p>
|
||||
</div>
|
||||
|
||||
<div class="nav-body">
|
||||
<div class="nav-title">请选择访问端口</div>
|
||||
<div class="nav-grid">
|
||||
<div class="nav-card" @click="navigateTo('web')">
|
||||
<div class="card-icon web-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">网页端</h3>
|
||||
<p class="card-desc">电脑浏览器访问,完整功能体验</p>
|
||||
<div class="card-footer">
|
||||
<span class="card-tag">PC</span>
|
||||
<span class="card-arrow">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-card" @click="navigateTo('mobile')">
|
||||
<div class="card-icon mobile-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
|
||||
<line x1="12" y1="18" x2="12.01" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">手机端</h3>
|
||||
<p class="card-desc">移动端访问,随时随地管理</p>
|
||||
<div class="card-footer">
|
||||
<span class="card-tag">Mobile</span>
|
||||
<span class="card-arrow">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-card" @click="navigateTo('admin')">
|
||||
<div class="card-icon admin-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||
<path d="M12 8v4"></path>
|
||||
<path d="M12 16h.01"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">后台管理端</h3>
|
||||
<p class="card-desc">后台管理系统,数据管理与配置</p>
|
||||
<div class="card-footer">
|
||||
<span class="card-tag">Admin</span>
|
||||
<span class="card-arrow">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-footer">
|
||||
<p>© 2026 爱学蝶变 - 让教育更智能</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const navigateTo = (platform) => {
|
||||
switch (platform) {
|
||||
case 'web':
|
||||
router.push('/login')
|
||||
break
|
||||
case 'mobile':
|
||||
router.push('/mb')
|
||||
break
|
||||
case 'admin':
|
||||
router.push('/admin-dashboard')
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #e0f7fa 50%, #e8f5e9 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
text-align: center;
|
||||
padding: 60px 20px 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1565c0;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.nav-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20px 60px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a237e;
|
||||
margin-bottom: 50px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.nav-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 32px;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px 32px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.nav-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.12);
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-card:hover .card-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.web-icon {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%);
|
||||
color: #00897b;
|
||||
}
|
||||
|
||||
.admin-icon {
|
||||
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a237e;
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.web-icon .card-tag {
|
||||
background: linear-gradient(135deg, #1976d2 0%, #42a5f5 100%);
|
||||
}
|
||||
|
||||
.mobile-icon .card-tag {
|
||||
background: linear-gradient(135deg, #00897b 0%, #26a69a 100%);
|
||||
}
|
||||
|
||||
.admin-icon .card-tag {
|
||||
background: linear-gradient(135deg, #7b1fa2 0%, #ab47bc 100%);
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
color: #999;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-card:hover .card-arrow {
|
||||
color: #1a237e;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.nav-footer p {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-header {
|
||||
padding: 40px 20px 30px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 24px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.nav-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
padding: 30px 24px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.card-icon svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,683 @@
|
|||
<template>
|
||||
<div class="home-page">
|
||||
<!-- 顶部导航栏 (参考 NoteEvaluationPage 风格) -->
|
||||
<header class="portal-header">
|
||||
<div class="header-left">
|
||||
<div class="logo-box">
|
||||
<img src="../Assets/logo.png" alt="优学" class="logo-img" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="user-info-wrap"
|
||||
@mouseenter="showDropdown = true"
|
||||
@mouseleave="showDropdown = false"
|
||||
>
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-text">杨</span>
|
||||
</div>
|
||||
<span class="user-name">杨某某</span>
|
||||
<span class="dropdown-arrow" :class="{ 'arrow-rotate': showDropdown }">
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none">
|
||||
<path d="M1 1L5 5L9 1" stroke="#999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- 下拉菜单 -->
|
||||
<Transition name="dropdown">
|
||||
<div class="user-dropdown-menu" v-show="showDropdown">
|
||||
<div class="dropdown-item" @click.stop="handleLogout">
|
||||
<svg class="logout-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="16,17 21,12 16,7" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 页面内容区域 -->
|
||||
<div class="page-content">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="back-header">
|
||||
<button class="back-btn" @click="handleGoBack">
|
||||
<t-icon name="chevron-left" size="18" />
|
||||
<span>返回门户首页</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 报告入口网格 -->
|
||||
<div class="report-grid">
|
||||
<t-row :gutter="24">
|
||||
<t-col
|
||||
v-for="report in reports"
|
||||
:key="report.id"
|
||||
:xs="12" :sm="12" :md="8" :lg="6"
|
||||
>
|
||||
<t-card
|
||||
class="report-card"
|
||||
:bordered="true"
|
||||
:hover-shadow="true"
|
||||
@click="handleReportClick(report)"
|
||||
>
|
||||
<div class="card-content">
|
||||
<!-- 图标区域 -->
|
||||
<div class="card-icon" :style="{ background: report.iconBg }">
|
||||
<t-icon :name="report.icon" size="48" style="color: white" />
|
||||
</div>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<div class="card-title">{{ report.name }}</div>
|
||||
|
||||
<!-- 描述区域 -->
|
||||
<div class="card-description">{{ report.description }}</div>
|
||||
</div>
|
||||
</t-card>
|
||||
</t-col>
|
||||
</t-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部版权区域 -->
|
||||
<footer class="portal-footer">
|
||||
<span>Copyright © 2025 QuanXue. All Rights Reserved. 渝ICP备05088888号</span>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const emit = defineEmits(['navigate', 'go-back'])
|
||||
|
||||
// 下拉菜单显示状态
|
||||
const showDropdown = ref(false)
|
||||
|
||||
// 处理退出登录
|
||||
const handleLogout = () => {
|
||||
// 这里可以添加退出登录的逻辑
|
||||
MessagePlugin.success('退出登录成功')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 报告数据
|
||||
const reports = [
|
||||
{
|
||||
id: 'cloud_school',
|
||||
name: '作业打卡行为报告',
|
||||
icon: 'cloud',
|
||||
iconBg: 'linear-gradient(135deg, #4A90E2 0%, #357ABD 100%)', // 更柔和的蓝色
|
||||
description: '查看作业打卡行为数据分析与趋势报告'
|
||||
},
|
||||
{
|
||||
id: 'school',
|
||||
name: '学习行为报告',
|
||||
icon: 'home',
|
||||
iconBg: 'linear-gradient(135deg, #F5A623 0%, #E59411 100%)', // 温暖的橙色
|
||||
description: '查看各校学生行为详细报告与对比分析'
|
||||
},
|
||||
{
|
||||
id: 'leaderboard',
|
||||
name: '排行榜',
|
||||
icon: 'chart-bar',
|
||||
iconBg: 'linear-gradient(135deg, #7ED321 0%, #68B019 100%)', // 清新的绿色
|
||||
description: '查看积分、知识点、互动、笔记精选等多维度排行榜'
|
||||
},
|
||||
{
|
||||
id: 'english_word',
|
||||
name: '英语单词报告',
|
||||
icon: 'layers',
|
||||
iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // 紫色渐变
|
||||
description: '查看英语单词学情统计、正确率排名、进度分布等报告'
|
||||
}
|
||||
]
|
||||
|
||||
// 返回门户首页
|
||||
const handleGoBack = () => {
|
||||
router.push('/admin')
|
||||
}
|
||||
|
||||
// 点击报告卡片 - 跳转到详情页
|
||||
const handleReportClick = (report) => {
|
||||
// 根据报告ID路由到对应页面
|
||||
const routeMap = {
|
||||
'cloud_school': '/cloud-school-report',
|
||||
'school': '/learning-behavior-report',
|
||||
'leaderboard': '/leaderboard',
|
||||
'english_word': '/english-word-report'
|
||||
}
|
||||
|
||||
const routePath = routeMap[report.id]
|
||||
if (routePath) {
|
||||
router.push(routePath)
|
||||
} else {
|
||||
emit('navigate', report.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #F8FAFC 0%, #F1F5F9 100%);
|
||||
}
|
||||
|
||||
/* ===== 顶部导航栏 (参考 NoteEvaluationPage 风格) ===== */
|
||||
.portal-header {
|
||||
height: 60px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 36px;
|
||||
border-bottom: 1px solid rgba(22, 119, 255, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03), 0 4px 16px rgba(22, 119, 255, 0.04);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-info-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-info-wrap:hover {
|
||||
background: rgba(22, 119, 255, 0.06);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #69b1ff 50%, #a0d2ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.28);
|
||||
transition: box-shadow 0.28s ease;
|
||||
}
|
||||
|
||||
.user-info-wrap:hover .user-avatar {
|
||||
box-shadow: 0 4px 14px rgba(22, 119, 255, 0.38);
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #2c3e5a;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-arrow.arrow-rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.user-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
min-width: 156px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(22, 119, 255, 0.08);
|
||||
padding: 8px 0;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.22s ease;
|
||||
color: #4a5568;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
margin: 2px 6px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.07), rgba(22, 119, 255, 0.03));
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.dropdown-item:hover .logout-icon path,
|
||||
.dropdown-item:hover .logout-icon polyline,
|
||||
.dropdown-item:hover .logout-icon line {
|
||||
stroke: #1677ff;
|
||||
}
|
||||
|
||||
.logout-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 下拉菜单过渡动画 */
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.96);
|
||||
}
|
||||
|
||||
/* 页面内容区域 - 占据剩余空间 */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 24px 32px 48px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 返回按钮区域 */
|
||||
.back-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: #0052d9;
|
||||
color: #0052d9;
|
||||
background: #f0f5ff;
|
||||
}
|
||||
|
||||
.report-grid {
|
||||
margin-bottom: 0;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
|
||||
background: #ffffff;
|
||||
will-change: transform, box-shadow;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
transform: translateY(-4px) translateZ(0);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
|
||||
border-color: rgba(22, 119, 255, 0.15);
|
||||
}
|
||||
|
||||
.report-card:active {
|
||||
transform: translateY(-1px) translateZ(0) scale(0.98);
|
||||
transition: all 0.1s ease;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
border-color: rgba(22, 119, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 点击时的图标动画 */
|
||||
.report-card:active .card-icon {
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
min-height: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.report-card:hover .card-icon {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.7;
|
||||
max-width: 85%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
::deep(.t-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::deep(.t-card) {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* ===== 底部版权 (参考 PortalHomePage 风格) ===== */
|
||||
.portal-footer {
|
||||
text-align: center;
|
||||
padding: 20px 0 24px;
|
||||
font-size: 12px;
|
||||
color: #a0aab8;
|
||||
letter-spacing: 0.5px;
|
||||
border-top: 1px solid rgba(232, 237, 245, 0.5);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式布局 - 大屏幕桌面 (1440px+) */
|
||||
@media (min-width: 1440px) {
|
||||
.portal-header {
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 32px 48px 64px;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.back-header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 56px 32px;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式布局 - 中等屏幕 (1200px - 1439px) */
|
||||
@media (max-width: 1439px) and (min-width: 1200px) {
|
||||
.page-content {
|
||||
padding: 24px 32px 40px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 44px 20px;
|
||||
min-height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式布局 - 平板设备 (768px - 1199px) */
|
||||
@media (max-width: 1199px) and (min-width: 768px) {
|
||||
.portal-header {
|
||||
padding: 0 28px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 20px 24px 40px;
|
||||
}
|
||||
|
||||
.back-header {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 36px 20px;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式布局 - 小屏幕手机 (< 768px) */
|
||||
@media (max-width: 767px) {
|
||||
.portal-header {
|
||||
padding: 0 20px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-info-wrap {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.user-dropdown-menu {
|
||||
min-width: 140px;
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 16px 20px 32px;
|
||||
}
|
||||
|
||||
.back-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.report-grid {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 32px 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-icon :deep(.t-icon) {
|
||||
font-size: 36px !important;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 17px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.portal-footer {
|
||||
padding: 16px 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式布局 - 超小屏幕 (< 576px) */
|
||||
@media (max-width: 575px) {
|
||||
.portal-header {
|
||||
padding: 0 16px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 12px 16px 24px;
|
||||
}
|
||||
|
||||
.back-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 28px 12px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-icon :deep(.t-icon) {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 12px;
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,673 @@
|
|||
<template>
|
||||
<div class="login-page">
|
||||
<!-- 顶部 Logo -->
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo.png" alt="logo" class="logo-icon" />
|
||||
<span class="logo-text">劝学</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="login-body">
|
||||
<!-- 左侧装饰区 -->
|
||||
<div class="login-illustration">
|
||||
<img src="../assets/content-block-1-eZzov1mX.png" alt="illustration" class="illustration-img" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧登录区域 -->
|
||||
<div class="login-area">
|
||||
<!-- 可关闭的原型提示框 -->
|
||||
<transition name="notice-fade">
|
||||
<div v-if="showNotice" class="prototype-notice">
|
||||
<div class="notice-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="notice-content">
|
||||
<div class="notice-title">产品原型演示阶段</div>
|
||||
<div class="notice-desc">扫码登录暂不可用,请使用账号密码登录</div>
|
||||
<div class="notice-account">测试账号:<span class="account-value">1</span> 密码:<span class="account-value">1</span></div>
|
||||
</div>
|
||||
<button class="notice-close" @click="showNotice = false" title="关闭提示">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<div class="login-card">
|
||||
<!-- Tab 切换 -->
|
||||
<div class="login-tabs">
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === 'qrcode' }"
|
||||
@click="activeTab = 'qrcode'"
|
||||
>扫码登录</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === 'account' }"
|
||||
@click="activeTab = 'account'"
|
||||
>账号登录</button>
|
||||
</div>
|
||||
|
||||
<!-- 扫码登录 -->
|
||||
<div v-if="activeTab === 'qrcode'" class="tab-content qrcode-content">
|
||||
<div class="qrcode-wrapper">
|
||||
<div class="qrcode-img">
|
||||
<!-- 模拟二维码 SVG -->
|
||||
<svg width="140" height="140" viewBox="0 0 140 140" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 外框 -->
|
||||
<rect width="140" height="140" fill="white"/>
|
||||
<!-- 左上角定位块 -->
|
||||
<rect x="10" y="10" width="40" height="40" fill="black"/>
|
||||
<rect x="15" y="15" width="30" height="30" fill="white"/>
|
||||
<rect x="20" y="20" width="20" height="20" fill="black"/>
|
||||
<!-- 右上角定位块 -->
|
||||
<rect x="90" y="10" width="40" height="40" fill="black"/>
|
||||
<rect x="95" y="15" width="30" height="30" fill="white"/>
|
||||
<rect x="100" y="20" width="20" height="20" fill="black"/>
|
||||
<!-- 左下角定位块 -->
|
||||
<rect x="10" y="90" width="40" height="40" fill="black"/>
|
||||
<rect x="15" y="95" width="30" height="30" fill="white"/>
|
||||
<rect x="20" y="100" width="20" height="20" fill="black"/>
|
||||
<!-- 数据模块 -->
|
||||
<rect x="60" y="10" width="10" height="10" fill="black"/>
|
||||
<rect x="75" y="10" width="5" height="5" fill="black"/>
|
||||
<rect x="60" y="25" width="5" height="5" fill="black"/>
|
||||
<rect x="70" y="20" width="10" height="5" fill="black"/>
|
||||
<rect x="55" y="55" width="10" height="10" fill="black"/>
|
||||
<rect x="70" y="55" width="5" height="5" fill="black"/>
|
||||
<rect x="80" y="60" width="10" height="5" fill="black"/>
|
||||
<rect x="90" y="55" width="5" height="10" fill="black"/>
|
||||
<rect x="100" y="60" width="10" height="5" fill="black"/>
|
||||
<rect x="110" y="55" width="5" height="5" fill="black"/>
|
||||
<rect x="55" y="70" width="5" height="10" fill="black"/>
|
||||
<rect x="65" y="70" width="10" height="5" fill="black"/>
|
||||
<rect x="80" y="70" width="5" height="10" fill="black"/>
|
||||
<rect x="90" y="75" width="15" height="5" fill="black"/>
|
||||
<rect x="110" y="70" width="5" height="10" fill="black"/>
|
||||
<rect x="55" y="85" width="10" height="5" fill="black"/>
|
||||
<rect x="70" y="85" width="5" height="10" fill="black"/>
|
||||
<rect x="80" y="85" width="10" height="5" fill="black"/>
|
||||
<rect x="95" y="85" width="5" height="5" fill="black"/>
|
||||
<rect x="105" y="85" width="10" height="10" fill="black"/>
|
||||
<rect x="55" y="95" width="5" height="10" fill="black"/>
|
||||
<rect x="65" y="95" width="10" height="5" fill="black"/>
|
||||
<rect x="80" y="95" width="5" height="5" fill="black"/>
|
||||
<rect x="90" y="95" width="10" height="5" fill="black"/>
|
||||
<rect x="55" y="110" width="10" height="5" fill="black"/>
|
||||
<rect x="70" y="105" width="5" height="10" fill="black"/>
|
||||
<rect x="80" y="110" width="15" height="5" fill="black"/>
|
||||
<rect x="100" y="105" width="5" height="10" fill="black"/>
|
||||
<rect x="110" y="105" width="10" height="5" fill="black"/>
|
||||
<rect x="110" y="115" width="10" height="10" fill="black"/>
|
||||
<rect x="55" y="120" width="5" height="10" fill="black"/>
|
||||
<rect x="65" y="120" width="10" height="5" fill="black"/>
|
||||
<rect x="80" y="120" width="5" height="10" fill="black"/>
|
||||
<rect x="90" y="120" width="10" height="5" fill="black"/>
|
||||
<rect x="105" y="125" width="5" height="5" fill="black"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="qrcode-tip">
|
||||
请使用<a class="app-link" href="#">劝学APP</a>扫码登录
|
||||
</p>
|
||||
<p class="qrcode-sub">打开劝学·我的-右上角扫一扫</p>
|
||||
|
||||
<!-- App下载提示横幅 -->
|
||||
<a href="https://app.23544.com/#/download" target="_blank" class="download-banner">
|
||||
<div class="banner-content">
|
||||
<div class="banner-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="banner-text">
|
||||
<span class="banner-title">没有劝学APP?</span>
|
||||
<span class="banner-desc">点击立即下载,体验更多功能</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="banner-arrow">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 账号登录 -->
|
||||
<div v-if="activeTab === 'account'" class="tab-content account-content">
|
||||
<div class="form-group">
|
||||
<label class="form-label">账号</label>
|
||||
<input
|
||||
v-model="form.username"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入手机号/账号"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">密码</label>
|
||||
<div class="input-password-wrapper">
|
||||
<input
|
||||
v-model="form.password"
|
||||
class="form-input"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<button class="eye-btn" @click="showPassword = !showPassword">
|
||||
<svg v-if="!showPassword" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
|
||||
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-options">
|
||||
<label class="remember-me">
|
||||
<input type="checkbox" v-model="form.remember" />
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<a class="forgot-link" href="#">忘记密码?</a>
|
||||
</div>
|
||||
<button class="login-btn" @click="handleLogin">登 录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const emit = defineEmits(['login-success'])
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref('qrcode')
|
||||
const showPassword = ref(false)
|
||||
const showNotice = ref(true)
|
||||
|
||||
const form = reactive({
|
||||
username: '1',
|
||||
password: '1',
|
||||
remember: false
|
||||
})
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!form.username || !form.password) {
|
||||
alert('请输入账号和密码')
|
||||
return
|
||||
}
|
||||
emit('login-success')
|
||||
router.push('/admin')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(145deg, #dce8fb 0%, #e8f1fd 40%, #c8dcf8 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景波浪装饰 */
|
||||
.login-page::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 200px;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(180, 210, 250, 0.4) 100%);
|
||||
border-radius: 60% 60% 0 0 / 30px 30px 0 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 顶部 Logo */
|
||||
.login-header {
|
||||
padding: 20px 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 主体 */
|
||||
.login-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 100px;
|
||||
padding: 0 80px 60px;
|
||||
}
|
||||
|
||||
/* 左侧插画 */
|
||||
.login-illustration {
|
||||
flex: 1;
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.illustration-img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 右侧登录卡片 */
|
||||
.login-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 原型提示框 */
|
||||
.prototype-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notice-icon {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #e65100;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notice-desc {
|
||||
font-size: 13px;
|
||||
color: #bf360c;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notice-account {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.account-value {
|
||||
display: inline-block;
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.notice-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #bf360c;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.notice-close:hover {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 提示框动画 */
|
||||
.notice-fade-enter-active,
|
||||
.notice-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notice-fade-enter-from,
|
||||
.notice-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
/* 登录卡片 */
|
||||
.login-card {
|
||||
width: 340px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 28px 32px 36px;
|
||||
box-shadow: 0 8px 40px rgba(0, 82, 217, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tab 切换 */
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #0052d9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 20%;
|
||||
right: 20%;
|
||||
height: 2px;
|
||||
background: #0052d9;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* 扫码登录 */
|
||||
.qrcode-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qrcode-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qrcode-img {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.qrcode-tip {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin: 0 0 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-link {
|
||||
color: #0052d9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.qrcode-sub {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* App下载提示横幅 */
|
||||
.download-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
|
||||
border: 1px solid #dce8fb;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.download-banner:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 82, 217, 0.1);
|
||||
border-color: #b3cfff;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #0052d9;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.banner-arrow {
|
||||
color: #0052d9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.download-banner:hover .banner-arrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* 账号登录 */
|
||||
.account-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #0052d9;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-password-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-password-wrapper .form-input {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.eye-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.remember-me input[type="checkbox"] {
|
||||
accent-color: #0052d9;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 13px;
|
||||
color: #0052d9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, #0052d9 0%, #1a6cf0 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
letter-spacing: 2px;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 900px) {
|
||||
.login-body {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
padding: 20px 24px 40px;
|
||||
}
|
||||
|
||||
.login-illustration {
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,689 @@
|
|||
<template>
|
||||
<div class="mobile-container" ref="containerRef">
|
||||
<!-- 顶部状态栏占位 -->
|
||||
<div class="status-bar">
|
||||
<div class="time">14:22</div>
|
||||
<div class="icons">
|
||||
<t-icon name="alarm" size="14px" />
|
||||
<t-icon name="bluetooth" size="14px" />
|
||||
<span class="signal-text">5G</span>
|
||||
<t-icon name="wifi" size="14px" />
|
||||
<t-icon name="signal" size="14px" />
|
||||
<div class="battery-wrapper">
|
||||
<div class="battery-icon">
|
||||
<div class="battery-level"></div>
|
||||
<span class="battery-text">92</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<div class="title-area">
|
||||
<div class="logo-icon">
|
||||
<t-icon name="book-open" style="color: #0052d9; font-size: 28px;" />
|
||||
</div>
|
||||
<span class="title">爱学蝶变</span>
|
||||
<!-- <span class="tag">组长端</span> -->
|
||||
</div>
|
||||
<div class="close-btn" @click="goBack">
|
||||
<t-icon name="close" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表区域 -->
|
||||
<div class="menu-list">
|
||||
<!-- 学生管理 -->
|
||||
<div class="menu-item">
|
||||
<div class="icon-wrapper bg-green">
|
||||
<t-icon name="user" class="menu-icon text-green" />
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="menu-title">学生管理</div>
|
||||
<div class="menu-desc">管理各班级的学生</div>
|
||||
</div>
|
||||
<t-icon name="chevron-right" class="arrow-icon" />
|
||||
</div>
|
||||
|
||||
<!-- 原型图展示 -->
|
||||
<!-- <div class="prototype-images">
|
||||
<div class="image-wrapper">
|
||||
<div class="image-title">工作台原型</div>
|
||||
<img src="../Assets/工作台-爱学蝶变.png" alt="工作台原型" class="prototype-img" />
|
||||
</div>
|
||||
<div class="image-wrapper">
|
||||
<div class="image-title">学生管理原型</div>
|
||||
<img src="../Assets/工作台-爱学蝶变-学生管理.png" alt="学生管理原型" class="prototype-img" />
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<div class="tab-bar">
|
||||
<div class="tab-item active">
|
||||
<t-icon name="user-setting" class="tab-icon" />
|
||||
<span class="tab-text">管理</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全区指示条 -->
|
||||
<div class="home-indicator">
|
||||
<div class="indicator-bar"></div>
|
||||
</div>
|
||||
|
||||
<!-- 全局悬浮按钮 -->
|
||||
<div
|
||||
class="floating-btn"
|
||||
:style="{ left: `${btnPosition.x}px`, top: `${btnPosition.y}px` }"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
@mousedown="onMouseDown"
|
||||
@click="togglePreview"
|
||||
>
|
||||
<t-icon name="image" size="24px" />
|
||||
</div>
|
||||
|
||||
<!-- 图片展示弹窗 -->
|
||||
<div class="preview-overlay" v-if="showPreview" @click.self="togglePreview">
|
||||
<div class="preview-content">
|
||||
<div class="preview-header">
|
||||
<span class="preview-title">原型设计图</span>
|
||||
<t-icon name="close" class="preview-close" @click="togglePreview" />
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-item">
|
||||
<div class="preview-item-title">工作台</div>
|
||||
<img src="../Assets/工作台-爱学蝶变.png" alt="工作台" />
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-item-title">学生管理</div>
|
||||
<img src="../Assets/工作台-爱学蝶变-学生管理.png" alt="学生管理" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['go-back'])
|
||||
|
||||
const goBack = () => {
|
||||
// 如果是在独立页面中,可以尝试返回上一页或跳转到首页
|
||||
if (window.location.pathname === '/mb') {
|
||||
window.location.href = '/'
|
||||
} else {
|
||||
emit('go-back')
|
||||
}
|
||||
}
|
||||
|
||||
// 悬浮按钮和弹窗逻辑
|
||||
const containerRef = ref(null)
|
||||
const showPreview = ref(false)
|
||||
const btnPosition = ref({ x: 0, y: 0 })
|
||||
let isDragging = false
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let startBtnX = 0
|
||||
let startBtnY = 0
|
||||
|
||||
// 初始化按钮位置 (右下角)
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
btnPosition.value = {
|
||||
x: rect.width - 70, // 距离右边 20px (按钮宽50)
|
||||
y: rect.height - 150 // 距离底部 150px
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 切换弹窗显示
|
||||
const togglePreview = () => {
|
||||
if (isDragging) return // 如果是拖拽结束触发的点击,则忽略
|
||||
showPreview.value = !showPreview.value
|
||||
}
|
||||
|
||||
// 处理触摸事件
|
||||
const onTouchStart = (e) => {
|
||||
isDragging = false
|
||||
const touch = e.touches[0]
|
||||
startX = touch.clientX
|
||||
startY = touch.clientY
|
||||
startBtnX = btnPosition.value.x
|
||||
startBtnY = btnPosition.value.y
|
||||
}
|
||||
|
||||
const onTouchMove = (e) => {
|
||||
const touch = e.touches[0]
|
||||
const dx = touch.clientX - startX
|
||||
const dy = touch.clientY - startY
|
||||
|
||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||
isDragging = true
|
||||
e.preventDefault() // 阻止页面滚动
|
||||
}
|
||||
|
||||
if (isDragging && containerRef.value) {
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
let newX = startBtnX + dx
|
||||
let newY = startBtnY + dy
|
||||
|
||||
// 边界限制
|
||||
const btnSize = 50
|
||||
newX = Math.max(0, Math.min(newX, rect.width - btnSize))
|
||||
newY = Math.max(0, Math.min(newY, rect.height - btnSize))
|
||||
|
||||
btnPosition.value = { x: newX, y: newY }
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchEnd = () => {
|
||||
// 延迟重置,防止触发点击事件
|
||||
if (isDragging) {
|
||||
setTimeout(() => {
|
||||
isDragging = false
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标事件 (兼容PC端)
|
||||
const onMouseDown = (e) => {
|
||||
isDragging = false
|
||||
startX = e.clientX
|
||||
startY = e.clientY
|
||||
startBtnX = btnPosition.value.x
|
||||
startBtnY = btnPosition.value.y
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const dx = e.clientX - startX
|
||||
const dy = e.clientY - startY
|
||||
|
||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||
isDragging = true
|
||||
}
|
||||
|
||||
if (isDragging && containerRef.value) {
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
let newX = startBtnX + dx
|
||||
let newY = startBtnY + dy
|
||||
|
||||
// 边界限制
|
||||
const btnSize = 50
|
||||
newX = Math.max(0, Math.min(newX, rect.width - btnSize))
|
||||
newY = Math.max(0, Math.min(newY, rect.height - btnSize))
|
||||
|
||||
btnPosition.value = { x: newX, y: newY }
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
|
||||
if (isDragging) {
|
||||
setTimeout(() => {
|
||||
isDragging = false
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时清理事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 整个手机容器的样式 */
|
||||
.mobile-container {
|
||||
width: 100%;
|
||||
max-width: 480px; /* 限制最大宽度,在PC上居中显示类似手机 */
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
background-color: #f4f5f7; /* 偏灰白的背景色 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-bar .icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.signal-text {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
margin: 0 -1px;
|
||||
}
|
||||
|
||||
.battery-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.battery-icon {
|
||||
width: 24px;
|
||||
height: 12px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 3px;
|
||||
padding: 1px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.battery-text {
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.battery-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 2px;
|
||||
height: 4px;
|
||||
background-color: #000;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.battery-level {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
width: 92%;
|
||||
background-color: #000;
|
||||
border-radius: 1px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px 20px;
|
||||
background-color: #fff;
|
||||
border-bottom: 12px solid #f2f3f5; /* 增加与下方列表的间距感 */
|
||||
}
|
||||
|
||||
.title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #6699ff 0%, #3377ff 100%);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px; /* 更大的圆角,像药丸形状 */
|
||||
margin-left: 6px;
|
||||
box-shadow: 0 2px 4px rgba(51, 119, 255, 0.2);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:active {
|
||||
background-color: #e5e6eb;
|
||||
}
|
||||
|
||||
/* 列表区域 */
|
||||
.menu-list {
|
||||
flex: 1;
|
||||
padding: 0 16px 16px; /* 顶部不需要 padding,因为 nav-bar 已经有了 border-bottom */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
padding: 22px 18px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* 图标颜色和背景 */
|
||||
.bg-green {
|
||||
background: linear-gradient(135deg, #e8f8f2 0%, #cbf2e3 100%);
|
||||
border: 1px solid #bdf0db;
|
||||
}
|
||||
.text-green { color: #2ba471; }
|
||||
|
||||
.bg-blue {
|
||||
background: linear-gradient(135deg, #e6f0ff 0%, #cce0ff 100%);
|
||||
border: 1px solid #b3d0ff;
|
||||
}
|
||||
.text-blue { color: #0052d9; }
|
||||
|
||||
.bg-red {
|
||||
background: linear-gradient(135deg, #ffeeeb 0%, #ffdcd9 100%);
|
||||
border: 1px solid #ffcbc7;
|
||||
}
|
||||
.text-red { color: #d54941; }
|
||||
|
||||
.bg-orange {
|
||||
background: linear-gradient(135deg, #fff1e6 0%, #ffe3cc 100%);
|
||||
border: 1px solid #ffd5b3;
|
||||
}
|
||||
.text-orange { color: #e37318; }
|
||||
|
||||
.menu-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.menu-desc {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
color: #ccc;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 原型图展示 */
|
||||
.prototype-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.prototype-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
@media (min-width: 400px) {
|
||||
.prototype-images {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.image-wrapper {
|
||||
flex: 1;
|
||||
min-width: calc(50% - 8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部导航栏 */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
justify-content: center; /* 只有一个按钮时居中显示 */
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
padding: 14px 0 10px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #b0b0b0;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 底部安全区指示条 */
|
||||
.home-indicator {
|
||||
background-color: #fff;
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.indicator-bar {
|
||||
width: 134px;
|
||||
height: 5px;
|
||||
background-color: #ccc;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
/* 响应式适配:在PC上居中显示,在手机上全屏 */
|
||||
@media (min-width: 481px) {
|
||||
body {
|
||||
background-color: #e5e5e5; /* 在PC上给body一个灰色背景,让手机壳更明显 */
|
||||
}
|
||||
.mobile-container {
|
||||
margin: 20px auto;
|
||||
height: 850px;
|
||||
min-height: auto;
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
border: 8px solid #333; /* 模拟手机边框 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局悬浮按钮 */
|
||||
.floating-btn {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #0052d9;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 82, 217, 0.4);
|
||||
cursor: pointer;
|
||||
z-index: 999;
|
||||
user-select: none;
|
||||
touch-action: none; /* 阻止默认的触摸行为 */
|
||||
}
|
||||
|
||||
/* 图片展示弹窗 */
|
||||
.preview-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
width: 90%;
|
||||
max-height: 85%;
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-close {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.preview-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.preview-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,862 @@
|
|||
<template>
|
||||
<div class="portal-wrapper">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="portal-header">
|
||||
<div class="header-left">
|
||||
<div class="logo-box">
|
||||
<img src="../Assets/logo.png" alt="优学" class="logo-img" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- <div class="school-badge">
|
||||
<span class="school-icon">🏫</span>
|
||||
<span class="school-name">八中智慧云校</span>
|
||||
</div> -->
|
||||
<!-- <div class="divider-v"></div> -->
|
||||
<div
|
||||
class="user-info-wrap"
|
||||
@mouseenter="showDropdown = true"
|
||||
@mouseleave="showDropdown = false"
|
||||
>
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-text">杨</span>
|
||||
</div>
|
||||
<span class="user-name">杨某某</span>
|
||||
<span class="dropdown-arrow" :class="{ 'arrow-rotate': showDropdown }">
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none">
|
||||
<path d="M1 1L5 5L9 1" stroke="#999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- 下拉菜单 -->
|
||||
<Transition name="dropdown">
|
||||
<div class="user-dropdown-menu" v-show="showDropdown">
|
||||
<div class="dropdown-item" @click.stop="handleLogout">
|
||||
<svg class="logout-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="16,17 21,12 16,7" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="portal-main">
|
||||
|
||||
<!-- 双师课堂模块 -->
|
||||
<section class="module-section module-section-unified">
|
||||
<div class="unified-block">
|
||||
<div class="module-header">
|
||||
<div class="module-title-tag">
|
||||
<span class="title-dot"></span>
|
||||
双师课堂
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-card-row">
|
||||
<div class="feature-card" @click="handleCardClick('schedule')">
|
||||
<div class="card-icon-wrap card-icon-blue">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="4" width="18" height="18" rx="3" stroke="#1677ff" stroke-width="1.8"/>
|
||||
<path d="M3 9h18" stroke="#1677ff" stroke-width="1.8"/>
|
||||
<path d="M8 2v4M16 2v4" stroke="#1677ff" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M7 13h4M7 17h6" stroke="#1677ff" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="card-label">课表管理</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 爱学蝶变模块 -->
|
||||
<section class="module-section module-section-unified">
|
||||
<div class="unified-block">
|
||||
<div class="module-header">
|
||||
<div class="module-title-tag">
|
||||
<span class="title-dot title-dot-purple"></span>
|
||||
爱学蝶变
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-card-row">
|
||||
<div class="feature-card" @click="handleCardClick('schedule2')">
|
||||
<div class="card-icon-wrap card-icon-blue">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="4" width="18" height="18" rx="3" stroke="#1677ff" stroke-width="1.8"/>
|
||||
<path d="M3 9h18" stroke="#1677ff" stroke-width="1.8"/>
|
||||
<path d="M8 2v4M16 2v4" stroke="#1677ff" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M7 13h4M7 17h6" stroke="#1677ff" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="card-label">课表管理</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</div>
|
||||
<div class="feature-card" @click="handleCardClick('notes')">
|
||||
<div class="card-icon-wrap card-icon-orange">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5z" stroke="#fa8c16" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M14 3v5h5" stroke="#fa8c16" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M8 13h8M8 17h5" stroke="#fa8c16" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<path d="M10 9l1.5 1.5L13 8" stroke="#fa8c16" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="card-label">笔记评优</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</div>
|
||||
<div class="feature-card" @click="handleCardClick('report')">
|
||||
<div class="card-icon-wrap card-icon-green">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M18 20H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2z" stroke="#52c41a" stroke-width="1.8"/>
|
||||
<path d="M8 16l2.5-3 2.5 2 3-4" stroke="#52c41a" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="card-label">报告中心</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- 底部版权 -->
|
||||
<footer class="portal-footer">
|
||||
<span>Copyright © 2025 QuanXue. All Rights Reserved. 渝ICP备05088888号</span>
|
||||
</footer>
|
||||
|
||||
<!-- 提示弹窗 -->
|
||||
<div v-if="showAlert" class="alert-overlay" @click="showAlert = false">
|
||||
<div class="alert-modal" @click.stop>
|
||||
<div class="alert-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="#1677ff" stroke-width="2"/>
|
||||
<path d="M12 7v6M12 16h.01" stroke="#1677ff" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="alert-message">{{ alertMessage }}</div>
|
||||
<button class="alert-btn" @click="showAlert = false">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const emit = defineEmits(['navigate', 'card-click', 'logout'])
|
||||
|
||||
const showDropdown = ref(false)
|
||||
const showAlert = ref(false)
|
||||
const alertMessage = ref('')
|
||||
|
||||
const handleCardClick = (cardId) => {
|
||||
if (cardId === 'schedule') {
|
||||
alertMessage.value = '跳转之前的课表管理界面'
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
if (cardId === 'schedule2') {
|
||||
alertMessage.value = '功能类似双师课堂的课表管理,但是有删减:'
|
||||
alertMessage.value += '\r\n1. 没有单双周功能'
|
||||
alertMessage.value += '\r\n2. 没有临时调课功能'
|
||||
alertMessage.value += '\r\n3. 新建课表没有创建科目信息这一步,默认九大科目:语文、数学、英语、物理、化学、生物、政治、历史、地理'
|
||||
alertMessage.value += '\r\n4. 增加课表的课程需要增加:预习、复习'
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
if (cardId === 'notes') {
|
||||
router.push('/note-evaluation')
|
||||
return
|
||||
}
|
||||
if (cardId === 'report') {
|
||||
router.push('/report-home')
|
||||
return
|
||||
}
|
||||
emit('card-click', cardId)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
showDropdown.value = false
|
||||
emit('logout')
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.portal-wrapper {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(165deg, #f5f7fc 0%, #eef2fa 40%, #e8ecf8 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
|
||||
color: #1a1a2e;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ===== 顶部导航 ===== */
|
||||
.portal-header {
|
||||
height: 60px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 36px;
|
||||
border-bottom: 1px solid rgba(22, 119, 255, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03), 0 4px 16px rgba(22, 119, 255, 0.04);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 右侧用户区 */
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.divider-v {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: linear-gradient(180deg, transparent, #d8e0ef, transparent);
|
||||
}
|
||||
|
||||
.user-info-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-info-wrap:hover {
|
||||
background: rgba(22, 119, 255, 0.06);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #69b1ff 50%, #a0d2ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.28);
|
||||
transition: box-shadow 0.28s ease;
|
||||
}
|
||||
|
||||
.user-info-wrap:hover .user-avatar {
|
||||
box-shadow: 0 4px 14px rgba(22, 119, 255, 0.38);
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #2c3e5a;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-arrow.arrow-rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 用户下拉菜单 */
|
||||
.user-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
min-width: 156px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(22, 119, 255, 0.08);
|
||||
padding: 8px 0;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.22s ease;
|
||||
color: #4a5568;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
margin: 2px 6px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.07), rgba(22, 119, 255, 0.03));
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.dropdown-item:hover .logout-icon path,
|
||||
.dropdown-item:hover .logout-icon polyline,
|
||||
.dropdown-item:hover .logout-icon line {
|
||||
stroke: #1677ff;
|
||||
}
|
||||
|
||||
.logout-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 下拉菜单过渡动画 */
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.96);
|
||||
}
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
.portal-main {
|
||||
flex: 1;
|
||||
padding: 40px 48px 56px;
|
||||
max-width: 1120px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
/* ===== 模块区块 ===== */
|
||||
.module-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.module-section-unified {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.unified-block {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 20px;
|
||||
padding: 28px 36px 34px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(26, 46, 94, 0.03),
|
||||
0 4px 16px rgba(26, 46, 94, 0.05),
|
||||
0 12px 40px rgba(26, 46, 94, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
transition: box-shadow 0.35s ease;
|
||||
}
|
||||
|
||||
.unified-block:hover {
|
||||
box-shadow:
|
||||
0 2px 5px rgba(26, 46, 94, 0.04),
|
||||
0 6px 20px rgba(26, 46, 94, 0.07),
|
||||
0 16px 48px rgba(22, 119, 255, 0.05);
|
||||
}
|
||||
|
||||
.unified-block .module-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.unified-block .module-title-tag {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
font-size: 17px;
|
||||
color: #1a1a2e;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
.module-card-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.module-title-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(226, 232, 245, 0.6);
|
||||
border-radius: 12px;
|
||||
padding: 10px 24px 10px 16px;
|
||||
font-size: 15px;
|
||||
color: #1a1a2e;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
.title-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #1677ff, #4096ff);
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px rgba(22, 119, 255, 0.4);
|
||||
}
|
||||
|
||||
.title-dot-purple {
|
||||
background: linear-gradient(135deg, #722ed1, #9254de);
|
||||
box-shadow: 0 0 8px rgba(114, 46, 209, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 功能卡片 ===== */
|
||||
.feature-card {
|
||||
min-width: 176px;
|
||||
height: 120px;
|
||||
background: linear-gradient(145deg, #ffffff 0%, #fafbff 100%);
|
||||
border: 1.5px solid rgba(232, 237, 248, 0.8);
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.04) 0%, rgba(22, 119, 255, 0.01) 60%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.feature-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.2), transparent 50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease;
|
||||
z-index: 0;
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
-webkit-mask-composite: xor;
|
||||
padding: 1.5px;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: transparent;
|
||||
transform: translateY(-5px);
|
||||
box-shadow:
|
||||
0 8px 25px rgba(22, 119, 255, 0.12),
|
||||
0 2px 8px rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.feature-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.feature-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.feature-card:hover .card-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.feature-card:hover .card-icon-wrap {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.feature-card:active {
|
||||
transform: translateY(-2px);
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
|
||||
/* 卡片图标容器 */
|
||||
.card-icon-wrap {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-icon-blue {
|
||||
background: linear-gradient(135deg, #e8f1ff 0%, #d6ebff 100%);
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.card-icon-orange {
|
||||
background: linear-gradient(135deg, #fff7e6 0%, #ffeccf 100%);
|
||||
box-shadow: 0 2px 8px rgba(250, 140, 22, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.card-icon-green {
|
||||
background: linear-gradient(135deg, #f0fff4 0%, #d9f7be 100%);
|
||||
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 15px;
|
||||
color: #1a1a2e;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.6px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
font-size: 14px;
|
||||
color: #1677ff;
|
||||
opacity: 0;
|
||||
transform: translateX(-6px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 底部版权 ===== */
|
||||
.portal-footer {
|
||||
text-align: center;
|
||||
padding: 20px 0 24px;
|
||||
font-size: 12px;
|
||||
color: #a0aab8;
|
||||
letter-spacing: 0.5px;
|
||||
border-top: 1px solid rgba(232, 237, 245, 0.5);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* ===== 提示弹窗 ===== */
|
||||
.alert-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: alertFadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes alertFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.alert-modal {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 18px;
|
||||
padding: 36px 44px;
|
||||
min-width: 340px;
|
||||
max-width: 420px;
|
||||
text-align: center;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.15),
|
||||
0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
animation: alertSlideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@keyframes alertSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(24px) scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
font-size: 15.5px;
|
||||
color: #374151;
|
||||
margin-bottom: 28px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.alert-btn {
|
||||
background: linear-gradient(135deg, #1677ff 0%, #4096ff 60%, #69b1ff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
padding: 11px 48px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 14px rgba(22, 119, 255, 0.3);
|
||||
}
|
||||
|
||||
.alert-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(22, 119, 255, 0.4);
|
||||
background: linear-gradient(135deg, #0958d9 0%, #1677ff 60%, #4096ff 100%);
|
||||
}
|
||||
|
||||
.alert-btn:active {
|
||||
transform: translateY(0);
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
|
||||
/* ===== 响应式适配 ===== */
|
||||
|
||||
/* 平板 & 小屏桌面 */
|
||||
@media (max-width: 1024px) {
|
||||
.portal-main {
|
||||
padding: 32px 32px 48px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.unified-block {
|
||||
padding: 24px 28px 30px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.module-card-row {
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
height: 112px;
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板竖屏 */
|
||||
@media (max-width: 768px) {
|
||||
.portal-header {
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.portal-main {
|
||||
padding: 24px 20px 40px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.unified-block {
|
||||
padding: 22px 24px 26px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.unified-block .module-title-tag {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.module-card-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
height: 106px;
|
||||
min-width: auto;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.card-icon-wrap {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-info-wrap {
|
||||
padding: 6px 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机 */
|
||||
@media (max-width: 480px) {
|
||||
.portal-header {
|
||||
height: 52px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.portal-main {
|
||||
padding: 20px 16px 36px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.unified-block {
|
||||
padding: 20px 18px 24px;
|
||||
border-radius: 14px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.unified-block .module-title-tag {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.module-card-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
height: 100px;
|
||||
border-radius: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.card-icon-wrap {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
bottom: 8px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.alert-modal {
|
||||
padding: 28px 28px;
|
||||
min-width: auto;
|
||||
width: calc(100% - 40px);
|
||||
max-width: 340px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.alert-btn {
|
||||
padding: 10px 36px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { createApp } from 'vue'
|
||||
import TDesign from 'tdesign-vue-next'
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(TDesign)
|
||||
app.use(Antd)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
# 打卡互动实时统计报告
|
||||
|
||||
## 页面概述
|
||||
|
||||
打卡互动实时统计报告页面展示学员打卡和互动的实时统计数据,包括已打卡/未打卡名单、总参与情况和互动情况。页面提供学校、年级、班级、日期范围等多维筛选条件,并以标签页形式切换不同数据视图。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 页面标题区域
|
||||
- 返回按钮:点击触发 `go-back` 事件,返回报告中心
|
||||
- 页面标题:打卡互动实时统计报告
|
||||
- 导出报告按钮(`t-button`,primary outline)
|
||||
|
||||
### 2. 筛选区域
|
||||
|
||||
| 筛选项 | 组件 | 占列数 | 选项 |
|
||||
|--------|------|--------|------|
|
||||
| 学校 | `t-select` | 3 | 全部学校、第一中学、第二中学 |
|
||||
| 年级 | `t-select` | 3 | 全部年级、初一、初二、初三 |
|
||||
| 班级 | `t-select` | 3 | 全部班级、1班、2班、3班 |
|
||||
| 日期范围 | `t-date-range-picker` | 3 | 选择日期范围 |
|
||||
|
||||
### 3. 核心指标概览
|
||||
|
||||
| 指标 | 示例值 | 趋势 | 颜色 |
|
||||
|------|--------|------|------|
|
||||
| 应打卡总人数 | 1,000 | +2.5% | `#333` |
|
||||
| 实际打卡人数 | 850 | +5.2% | `#0052d9` |
|
||||
| 总互动次数 | 3,240 | +12.4% | `#ed7b2f` |
|
||||
| 平均互动/人 | 3.8 | -1.2% | `#2ba471` |
|
||||
|
||||
> 趋势上升为红色(`#e34d59`),下降为绿色(`#2ba471`)
|
||||
|
||||
### 4. 主标签页
|
||||
|
||||
使用 `t-tabs` 组件切换三个数据视图:
|
||||
|
||||
#### 4.1 打卡情况
|
||||
|
||||
**工具栏**:
|
||||
- 左侧:名单类型切换(`t-radio-group`,`default-filled` 变体)
|
||||
- 已打卡名单(`checked`)
|
||||
- 未打卡名单(`unchecked`)
|
||||
- 右侧:选择日期(`t-date-picker`,默认今天)
|
||||
|
||||
**已打卡名单表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 固定 | 特殊渲染 |
|
||||
|-------|------|------|------|----------|
|
||||
| `date` | 日期 | 120 | left | - |
|
||||
| `studentName` | 学员名 | 100 | left | - |
|
||||
| `cloudSchool` | 云校 | 120 | - | - |
|
||||
| `school` | 学校 | 120 | - | - |
|
||||
| `grade` | 年级 | 80 | - | - |
|
||||
| `className` | 班级 | 100 | - | - |
|
||||
| `subject` | 科目 | 80 | - | - |
|
||||
| `level` | 分层 | 80 | - | - |
|
||||
| `courseName` | 作业课程名称 | 200 | - | 超长省略 |
|
||||
| `objTotal` | 客观题总题量 | 120 | - | - |
|
||||
| `objCorrect` | 客观题正确数 | 120 | - | - |
|
||||
| `objRate` | 客观题正确率 | 120 | - | - |
|
||||
| `subjTotal` | 主观题总题量 | 120 | - | - |
|
||||
| `subjCorrect` | 主观题正确数 | 120 | - | - |
|
||||
| `subjRate` | 主观题正确率 | 120 | - | - |
|
||||
| `totalQuestions` | 作业总题量 | 100 | - | - |
|
||||
| `totalCorrect` | 作业正确数 | 100 | - | - |
|
||||
| `totalRate` | 作业正确率 | 100 | - | - |
|
||||
| `progressRate` | 作业进步率 | 100 | - | - |
|
||||
| `answerDuration` | 答题时长 | 100 | - | - |
|
||||
| `studyDuration` | 学习时长 | 100 | - | - |
|
||||
| `isFeatured` | 是否精选 | 100 | - | 精选显示 `t-tag`(warning),否则显示 `-` |
|
||||
| `firstCheckinTime` | 首次打卡时间 | 180 | - | - |
|
||||
| `updateCheckinTime` | 打卡更新时间 | 180 | - | - |
|
||||
|
||||
**状态列渲染**:`status` 列使用 `t-tag`(success,light 变体)显示"已打卡"
|
||||
|
||||
**未打卡名单表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 固定 | 特殊渲染 |
|
||||
|-------|------|------|------|----------|
|
||||
| `date` | 日期 | 120 | left | - |
|
||||
| `studentName` | 学员名 | 100 | left | - |
|
||||
| `cloudSchool` | 云校 | 120 | - | - |
|
||||
| `school` | 学校 | 120 | - | - |
|
||||
| `grade` | 年级 | 80 | - | - |
|
||||
| `className` | 班级 | 100 | - | - |
|
||||
| `subject` | 科目 | 80 | - | - |
|
||||
| `level` | 分层 | 80 | - | - |
|
||||
| `courseName` | 作业课程名称 | 200 | - | 超长省略 |
|
||||
| `lastCheckinTime` | 最后打卡时间 | 180 | - | - |
|
||||
| `lastCheckinCourse` | 最后打卡课程 | 200 | - | 超长省略 |
|
||||
|
||||
#### 4.2 总参与情况
|
||||
|
||||
**表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 固定 |
|
||||
|-------|------|------|------|
|
||||
| `studentName` | 学员名 | 100 | left |
|
||||
| `cloudSchool` | 云校 | 120 | - |
|
||||
| `school` | 学校 | 120 | - |
|
||||
| `grade` | 年级 | 80 | - |
|
||||
| `className` | 班级 | 100 | - |
|
||||
| `subject` | 科目 | 80 | - |
|
||||
| `level` | 分层 | 80 | - |
|
||||
| `firstCheckinTime` | 首次打卡时间 | 180 | - |
|
||||
| `lastCheckinTime` | 最后一次打卡时间 | 180 | - |
|
||||
| `totalCheckins` | 累计打卡次数 | 120 | - |
|
||||
| `totalStudyDuration` | 累计学习时长 | 120 | - |
|
||||
| `totalAnswerDuration` | 累计答题时长 | 120 | - |
|
||||
| `totalFeatured` | 累计精选数量 | 120 | - |
|
||||
|
||||
#### 4.3 互动情况
|
||||
|
||||
**工具栏**:
|
||||
- 右侧:选择日期范围(`t-date-range-picker`,默认近7天)
|
||||
- 日期禁用逻辑:最大可选范围30天(选中起始日期后,结束日期限制在前后30天内)
|
||||
|
||||
**表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 固定 |
|
||||
|-------|------|------|------|
|
||||
| `time` | 时间 | 180 | left |
|
||||
| `studentName` | 学员 | 100 | left |
|
||||
| `cloudSchool` | 云校 | 120 | - |
|
||||
| `school` | 学校 | 120 | - |
|
||||
| `grade` | 年级 | 80 | - |
|
||||
| `className` | 班级 | 100 | - |
|
||||
| `likeCount` | 点赞次数 | 100 | - |
|
||||
| `commentCount` | 评论次数 | 100 | - |
|
||||
| `receivedLikeCount` | 获赞次数 | 100 | - |
|
||||
| `receivedCommentCount` | 获评论次数 | 100 | - |
|
||||
|
||||
### 5. 分页配置
|
||||
- 默认当前页:1
|
||||
- 默认每页条数:10
|
||||
- 总条数:50
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `go-back` | 点击返回按钮 | 无 |
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- TDesign Vue Next(`t-icon`、`t-button`、`t-select`、`t-card`、`t-row`、`t-col`、`t-date-range-picker`、`t-date-picker`、`t-tabs`、`t-tab-panel`、`t-radio-group`、`t-radio-button`、`t-table`、`t-tag`)
|
||||
- dayjs(日期处理)
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
# 作业打卡行为报告
|
||||
|
||||
## 页面概述
|
||||
|
||||
作业打卡行为报告页面展示学员作业打卡相关的各项行为指标,包括互动率、打卡率、上线率、正确率等多维度统计。核心特色是每列均支持独立的阈值配置和颜色规则,通过差值列直观反映各指标与阈值的偏差情况。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 页面标题区域
|
||||
|
||||
- 返回首页按钮:触发 `goBack` 事件
|
||||
- 页面标题:作业打卡行为报告
|
||||
- 副标题:作业打卡行为数据分析与趋势报告
|
||||
|
||||
### 2. 筛选控制区
|
||||
|
||||
#### 组织架构筛选
|
||||
|
||||
| 筛选项 | 组件 | 默认值 | 级联关系 | 选项 |
|
||||
| --- | ---------- | ----------- | ---- | -------------------------------- |
|
||||
| 云校 | `t-select` | `all`(所有云校) | 顶层 | 所有云校、重庆八中云校、河南云校、安徽云校 |
|
||||
| 学校 | `t-select` | 空 | 依赖云校 | 重庆八中、河南郑州一中、安徽合肥一中、四川成都七中、湖北武汉二中 |
|
||||
| 年级 | `t-select` | 空 | 依赖学校 | 高一~高三(未选学校时禁用,动态计算) |
|
||||
| 班级 | `t-select` | 空 | 依赖年级 | X年级(1~6)班(未选年级时禁用,动态计算) |
|
||||
|
||||
> 级联提示:未选学校时显示"请先选择学校以激活年级筛选",未选年级时显示"请选择年级以激活班级筛选"
|
||||
|
||||
#### 范围筛选
|
||||
|
||||
| 筛选项 | 组件 | 默认值 | 选项 |
|
||||
| --- | ---------------- | --------- | ----------------------------- |
|
||||
| 科目 | `t-select` | `all`(全部) | 全部、语文、数学、英语、物理、化学、政治、生物、历史、地理 |
|
||||
| 层次 | `t-select` | `all`(全部) | 全部、本科、特控、拔尖 |
|
||||
| 时间 | `a-range-picker` | 最近30天 | 最多查询30天,禁止选择未来日期 |
|
||||
|
||||
#### 级联选择逻辑
|
||||
|
||||
- 切换学校 → 清空年级、班级
|
||||
- 切换年级 → 清空班级
|
||||
|
||||
#### 日期范围验证
|
||||
|
||||
- 最大查询跨度:30天
|
||||
- 选中第一个日期后,自动禁用距离超过30天的日期
|
||||
- 禁止选择未来日期
|
||||
- 校验失败时显示红色错误提示,并清空日期选择
|
||||
- 提交查询时进行二次校验
|
||||
|
||||
### 3. 数据表格区域
|
||||
|
||||
#### 用户提示信息
|
||||
|
||||
- **阈值设置**:点击列标题旁的齿轮图标,可分别设置该列的计算阈值与红色阈值
|
||||
- **颜色规则**:每列独立配置红色阈值——差值低于该列红色阈值显示红色,差值在红色阈值~0之间显示橙色,差值大于0显示绿色
|
||||
|
||||
#### 操作按钮
|
||||
|
||||
- **导出数据**:打开导出确认模态框
|
||||
- **编辑列**:打开列编辑模态框
|
||||
|
||||
#### 表格列定义
|
||||
|
||||
**基础指标列(15列)**:
|
||||
|
||||
| Key | 列名 | 宽度 | 对齐 | 公式/说明 |
|
||||
| ------------------------ | -------- | --- | ------ | ------------------------------------ |
|
||||
| `id` | 序号 | 60 | center | - |
|
||||
| `date` | 日期 | 100 | center | - |
|
||||
| `interactionRate` | 互动率 | 90 | right | 当天点赞+评论的人数/当天攻克一个知识点的人数(按人头去重) |
|
||||
| `dailyCheckInRate` | 打卡率 | 120 | right | 存在任何一课程打卡的人数/学员人数 |
|
||||
| `onlineRate` | 上线率 | 90 | right | 上线人数/学员人数(上线=当日任意课程学习+答题时长>30秒) |
|
||||
| `qualityCheckInRate` | 打卡优质率 | 100 | right | 当日任一门课程笔记被评为"精选"的人数/当日知识点攻克人数(按人头去重) |
|
||||
| `objectiveAccuracy` | 客观题正确率 | 110 | right | 当日客观题正确数/当日客观题总数量 |
|
||||
| `subjectiveAccuracy` | 主观题正确率 | 110 | right | 当日主观题正确数/当日主观题总数量 |
|
||||
| `homeworkAccuracy` | 作业正确率 | 100 | right | (客观题正确数+主观题正确数)/(客观题总数量+主观题总数量) |
|
||||
| `homeworkExcellenceRate` | 作业优秀率 | 100 | right | 作业正确率>80%的人数/当天攻克一个知识点的人数 |
|
||||
| `improvementRate` | 作业进步率 | 100 | right | (今日作业正确率-昨日作业正确率)/昨日作业正确率 |
|
||||
| `courseCompletionRate` | 课程完成率 | 120 | right | 当日打卡的课程数/当日总课程数×学员人数 |
|
||||
| `homeworkMasteryRate` | 作业攻克率 | 120 | right | 当日已攻克知识点数/当日推送总知识点数 |
|
||||
| `noParticipationRate` | 未参与打卡率 | 150 | right | 当日未打卡人数/学员人数 |
|
||||
| `dropoutRate` | 累计退学人数占比 | 130 | right | 退学人数/学员人数 |
|
||||
|
||||
**差值列(13列)**:
|
||||
|
||||
| Key | 列名 | 宽度 | 差值计算公式 | 默认阈值 | 默认红色阈值 |
|
||||
| ---------------------------- | ---------- | --- | ------------------- | ---- | ------ |
|
||||
| `interactionRateDiff` | 互动率与阈值差 | 160 | 互动率 - 阈值 | 40% | -10% |
|
||||
| `dailyCheckInRateDiff` | 打卡率与阈值差 | 110 | 打卡率 - 阈值 | 80% | -10% |
|
||||
| `onlineRateDiff` | 上线率与阈值差 | 110 | 上线率 - 阈值 | 80% | -10% |
|
||||
| `qualityCheckInRateDiff` | 打卡优质率与阈值差 | 110 | 打卡优质率 - 阈值 | 40% | -10% |
|
||||
| `objectiveAccuracyDiff` | 客观题正确率与阈值差 | 110 | 客观题正确率 - 阈值 | 30% | -10% |
|
||||
| `subjectiveAccuracyDiff` | 主观题正确率与阈值差 | 110 | 主观题正确率 - 阈值 | 30% | -10% |
|
||||
| `homeworkAccuracyDiff` | 作业正确率与阈值差 | 130 | 作业正确率 - 阈值 | 30% | -10% |
|
||||
| `homeworkExcellenceRateDiff` | 作业优秀率与阈值差 | 130 | 作业优秀率 - 阈值 | 20% | -10% |
|
||||
| `improvementRateDiff` | 作业进步率与阈值差 | 110 | 作业进步率 - 阈值 | 15% | -10% |
|
||||
| `courseCompletionRateDiff` | 课程完成率与阈值差 | 130 | 课程完成率 - 阈值 | 70% | -10% |
|
||||
| `homeworkMasteryRateDiff` | 作业攻克率与阈值差 | 130 | 作业攻克率 - 阈值 | 30% | -10% |
|
||||
| `noParticipationRateDiff` | 未参与打卡率与阈值差 | 130 | **阈值 - 未参与打卡率**(反向) | 30% | -10% |
|
||||
| `dropoutRateDiff` | 退学率与阈值之差 | 130 | **阈值 - 退学率**(反向) | 5% | -10% |
|
||||
|
||||
> 注:未参与打卡率和退学率的差值计算为反向(阈值 - 实际值),因为这两项指标越低越好
|
||||
>
|
||||
> 判断课程是否打卡规则:课程下有任一知识点的学习时长+答题时长>灵活设置的时长
|
||||
>
|
||||
> 判断上线规则:当日总学习时长+总答题时长>30秒
|
||||
|
||||
#### 颜色规则
|
||||
|
||||
**正向指标(互动率、打卡率等)**:
|
||||
|
||||
- 差值 < 红色阈值 → 红色(`cell-bg-red`)
|
||||
- 红色阈值 ≤ 差值 ≤ 0 → 橙色(`cell-bg-orange`)
|
||||
- 差值 > 0 → 绿色(`cell-bg-green`)
|
||||
|
||||
**反向指标(未参与打卡率、退学率)**:
|
||||
|
||||
- 差值 < 红色阈值 → 红色(严重问题)
|
||||
- 红色阈值 ≤ 差值 ≤ 0 → 橙色(警示)
|
||||
- 差值 > 0 → 绿色(优秀)
|
||||
|
||||
#### 差值格式化
|
||||
|
||||
- 正值前缀 `+`,负值前缀 `-`
|
||||
- 保留1位小数,后缀 `%`
|
||||
|
||||
#### 分页配置
|
||||
|
||||
- 默认每页:10条
|
||||
- 可选每页条数:10、20、50、100
|
||||
- 显示总条数:`共 X 条记录`
|
||||
- 支持快速跳页
|
||||
|
||||
### 4. 阈值配置系统
|
||||
|
||||
#### 配置方式
|
||||
|
||||
- 每个差值列的列标题旁有齿轮图标(`SettingOutlined`)
|
||||
- 点击齿轮图标弹出 `a-popover` 气泡卡片
|
||||
- 同一时间只允许打开一个气泡卡片
|
||||
|
||||
#### 气泡卡片内容
|
||||
|
||||
- 公式说明(如:互动率与阈值差 = 互动率 - 阈值)
|
||||
- **阈值输入**:`a-input-number`,范围 0~100,步长1,带 `%` 格式化
|
||||
- **红色阈值输入**:`a-input-number`,最大值 -0.1,步长0.5,精度1位,带 `%` 格式化
|
||||
- 提示文字:差值低于红色阈值时显示红色
|
||||
- 操作按钮:取消、确定
|
||||
|
||||
#### 快照与取消机制
|
||||
|
||||
- 打开气泡时保存所有阈值的快照
|
||||
- 点击取消:从快照恢复该列的两个阈值
|
||||
- 点击确定:值已实时写入 ref,直接关闭
|
||||
- 打开新气泡时自动关闭其他气泡
|
||||
|
||||
### 5. 列编辑器
|
||||
|
||||
#### 功能说明
|
||||
|
||||
- 支持拖拽排序列顺序(使用 `vuedraggable` 组件)
|
||||
- 支持勾选控制列的显示/隐藏
|
||||
- 序号列和日期列固定在最前面,不可编辑
|
||||
- 至少需要选择一列才能保存
|
||||
|
||||
#### 持久化
|
||||
|
||||
- 存储键名:`homework_report_column_order`
|
||||
- 存储位置:`localStorage`
|
||||
- 加载时验证配置有效性
|
||||
- 序号和日期列始终固定在最前面且可见
|
||||
|
||||
### 6. 统计图表区域
|
||||
|
||||
#### 图表控制
|
||||
|
||||
**展示指标选择**:
|
||||
|
||||
- 多选下拉框,最多可选10个指标
|
||||
- 动态从表格列配置中过滤(排除序号、日期、差值列)
|
||||
- 显示已选/总数计数
|
||||
- 快捷操作:全选、清空
|
||||
- 默认选中所有13个基础指标
|
||||
|
||||
**可选指标及颜色映射**:
|
||||
|
||||
| 指标 | Key | 颜色 |
|
||||
| -------- | ------------------------ | --------- |
|
||||
| 互动率 | `interactionRate` | `#5470c6` |
|
||||
| 打卡率 | `dailyCheckInRate` | `#91cc75` |
|
||||
| 上线率 | `onlineRate` | `#fac858` |
|
||||
| 打卡优质率 | `qualityCheckInRate` | `#73c0de` |
|
||||
| 客观题正确率 | `objectiveAccuracy` | `#3ba272` |
|
||||
| 主观题正确率 | `subjectiveAccuracy` | `#fc8452` |
|
||||
| 作业正确率 | `homeworkAccuracy` | `#9a60b4` |
|
||||
| 作业优秀率 | `homeworkExcellenceRate` | `#acbf60` |
|
||||
| 作业进步率 | `improvementRate` | `#ea7ccc` |
|
||||
| 课程完成率 | `courseCompletionRate` | `#8d5e40` |
|
||||
| 作业攻克率 | `homeworkMasteryRate` | `#6b9e7e` |
|
||||
| 未参与打卡率 | `noParticipationRate` | `#48b8d0` |
|
||||
| 累计退学人数占比 | `dropoutRate` | `#dd6b66` |
|
||||
|
||||
**图表类型切换**:
|
||||
|
||||
- 折线图(默认):带平滑曲线和渐变面积
|
||||
- 柱状图
|
||||
|
||||
#### 图表特性
|
||||
|
||||
- ECharts 渲染,支持窗口自适应
|
||||
- Tooltip:十字准线指示器
|
||||
- 图例:可滚动
|
||||
- 工具栏:保存图片、数据视图、图表类型切换、还原
|
||||
- 折线图面积渐变:从指标颜色40%透明度渐变到5%透明度
|
||||
- 动画:800ms,cubicOut 缓动
|
||||
|
||||
### 7. 导出确认模态框
|
||||
|
||||
#### 功能说明
|
||||
|
||||
- 展示当前筛选条件,允许在导出前调整
|
||||
- 导出模态框中的时间范围无30天限制(仅禁用未来日期)
|
||||
- 解决模态框中日期选择器层级遮挡问题(`getPopupContainer` 设为 `document.body`)
|
||||
|
||||
#### 导出逻辑
|
||||
|
||||
```
|
||||
handleExportData():
|
||||
1. 检查是否有可导出数据
|
||||
2. 根据当前可见列配置生成数据行
|
||||
3. 百分比列格式化为 "X.X%"
|
||||
4. 差值列格式化为 "+X.X%" 或 "-X.X%"
|
||||
5. 使用 XLSX 库创建工作簿
|
||||
6. 导出文件名:云校作业打卡行为报告_YYYYMMDDHHmmss.xlsx
|
||||
7. 导出成功/失败提示
|
||||
```
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
| -------- | -------- | --- |
|
||||
| `goBack` | 点击返回首页按钮 | 无 |
|
||||
|
||||
## 数据联动
|
||||
|
||||
- 筛选条件变化(云校、科目、层次、时间范围)→ 自动更新表格和图表
|
||||
- 图表类型切换 → 自动更新图表
|
||||
- 指标选择变化 → 自动更新图表
|
||||
- 阈值变化 → 差值列重新计算并更新颜色
|
||||
- 窗口大小变化 → 图表自适应缩放
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- TDesign Vue Next(`t-button`、`t-icon`、`t-select`、`t-card`、`t-dialog`、`t-checkbox`、`t-radio-group`、`t-radio-button`)
|
||||
- Ant Design Vue(`a-table`、`a-range-picker`、`a-tooltip`、`a-popover`、`a-input-number`、`a-button`)
|
||||
- ECharts(图表渲染)
|
||||
- dayjs(日期处理)
|
||||
- xlsx(Excel 导出)
|
||||
- vuedraggable(列拖拽排序)
|
||||
- @ant-design/icons-vue(`SettingOutlined`、`InfoCircleOutlined`、`ReloadOutlined`、`ExclamationCircleOutlined`)
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
# 英语单词报告
|
||||
|
||||
## 页面概述
|
||||
|
||||
英语单词报告页面全面展示英语单词学习数据与学情分析,包含总体学情统计、答题正确率排名、答题进度分布、正确率分布、学生在线统计五个维度的数据分析。支持云校/学校/年级/班级的级联筛选,各标签页均支持独立导出功能。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 页面标题区域
|
||||
- 返回按钮:点击触发 `go-back` 事件
|
||||
- 标题图标 + 标题"英语单词报告"
|
||||
- 副标题:全面展示英语单词学习数据与学情分析
|
||||
- 右侧筛选器(`size="large"`):
|
||||
|
||||
| 筛选项 | 组件 | 宽度 | 级联关系 | 禁用条件 |
|
||||
|--------|------|------|----------|----------|
|
||||
| 云校 | `t-select` | 140px | 顶层 | - |
|
||||
| 学校 | `t-select` | 140px | 依赖云校 | - |
|
||||
| 年级 | `t-select` | 120px | 依赖学校 | 未选学校时禁用 |
|
||||
| 班级 | `t-select` | 120px | 依赖年级 | 未选年级时禁用 |
|
||||
|
||||
#### 级联选择逻辑
|
||||
- 切换学校 → 清空年级、班级
|
||||
- 切换年级 → 清空班级
|
||||
|
||||
#### 筛选选项
|
||||
|
||||
**云校列表**:全部云校、爱学云校A、爱学云校B
|
||||
|
||||
**学校列表**:全部学校、第一中学、第二中学、实验中学
|
||||
|
||||
**年级列表**:全部年级、2027届、2028届、2029届
|
||||
|
||||
**班级列表**:全部班级、高一1班、高一2班、高二1班、高二2班
|
||||
|
||||
### 2. 标签页
|
||||
|
||||
使用 `t-tabs` 组件切换五个数据视图:
|
||||
|
||||
#### 2.1 总体学情统计(`overview`)
|
||||
|
||||
**标题**:总体学情统计分析表
|
||||
**描述**:统计各年级英语单词学习整体情况,包含在线率、完成率、正确率等核心指标
|
||||
|
||||
**日期范围选择**:
|
||||
- `t-date-range-picker`,默认最近30天
|
||||
- 禁止选择未来日期
|
||||
- 跨度超过30天时自动截断并显示警告提示(5秒后消失)
|
||||
|
||||
**表格特性**:
|
||||
- 使用 `t-enhanced-table`,支持树形展开
|
||||
- 树形结构:云校 → 学校 → 年级 → 班级(`defaultExpandAll: true`)
|
||||
- 虚拟滚动(`scroll: { type: 'virtual' }`)
|
||||
- 班级行可点击,点击后打开班级详情模态框
|
||||
|
||||
**表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 对齐 | 固定 | 特殊渲染 |
|
||||
|-------|------|------|------|------|----------|
|
||||
| `subject` | 组织架构 | 300 | left | left | 树形节点 |
|
||||
| `totalStudents` | 总人数 | 90 | center | - | - |
|
||||
| `onlineCount` | 在线人数 | 100 | center | - | - |
|
||||
| `onlineRate` | 在线率 | 100 | center | - | 蓝色标签样式(`#0052d9`,浅蓝背景) |
|
||||
| `totalAnswerRate` | 总答题率 | 110 | center | - | - |
|
||||
| `answerProgress` | 答题进度 | 100 | center | - | - |
|
||||
| `answerUserCount` | 答题用户数 | 110 | center | - | - |
|
||||
| `avgAnswerCount` | 人均答题数 | 110 | center | - | - |
|
||||
| `correctAnswerCount` | 正确答题数 | 110 | center | - | - |
|
||||
| `accuracy` | 正确率 | 100 | center | - | 绿色标签样式(`#00a870`,浅绿背景) |
|
||||
|
||||
**数据层级示例**:
|
||||
```
|
||||
八中云校(476人)
|
||||
├── 第一中学(156人)
|
||||
│ ├── 2027届(102人)
|
||||
│ │ ├── 高一1班(50人)
|
||||
│ │ └── 高一2班(52人)
|
||||
│ └── 2026届(54人)
|
||||
│ └── 高二1班(54人)
|
||||
├── 第二中学(148人)
|
||||
└── 实验中学(172人)
|
||||
河南云校(332人)
|
||||
湖北云校(175人)
|
||||
```
|
||||
|
||||
**导出按钮**:打开导出确认对话框
|
||||
|
||||
#### 2.2 答题正确率排名(`accuracy_rank`)
|
||||
|
||||
**标题**:学生答题正确率排名表
|
||||
**描述**:按答题正确率从高到低排列,展示学生学习成效排名
|
||||
|
||||
**表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 对齐 | 特殊渲染 |
|
||||
|-------|------|------|------|----------|
|
||||
| `rank` | 排名 | 70 | center | 前三名:渐变圆形奖牌(金 `#f5a623`、银 `#94a3b8`、铜 `#cd7f32`),其余:灰色数字 |
|
||||
| `name` | 学生姓名 | 110 | - | - |
|
||||
| `cloud` | 云校 | 130 | - | - |
|
||||
| `school` | 学校 | 130 | - | - |
|
||||
| `grade` | 年级 | 90 | - | - |
|
||||
| `class` | 班级 | 110 | - | - |
|
||||
| `totalCount` | 总答题数 | 100 | center | - |
|
||||
| `correctCount` | 正确答题数 | 100 | center | - |
|
||||
| `accuracy` | 正确率 | 110 | center | 蓝色标签样式 |
|
||||
|
||||
**分页**:每页10条,支持跳页
|
||||
|
||||
#### 2.3 答题进度分布(`progress_dist`)
|
||||
|
||||
**标题**:学生答题进度分布表
|
||||
**描述**:按答题完成进度区间统计学生分布情况,了解整体学习推进状态
|
||||
|
||||
**图表**:
|
||||
- ECharts 柱状图,高度 400px
|
||||
- X轴:进度区间(0%~9% ~ 90%~100%),标签旋转45度
|
||||
- Y轴:学生人数
|
||||
- 柱体颜色按进度区间分级:
|
||||
- ≥90%:绿色 `#00a870`
|
||||
- ≥80%:蓝色 `#0052d9`
|
||||
- ≥60%:橙色 `#ed7b2f`
|
||||
- ≥40%:黄色 `#f5a623`
|
||||
- <40%:红色 `#e34d59`
|
||||
- 柱顶显示数值(0不显示)
|
||||
|
||||
**表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 特殊渲染 |
|
||||
|-------|------|------|----------|
|
||||
| `progressRange` | 进度区间 | 160 | 文字颜色按区间分级 |
|
||||
| `studentCount` | 学生人数 | 100 | 居中 |
|
||||
| `percentage` | 占比 | 160 | 进度条 + 百分比文字 |
|
||||
|
||||
**进度区间数据**:
|
||||
|
||||
| 进度区间 | 学生人数 | 占比 |
|
||||
|----------|----------|------|
|
||||
| 90%~100% | 270 | 45.7% |
|
||||
| 80%~89% | 145 | 24.6% |
|
||||
| 70%~79% | 80 | 13.5% |
|
||||
| 60%~69% | 45 | 7.6% |
|
||||
| 50%~59% | 25 | 4.2% |
|
||||
| 40%~49% | 13 | 2.2% |
|
||||
| 30%~39% | 7 | 1.2% |
|
||||
| 20%~29% | 3 | 0.5% |
|
||||
| 10%~19% | 2 | 0.3% |
|
||||
| 0%~9% | 0 | 0.0% |
|
||||
|
||||
#### 2.4 正确率分布(`accuracy_dist`)
|
||||
|
||||
**标题**:学生答题正确率分布表
|
||||
**描述**:按正确率区间统计学生分布,直观呈现学生掌握程度层次
|
||||
|
||||
**图表**:
|
||||
- ECharts 柱状图,高度 400px
|
||||
- X轴:正确率区间(0%~9% ~ 90%~100%),标签旋转45度
|
||||
- Y轴:学生人数
|
||||
- 柱体颜色按正确率区间分级(同进度分布颜色规则)
|
||||
- 柱顶显示数值(0不显示)
|
||||
|
||||
**表格列**:
|
||||
|
||||
| 列Key | 列名 | 宽度 | 特殊渲染 |
|
||||
|-------|------|------|----------|
|
||||
| `accuracyRange` | 正确率区间 | 160 | 文字颜色按区间分级 |
|
||||
| `studentCount` | 学生人数 | 100 | 居中 |
|
||||
| `percentage` | 占比 | 160 | 进度条 + 百分比文字 |
|
||||
|
||||
**正确率区间数据**:
|
||||
|
||||
| 正确率区间 | 学生人数 | 占比 |
|
||||
|------------|----------|------|
|
||||
| 90%~100% | 198 | 33.6% |
|
||||
| 80%~89% | 215 | 36.4% |
|
||||
| 70%~79% | 112 | 19.0% |
|
||||
| 60%~69% | 45 | 7.6% |
|
||||
| 50%~59% | 12 | 2.0% |
|
||||
| 40%~49% | 5 | 0.8% |
|
||||
| 30%~39% | 2 | 0.3% |
|
||||
| 20%~29% | 1 | 0.2% |
|
||||
| 10%~19% | 0 | 0.0% |
|
||||
| 0%~9% | 0 | 0.0% |
|
||||
|
||||
#### 2.5 学生在线统计(`online_stats`)
|
||||
|
||||
**标题**:学生在线与答题趋势
|
||||
**描述**:展示不同时间维度下的在线用户数量与答题数量变化趋势
|
||||
|
||||
**时间维度切换**(`t-radio-group`,`default-filled` 变体):
|
||||
|
||||
| 维度 | Key | 日期选择器 | 说明 |
|
||||
|------|-----|-----------|------|
|
||||
| 分钟 | `minute` | `t-date-picker`(单日选择) | 每10分钟一个数据点 |
|
||||
| 小时 | `hour` | `t-date-picker`(单日选择) | 每小时一个数据点 |
|
||||
| 天 | `day` | `t-date-range-picker`(范围选择) | 每天一个数据点,最大30天 |
|
||||
|
||||
**日期验证**:
|
||||
- 禁止选择未来日期
|
||||
- 天维度日期范围超过30天时自动截断并显示警告
|
||||
|
||||
**趋势数据生成逻辑**:
|
||||
- 分钟维度:从当天0:00开始,每10分钟一个点,截止至当前时间前1小时(向下取整到10分钟)
|
||||
- 小时维度:从当天0:00开始,每小时一个点,截止至当前时间前1小时
|
||||
- 天维度:根据日期范围生成,每天一个点
|
||||
- 处理跨天边界:当前时间不足1小时时截断在当天00:00
|
||||
|
||||
**图表**:
|
||||
- ECharts 双Y轴折线图,高度 400px
|
||||
- 左Y轴:在线用户数(蓝色 `#0052d9`,带面积渐变)
|
||||
- 右Y轴:答题数量(绿色 `#00a870`,带面积渐变)
|
||||
- 平滑曲线
|
||||
- Tooltip:十字准线指示器
|
||||
- X轴标签:分钟维度每隔5个点显示(即1小时间隔),天维度旋转45度
|
||||
|
||||
### 3. 导出确认对话框
|
||||
|
||||
#### 功能说明
|
||||
- `t-dialog` 弹窗,标题"确认导出"
|
||||
- 展示当前筛选条件,允许在导出前调整
|
||||
- 级联重置:切换学校清空年级和班级,切换年级清空班级
|
||||
|
||||
#### 导出筛选条件
|
||||
|
||||
| 筛选项 | 组件 | 显示条件 |
|
||||
|--------|------|----------|
|
||||
| 云校 | `t-select` | 始终显示 |
|
||||
| 学校 | `t-select` | 始终显示 |
|
||||
| 年级 | `t-select` | 始终显示(未选学校时禁用) |
|
||||
| 班级 | `t-select` | 始终显示(未选年级时禁用) |
|
||||
| 日期范围 | `t-date-range-picker` | 总体学情 或 在线统计(天维度) |
|
||||
| 选择日期 | `t-date-picker` | 在线统计(分钟/小时维度) |
|
||||
| 时间维度 | 文字显示 | 在线统计时显示 |
|
||||
|
||||
#### 导出逻辑
|
||||
```
|
||||
confirmExport():
|
||||
1. 根据 exportType 确定数据源和列配置
|
||||
2. 提取表头和数据行
|
||||
3. 使用 XLSX 库创建工作簿
|
||||
4. 文件名格式:{报告名}_{云校}_{学校}_{年级}_{班级}.xlsx
|
||||
5. 导出成功提示
|
||||
```
|
||||
|
||||
**各标签页导出文件名**:
|
||||
- 总体学情:`总体学情统计分析表`
|
||||
- 正确率排名:`学生答题正确率排名表`
|
||||
- 进度分布:`学生答题进度分布表`
|
||||
- 正确率分布:`学生答题正确率分布表`
|
||||
- 在线统计:`学生在线与答题趋势`
|
||||
|
||||
### 4. 班级详情模态框
|
||||
|
||||
#### 功能说明
|
||||
- `t-dialog` 弹窗,宽度 900px,无底部按钮
|
||||
- 标题:`{科目} - 学生详情`
|
||||
- 触发方式:点击总体学情表格中的班级行
|
||||
|
||||
#### 班级概览信息
|
||||
|
||||
| 信息项 | 说明 |
|
||||
|--------|------|
|
||||
| 班级名称 | 科目名称 |
|
||||
| 总人数 | 高亮显示 |
|
||||
| 平均在线率 | 蓝色样式,计算公式:在线人数/总人数×100% |
|
||||
| 平均完成率 | 普通样式 |
|
||||
| 平均正确率 | 绿色样式 |
|
||||
|
||||
#### 学生数据表格
|
||||
|
||||
| 列Key | 列名 | 宽度 | 对齐 | 特殊渲染 |
|
||||
|-------|------|------|------|----------|
|
||||
| `name` | 姓名 | 100 | center | 固定左侧 |
|
||||
| `onlineDays` | 在线天数 | 90 | center | - |
|
||||
| `onlineRate` | 在线率 | 90 | center | 蓝色标签样式 |
|
||||
| `totalAnswerCount` | 总答题数 | 95 | center | - |
|
||||
| `completionRate` | 完成率 | 90 | center | ≥80%绿色,≥60%蓝色,<60%橙色 |
|
||||
| `answerProgress` | 答题进度 | 90 | center | - |
|
||||
| `correctAnswerCount` | 正确答题数 | 100 | center | - |
|
||||
| `accuracy` | 正确率 | 90 | center | ≥85%绿色背景,≥60%蓝色背景,<60%红色背景 |
|
||||
|
||||
**学生数据导出**:
|
||||
- 导出格式:CSV(UTF-8 BOM)
|
||||
- 文件名:`{班级名}_学生数据_{YYYY-MM-DD_HH-mm-ss}.csv`
|
||||
- 导出过程有 loading 状态
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `go-back` | 点击返回按钮 | 无 |
|
||||
|
||||
## 数据联动
|
||||
|
||||
- 标签页切换 → 重置页码到第1页,对应标签页图表重新渲染
|
||||
- 时间维度切换 → 重新生成趋势数据并渲染图表
|
||||
- 日期选择变化 → 重新生成趋势数据并渲染图表
|
||||
- 窗口大小变化 → 所有图表自适应缩放
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- TDesign Vue Next(`t-button`、`t-icon`、`t-select`、`t-option`、`t-tabs`、`t-tab-panel`、`t-table`、`t-enhanced-table`、`t-date-range-picker`、`t-date-picker`、`t-radio-group`、`t-radio-button`、`t-dialog`、`MessagePlugin`)
|
||||
- ECharts(图表渲染)
|
||||
- dayjs(日期处理)
|
||||
- xlsx(Excel 导出)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# 首页(报告中心)
|
||||
|
||||
## 页面概述
|
||||
|
||||
首页是报告中心的入口页面,以卡片网格形式展示各类数据报告的入口。用户可通过点击卡片跳转到对应的报告详情页,也可通过返回按钮回到门户首页。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 返回按钮区域
|
||||
- 位于页面顶部
|
||||
- 点击后触发 `go-back` 事件,返回门户首页
|
||||
- 悬停时边框变为主题蓝色,背景变为浅蓝
|
||||
|
||||
### 2. 报告入口网格
|
||||
采用 `t-row` / `t-col` 栅格布局,每行4列(`span=6`),展示以下5个报告入口卡片:
|
||||
|
||||
| 序号 | 报告ID | 报告名称 | 图标 | 图标背景色 | 描述 |
|
||||
|------|--------|----------|------|------------|------|
|
||||
| 1 | `cloud_school` | 作业打卡行为报告 | cloud | 蓝色渐变 `#4A90E2 → #357ABD` | 查看作业打卡行为数据分析与趋势报告 |
|
||||
| 2 | `school` | 学习行为报告 | home | 橙色渐变 `#F5A623 → #E59411` | 查看各校学生行为详细报告与对比分析 |
|
||||
| 3 | `leaderboard` | 排行榜 | chart-bar | 绿色渐变 `#7ED321 → #68B019` | 查看积分、知识点、互动、笔记精选等多维度排行榜 |
|
||||
| 4 | `english_word` | 英语单词报告 | layers | 紫色渐变 `#667eea → #764ba2` | 查看英语单词学情统计、正确率排名、进度分布等报告 |
|
||||
| 5 | `checkin_stats` | 打卡互动实时统计报告 | chart-pie | 蓝色渐变 `#00C6FF → #0072FF` | 查看已打卡名单、未打卡名单、总参与概况、互动统计等实时数据 |
|
||||
|
||||
## 交互行为
|
||||
|
||||
- **点击卡片**:触发 `navigate` 事件,传递对应报告的 `id`,由父组件路由到对应报告页面
|
||||
- **卡片悬停**:卡片上浮4px,阴影加深,过渡动画使用 `cubic-bezier(0.4, 0, 0.2, 1)`
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `navigate` | 点击报告卡片 | `report.id`(如 `'cloud_school'`、`'leaderboard'` 等) |
|
||||
| `go-back` | 点击返回按钮 | 无 |
|
||||
|
||||
## 响应式布局
|
||||
|
||||
- `max-width: 1200px`:卡片下边距缩小为16px
|
||||
- `max-width: 768px`:卡片内边距缩小,卡片下边距缩小为16px
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- TDesign Vue Next(`t-row`、`t-col`、`t-card`、`t-icon`)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# 排行榜
|
||||
|
||||
## 页面概述
|
||||
|
||||
排行榜页面展示学员在多个维度的排名数据,支持灵活的筛选条件和时间范围选择。页面包含4个排行榜标签页,每个标签页均有独立的时间筛选器和数据表格。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 页面标题区域
|
||||
- 返回首页按钮:触发 `go-back` 事件
|
||||
- 页面标题:排行榜
|
||||
|
||||
### 2. 筛选区域
|
||||
|
||||
#### 筛选规则提示
|
||||
- 提示文字:先选**云校/学校**,再选**年级**,最后选**班级**(级联选择)
|
||||
- 重置按钮:恢复所有筛选条件为默认值
|
||||
|
||||
#### 筛选条件
|
||||
|
||||
| 筛选项 | 组件 | 默认值 | 级联关系 | 选项 |
|
||||
|--------|------|--------|----------|------|
|
||||
| 云校 | `a-select` | `all`(全部云校) | 顶层 | 全部云校、华东云校、华北云校、华南云校、西部云校 |
|
||||
| 学校 | `a-select` | 无 | 依赖云校 | 全部学校 + 根据云校动态过滤的学校列表 |
|
||||
| 年级 | `a-select` | 无 | 依赖学校 | 高一、高二、高三(未选学校时禁用) |
|
||||
| 班级 | `a-select` | 无 | 依赖年级 | 根据年级动态过滤的班级列表(未选年级时禁用) |
|
||||
| 科目 | `a-select` | `all`(全部科目) | 独立 | 全部科目、语文、数学、英语、物理、化学、生物、历史、地理、政治 |
|
||||
| 学生层次 | `a-select` | `all`(全部) | 独立 | 全部、本科、特控、拔尖 |
|
||||
|
||||
#### 级联选择逻辑
|
||||
- 切换云校 → 清空学校、年级、班级
|
||||
- 切换学校 → 清空年级、班级
|
||||
- 切换年级 → 清空班级
|
||||
- 年级列和班级列根据筛选条件动态显示/隐藏
|
||||
|
||||
#### 学校与云校的对应关系
|
||||
| 云校 | 所属学校 |
|
||||
|------|----------|
|
||||
| 华东云校 | 清华大学、北京大学、复旦大学、上海交通大学 |
|
||||
| 华北云校 | 浙江大学、南京大学 |
|
||||
| 华南云校 | 武汉大学、华中科技大学、中山大学 |
|
||||
| 西部云校 | 四川大学 |
|
||||
|
||||
#### 年级与班级的对应关系
|
||||
| 年级 | 班级 |
|
||||
|------|------|
|
||||
| 高一 | 1班、2班、3班 |
|
||||
| 高二 | 4班、5班、6班 |
|
||||
| 高三 | 7班、8班、9班 |
|
||||
|
||||
### 3. 排行榜标签页
|
||||
|
||||
#### 标签页列表
|
||||
|
||||
| Key | 标签名 | 数据列 |
|
||||
|-----|--------|--------|
|
||||
| `points` | 积分排行榜 | 排名、学员姓名、学校、[年级]、[班级]、总积分 |
|
||||
| `courses` | 完成知识点排行榜 | 排名、学员姓名、学校、[年级]、[班级]、完成知识点数 |
|
||||
| `likes` | 互动排行榜 | 排名、学员姓名、学校、[年级]、[班级]、点赞+评论数 |
|
||||
| `answers` | 笔记精选排行榜 | 排名、学员姓名、学校、[年级]、[班级]、精选次数 |
|
||||
|
||||
> 注:年级列和班级列仅在对应筛选条件被选中时才显示(动态列配置)
|
||||
|
||||
### 4. 时间筛选器
|
||||
|
||||
每个标签页均有独立的时间筛选器:
|
||||
|
||||
| 选项 | Key | 日期范围计算逻辑 |
|
||||
|------|-----|------------------|
|
||||
| 总榜 | `all` | 默认最近30天,可自定义(最多60天) |
|
||||
| 本周榜 | `thisWeek` | 本周一至本周日 |
|
||||
| 上周榜 | `lastWeek` | 上周一至上周日 |
|
||||
| 今日榜 | `today` | 当天 |
|
||||
| 昨日榜 | `yesterday` | 昨天 |
|
||||
|
||||
#### 自定义时间范围(仅总榜可用)
|
||||
- 非总榜时,时间范围选择器禁用,自动计算对应日期范围
|
||||
- 总榜时,可手动选择起止日期
|
||||
- 最大跨度限制:60天
|
||||
- 起始日期最大值 = 结束日期
|
||||
- 结束日期最小值 = 起始日期
|
||||
- 结束日期最大值 = 起始日期 + 60天
|
||||
|
||||
### 5. 排行榜表格
|
||||
|
||||
#### 排名展示
|
||||
- 前三名:显示奖牌图标(金 `rank-gold`、银 `rank-silver`、铜 `rank-bronze`)
|
||||
- 第四名及以后:显示数字
|
||||
|
||||
#### 学员信息
|
||||
- 头像:根据姓名首字生成,颜色由姓名字符编码决定
|
||||
- 姓名:直接显示
|
||||
|
||||
#### 数据展示
|
||||
- 积分:带千分位格式化,后缀"积分"
|
||||
- 完成知识点数:后缀"个"
|
||||
- 互动数:带千分位格式化,后缀"次"
|
||||
- 精选次数:后缀"次"
|
||||
|
||||
#### 分页
|
||||
- 每页10条
|
||||
|
||||
### 6. 导出功能
|
||||
- 每个标签页均有"导出表格"按钮
|
||||
- 导出格式:CSV
|
||||
|
||||
## 数据生成逻辑
|
||||
|
||||
模拟数据采用确定性算法生成,覆盖所有学校、年级、班级、科目、学生层次的组合,每个组合生成2名学生。数据按数值降序排序后分配排名。筛选后重新计算排名。
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `go-back` | 点击返回首页按钮 | 无 |
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- TDesign Vue Next(`t-button`、`t-icon`)
|
||||
- Ant Design Vue(`a-select`、`a-table`、`a-tabs`、`a-avatar`、`a-button`)
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
# 学习行为报告
|
||||
|
||||
## 页面概述
|
||||
|
||||
学习行为报告页面展示学员学习行为的各项数据指标,包括学员人数、上线人数、打卡人数、互动数据、学习时长等多维度统计。页面提供灵活的筛选条件、可配置的数据表格和交互式趋势图表。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 页面标题区域
|
||||
- 返回首页按钮:触发 `goBack` 事件
|
||||
- 页面标题:学习行为报告
|
||||
- 副标题:学员学习行为数据统计与分析
|
||||
|
||||
### 2. 筛选控制区
|
||||
|
||||
#### 筛选规则
|
||||
- 标题:筛选条件(带过滤图标)
|
||||
- 操作按钮:重置、查询
|
||||
|
||||
#### 组织架构筛选
|
||||
|
||||
| 筛选项 | 组件 | 默认值 | 级联关系 | 选项 |
|
||||
|--------|------|--------|----------|------|
|
||||
| 云校 | `t-select` | `all`(所有云校) | 顶层 | 所有云校、重庆八中云校、河南云校、安徽云校 |
|
||||
| 学校 | `t-select` | 空 | 依赖云校 | 重庆八中、河南郑州一中、安徽合肥一中、四川成都七中、湖北武汉二中 |
|
||||
| 年级 | `t-select` | 空 | 依赖学校 | 高一、高二、高三(未选学校时禁用) |
|
||||
| 班级 | `t-select` | 空 | 依赖年级 | 1班~6班(未选年级时禁用) |
|
||||
|
||||
#### 范围筛选
|
||||
|
||||
| 筛选项 | 组件 | 默认值 | 选项 |
|
||||
|--------|------|--------|------|
|
||||
| 科目 | `t-select` | `all`(全部) | 全部、语文、数学、英语、物理、化学、政治、生物、历史、地理 |
|
||||
| 层次 | `t-select` | `all`(全部) | 全部、本科、特控、拔尖 |
|
||||
| 时间 | `a-range-picker` | 最近30天 | 最多查询30天,禁止选择未来日期 |
|
||||
|
||||
#### 级联选择逻辑
|
||||
- 切换云校 → 触发 `handleCloudSchoolChange`
|
||||
- 切换学校 → 清空年级、班级
|
||||
- 切换年级 → 清空班级
|
||||
|
||||
#### 日期范围验证
|
||||
- 最大查询跨度:30天
|
||||
- 选中第一个日期后,自动禁用距离超过30天的日期
|
||||
- 禁止选择未来日期
|
||||
- 校验失败时显示红色错误提示,并清空日期选择
|
||||
- 提交查询时进行二次校验
|
||||
|
||||
### 3. 数据表格区域
|
||||
|
||||
#### 操作按钮
|
||||
- **导出数据**:打开导出确认模态框
|
||||
- **编辑列**:打开列编辑模态框
|
||||
|
||||
#### 表格列定义(共17列)
|
||||
|
||||
| Key | 列名 | 宽度 | 对齐 | 固定 | Tooltip 说明 |
|
||||
|-----|------|------|------|------|-------------|
|
||||
| `id` | 序号 | 70 | center | left | - |
|
||||
| `date` | 日期 | 110 | center | left | - |
|
||||
| `studentCount` | 学员人数 | 100 | right | - | - |
|
||||
| `onlineCount` | 上线人数 | 100 | right | - | 上线标准=当日学习+答题时长>30秒 |
|
||||
| `checkInCount` | 打卡人数 | 100 | right | - | 打卡标准=当日任意课程完成打卡,课程打卡指标需灵活设置 |
|
||||
| `dailyDropoutCount` | 当日退学人数 | 120 | right | - | - |
|
||||
| `totalDropoutCount` | 累计退学人数 | 120 | right | - | - |
|
||||
| `likedCount` | 点赞人数 | 110 | right | - | 当日点赞人数(按人头去重) |
|
||||
| `commentedCount` | 评论人数 | 110 | right | - | 当日评论人数(按人头去重) |
|
||||
| `homeworkCompletedCount` | 完成所有课程人数 | 140 | right | - | 当日所有课程完成打卡人数(按人头去重) |
|
||||
| `noCheckInCount` | 未打卡人数 | 110 | right | - | 当日所有课程未打卡 |
|
||||
| `excellentHomeworkCount` | 笔记评精选人数 | 140 | right | - | 当日笔记被评选为精选的人数(按人头去重) |
|
||||
| `avgStudyDuration` | 平均学习时长 | 120 | right | - | - |
|
||||
| `onlyStudyNoPayCount` | 仅学无交人数 | 120 | right | - | 当日仅参与学习,但没有提交动作的人数 |
|
||||
| `masteredKnowledgePoints` | 攻克知识点数 | 120 | right | - | 当日所有人攻克知识点的数量 |
|
||||
| `incompleteCount` | 未完成所有课程人数 | 140 | right | - | 当日未完成所有课程的人数 |
|
||||
| `failedMasterCount` | 攻克失败人数 | 120 | right | - | 当日最大重试后仍未掌握知识点的人数 |
|
||||
|
||||
#### 平均学习时长格式化
|
||||
- 格式化规则:分钟数转换为"X小时Y分钟"格式
|
||||
- 不足1小时时仅显示"Y分钟"
|
||||
|
||||
#### 分页配置
|
||||
- 默认每页:10条
|
||||
- 可选每页条数:10、20、50、100
|
||||
- 显示总条数:`共 X 条数据`
|
||||
- 支持快速跳页
|
||||
|
||||
### 4. 列编辑器
|
||||
|
||||
#### 功能说明
|
||||
- 支持拖拽排序列顺序(使用 `vuedraggable` 组件)
|
||||
- 支持勾选控制列的显示/隐藏
|
||||
- 序号列和日期列固定在最前面,不可编辑
|
||||
- 至少需要选择一列才能保存
|
||||
|
||||
#### 操作按钮
|
||||
- 全选:选中所有可编辑列
|
||||
- 清空:取消所有可编辑列
|
||||
- 保存:保存列配置到 `localStorage`
|
||||
- 取消:放弃本次修改
|
||||
|
||||
#### 持久化
|
||||
- 存储键名:`learning_behavior_report_column_order`
|
||||
- 存储位置:`localStorage`
|
||||
- 加载时验证配置有效性,确保所有 key 存在于默认配置中
|
||||
- 序号和日期列始终固定在最前面且可见
|
||||
|
||||
### 5. 统计图表区域
|
||||
|
||||
#### 图表控制
|
||||
|
||||
**展示指标选择**:
|
||||
- 多选下拉框,最多可选10个指标
|
||||
- 显示已选/总数计数
|
||||
- 快捷操作:全选、清空
|
||||
- 默认选中:学员人数、上线人数、打卡人数、完成所有课程人数、仅学无交人数
|
||||
|
||||
**可选指标及颜色映射**:
|
||||
|
||||
| 指标 | Key | 颜色 |
|
||||
|------|-----|------|
|
||||
| 学员人数 | `studentCount` | `#5470c6` |
|
||||
| 上线人数 | `onlineCount` | `#91cc75` |
|
||||
| 打卡人数 | `checkInCount` | `#2ba471` |
|
||||
| 当日退学人数 | `dailyDropoutCount` | `#ee6666` |
|
||||
| 累计退学人数 | `totalDropoutCount` | `#fc8452` |
|
||||
| 点赞人数 | `likedCount` | `#fac858` |
|
||||
| 评论人数 | `commentedCount` | `#73c0de` |
|
||||
| 完成所有课程人数 | `homeworkCompletedCount` | `#3ba272` |
|
||||
| 未打卡人数 | `noCheckInCount` | `#9a60b4` |
|
||||
| 笔记评精选人数 | `excellentHomeworkCount` | `#ea7ccc` |
|
||||
| 平均学习时长(分钟) | `avgStudyDuration` | `#48b8d0` |
|
||||
| 仅学无交人数 | `onlyStudyNoPayCount` | `#dd6b66` |
|
||||
| 攻克知识点数 | `masteredKnowledgePoints` | `#c23531` |
|
||||
| 未完成所有课程人数 | `incompleteCount` | `#61a0a8` |
|
||||
| 攻克失败人数 | `failedMasterCount` | `#ee6666` |
|
||||
|
||||
**图表类型切换**:
|
||||
- 折线图(默认):带平滑曲线和渐变面积
|
||||
- 柱状图
|
||||
|
||||
#### 图表特性
|
||||
- ECharts 渲染,支持窗口自适应
|
||||
- Tooltip:十字准线指示器,白色背景带阴影
|
||||
- 图例:可滚动,支持翻页
|
||||
- 工具栏:保存图片(2倍像素比)、数据视图(只读)、图表类型切换、还原
|
||||
- X轴:日期,标签旋转45度
|
||||
- Y轴:数值轴,虚线网格线
|
||||
- 折线图面积渐变:从指标颜色40%透明度渐变到5%透明度
|
||||
- 动画:800ms,cubicOut 缓动
|
||||
|
||||
### 6. 导出确认模态框
|
||||
|
||||
#### 功能说明
|
||||
- 展示当前筛选条件,允许在导出前调整
|
||||
- 导出模态框中的时间范围无30天限制(仅禁用未来日期)
|
||||
- 解决模态框中日期选择器层级遮挡问题(`getPopupContainer` 设为 `document.body`)
|
||||
|
||||
#### 导出逻辑
|
||||
```
|
||||
handleExportData():
|
||||
1. 检查是否有可导出数据
|
||||
2. 根据当前可见列配置生成数据行
|
||||
3. 平均学习时长列使用格式化后的文本
|
||||
4. 使用 XLSX 库创建工作簿
|
||||
5. 导出文件名:学习行为报告_YYYYMMDDHHmmss.xlsx
|
||||
6. 导出成功/失败提示
|
||||
```
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `goBack` | 点击返回首页按钮 | 无 |
|
||||
|
||||
## 数据联动
|
||||
|
||||
- 筛选条件变化(云校、科目、层次、时间范围)→ 自动更新图表
|
||||
- 图表类型切换 → 自动更新图表
|
||||
- 指标选择变化 → 自动更新图表
|
||||
- 窗口大小变化 → 图表自适应缩放
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- TDesign Vue Next(`t-button`、`t-icon`、`t-select`、`t-card`、`t-dialog`、`t-checkbox`、`t-radio-group`、`t-radio-button`)
|
||||
- Ant Design Vue(`a-table`、`a-range-picker`、`a-tooltip`)
|
||||
- ECharts(图表渲染)
|
||||
- dayjs(日期处理)
|
||||
- xlsx(Excel 导出)
|
||||
- vuedraggable(列拖拽排序)
|
||||
- @ant-design/icons-vue(`ExclamationCircleOutlined`、`InfoCircleOutlined`)
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
# 登录页面
|
||||
|
||||
## 页面概述
|
||||
|
||||
登录页面是系统的入口,提供扫码登录和账号密码登录两种方式。页面采用左右分栏布局,左侧为装饰插画区,右侧为登录操作区。当前为产品原型演示阶段,扫码登录暂不可用。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 顶部 Logo 区域
|
||||
- 展示劝学品牌 Logo 图标和"劝学"文字
|
||||
- Logo 图标来源于 `../assets/logo.png`
|
||||
|
||||
### 2. 主体内容区(左右分栏)
|
||||
|
||||
#### 左侧装饰区
|
||||
- 展示装饰插画,图片来源 `../assets/content-block-1-eZzov1mX.png`
|
||||
- 最大宽度 480px,自适应缩放
|
||||
|
||||
#### 右侧登录区域
|
||||
|
||||
##### 原型提示框
|
||||
- 默认显示(`showNotice = true`),可手动关闭
|
||||
- 提示内容:
|
||||
- 标题:产品原型演示阶段
|
||||
- 说明:扫码登录暂不可用,请使用账号密码登录
|
||||
- 测试账号:账号 `1`,密码 `1`(以橙色标签高亮展示)
|
||||
- 关闭按钮位于右上角
|
||||
- 带有淡入淡出动画(`notice-fade` 过渡)
|
||||
|
||||
##### 登录卡片(宽度 340px)
|
||||
|
||||
**Tab 切换**:
|
||||
- 扫码登录(默认激活)
|
||||
- 账号登录
|
||||
- 激活状态:文字变为主题蓝色,底部显示蓝色下划线指示器
|
||||
|
||||
**扫码登录面板**:
|
||||
- 模拟二维码 SVG 展示(150×150px)
|
||||
- 提示文字:请使用劝学APP扫码登录
|
||||
- 操作路径说明:打开劝学·我的-右上角扫一扫
|
||||
- App 下载横幅:
|
||||
- 链接地址:`https://app.23544.com/#/download`(新窗口打开)
|
||||
- 内容:下载图标 + "没有劝学APP?" + "点击立即下载,体验更多功能"
|
||||
- 悬停效果:上浮2px,阴影加深,边框变蓝
|
||||
|
||||
**账号登录面板**:
|
||||
- 账号输入框:
|
||||
- 标签:账号
|
||||
- 占位符:请输入手机号/账号
|
||||
- 默认值:`1`(测试账号)
|
||||
- 密码输入框:
|
||||
- 标签:密码
|
||||
- 占位符:请输入密码
|
||||
- 默认值:`1`(测试密码)
|
||||
- 支持密码显示/隐藏切换(眼睛图标按钮)
|
||||
- `showPassword` 状态控制输入框类型(`text` / `password`)
|
||||
- 表单选项行:
|
||||
- 记住我:复选框,绑定 `form.remember`
|
||||
- 忘记密码:链接(功能待实现)
|
||||
- 登录按钮:点击触发 `handleLogin` 方法
|
||||
|
||||
## 交互逻辑
|
||||
|
||||
### 登录验证
|
||||
```
|
||||
handleLogin():
|
||||
1. 校验:用户名和密码不能为空
|
||||
2. 校验失败:弹出 alert 提示"请输入账号和密码"
|
||||
3. 校验成功:触发 login-success 事件
|
||||
```
|
||||
|
||||
### 表单数据模型
|
||||
```javascript
|
||||
form = {
|
||||
username: '1', // 默认测试账号
|
||||
password: '1', // 默认测试密码
|
||||
remember: false // 记住我状态
|
||||
}
|
||||
```
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `login-success` | 账号密码校验通过 | 无 |
|
||||
|
||||
## 页面样式
|
||||
|
||||
- 整体背景:蓝色渐变 `linear-gradient(145deg, #dce8fb, #e8f1fd, #c8dcf8)`
|
||||
- 底部波浪装饰:半透明蓝色渐变
|
||||
- 登录卡片:白色圆角卡片,阴影 `0 8px 40px rgba(0, 82, 217, 0.1)`
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 当前为原型演示阶段,扫码登录仅为 UI 展示,实际不可用
|
||||
- 测试账号密码均为 `1`,已预填在表单中
|
||||
- 忘记密码链接功能尚未实现
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- 无外部组件库依赖,纯 Vue 3 组件
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
# 笔记评优
|
||||
|
||||
## 页面概述
|
||||
|
||||
笔记评优页面用于查看和管理班级课程笔记的评价数据。页面采用左右分栏布局,左侧为日期选择和课程列表,右侧为朋友圈风格的笔记提交记录展示。教师可对学员笔记进行评优操作,包括选择图片、添加点评等。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 页面标题区域
|
||||
- 返回按钮:触发 `go-back` 事件
|
||||
- 标题图标:笔记评优
|
||||
- 副标题:查看和管理班级课程笔记评价数据
|
||||
|
||||
### 2. 标签页
|
||||
|
||||
| Key | 标签名 | 状态 |
|
||||
|-----|--------|------|
|
||||
| `class1` | 爱学蝶变高一1班 | 已开发 |
|
||||
| `class2` | 爱学蝶变高一2班 | 开发中 |
|
||||
| `class3` | 爱学蝶变高一3班 | 开发中 |
|
||||
|
||||
> 仅高一1班有完整功能,其余班级显示"功能开发中"占位
|
||||
|
||||
### 3. 左侧面板(30%宽度)
|
||||
|
||||
#### 日期选择器
|
||||
- 默认选中当天
|
||||
- 左右箭头切换日期(左箭头=下一天,右箭头=上一天)
|
||||
- 点击日期显示区域展开自定义日历选择器
|
||||
- 活动时间范围:过去30天至今天,禁止选择未来日期
|
||||
- 日历组件功能:月份切换、星期标题、日期网格、清除/今天/确定按钮
|
||||
- 点击外部区域自动关闭日历
|
||||
- 日期验证:不在活动范围内时显示错误提示
|
||||
|
||||
#### 课程安排列表
|
||||
- 显示课程数量徽标
|
||||
- 课程卡片内容:节次名称、上课时间、科目名称、课程类型标签(预习/复习)、状态图标
|
||||
- 课程状态根据选中日期动态计算:
|
||||
- 过去日期:全部"已上课"(绿色对勾)
|
||||
- 未来日期:全部"未上课"(灰色时钟)
|
||||
- 当天:第1~3节"已上课",第4节"正在上课"(黄色播放图标+脉冲动画),第5~8节"未上课"
|
||||
- 点击课程卡片选中/取消选中
|
||||
- 选中状态:卡片高亮显示
|
||||
|
||||
**课程数据**:
|
||||
|
||||
| ID | 节次 | 时间 | 科目 | 类型 |
|
||||
|----|------|------|------|------|
|
||||
| 1 | 第1节 | 8:00-8:45 | 语文 | 预习 |
|
||||
| 2 | 第2节 | 8:55-9:40 | 数学 | 复习 |
|
||||
| 3 | 第3节 | 10:00-10:45 | 英语 | 预习 |
|
||||
| 4 | 第4节 | 10:55-11:40 | 物理 | 复习 |
|
||||
| 5 | 第5节 | 14:00-14:45 | 化学 | 预习 |
|
||||
| 6 | 第6节 | 14:55-15:40 | 生物 | 复习 |
|
||||
| 7 | 第7节 | 16:00-16:45 | 历史 | 预习 |
|
||||
| 8 | 第8节 | 16:55-17:40 | 地理 | 复习 |
|
||||
|
||||
### 4. 右侧面板(70%宽度)
|
||||
|
||||
#### 空状态
|
||||
- 未选择课程时显示
|
||||
- 提示:选择左侧课程卡片查看详情
|
||||
|
||||
#### 笔记提交记录(选择课程后显示)
|
||||
|
||||
**标题栏**:
|
||||
- 课程名称 - 笔记提交记录
|
||||
- 已交人数 / 筛选结果人数
|
||||
|
||||
**筛选区域**:
|
||||
|
||||
| 筛选项 | 组件 | 选项 |
|
||||
|--------|------|------|
|
||||
| 评优状态 | `a-radio-group` | 全部、已评优、未评优 |
|
||||
| 学生姓名 | `a-input` | 关键词搜索,支持清除 |
|
||||
|
||||
- 有筛选条件时显示"清除筛选"按钮
|
||||
- 筛选条件变化时自动重置到第1页
|
||||
|
||||
**笔记卡片(朋友圈风格)**:
|
||||
|
||||
每条笔记包含以下区域:
|
||||
1. **评优标签**:已评优笔记顶部显示星形图标 + "优秀标记"
|
||||
2. **头像**:学员头像
|
||||
3. **姓名**:学员姓名
|
||||
4. **内容**:笔记文字描述
|
||||
5. **图片墙**:
|
||||
- 布局规则:1张=`grid-1`,2或4张=`grid-2`,其他=`grid-3`
|
||||
- 超过9张图片时只显示前9张,第9格显示"+N 查看全部"遮罩
|
||||
- 未评优状态下:左上角显示勾选圆圈,点击切换图片选中状态
|
||||
- 已评优状态下:图片锁定,不可取消选中
|
||||
- 选中逻辑:默认全选(`selectedImages` 为空时所有图片视为选中)
|
||||
6. **评优操作栏**:
|
||||
- 左侧:评优标签文字 + 已选图片数量提示
|
||||
- 右侧:`a-switch` 开关(选中显示"优",未选中显示"评")
|
||||
7. **教师点评**:已评优笔记显示点评内容和时间
|
||||
8. **底部**:提交时间 + 点赞数/评论数(点击打开详情弹窗)
|
||||
|
||||
**图片选择逻辑**:
|
||||
```
|
||||
toggleImageSelect(student, index):
|
||||
- 已评优 → 不允许操作
|
||||
- selectedImages 为空(全选状态)→ 取消该张,其余保留
|
||||
- selectedImages 非空:
|
||||
- 已选中 → 从列表移除
|
||||
- 未选中 → 添加到列表,若全部选中则重置为空(全选)
|
||||
```
|
||||
|
||||
### 5. 分页控件
|
||||
- 总记录数 / 当前页 / 总页数
|
||||
- 每页显示数量选择:10、20、50 条
|
||||
- 页码导航:首页、上一页、页码按钮、下一页、末页
|
||||
- 页码显示规则:总页数≤7时全部显示,否则使用省略号
|
||||
|
||||
### 6. 灯箱(Lightbox)
|
||||
|
||||
#### 功能说明
|
||||
- 全屏遮罩层,使用 `teleport` 渲染到 `body`
|
||||
- 图片居中显示,带切换动画
|
||||
- 图片计数器:`当前索引 + 1 / 总数`
|
||||
- 左右箭头切换图片(循环)
|
||||
- 底部缩略图条:当前图片高亮,已选中图片带勾选标记
|
||||
- 未评优状态下:灯箱内显示"选择此图/已选中"按钮,缩略图支持勾选
|
||||
|
||||
#### 键盘快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `ArrowLeft` | 上一张图片 |
|
||||
| `ArrowRight` | 下一张图片 |
|
||||
| `Escape` | 关闭灯箱 |
|
||||
|
||||
### 7. 详情弹窗
|
||||
|
||||
#### 功能说明
|
||||
- `a-modal` 弹窗,宽度 520px,居中显示
|
||||
- 头部:学员头像、姓名、提交时间、优秀笔记标签
|
||||
- 笔记内容文字
|
||||
- 点赞区:点赞数量 + 点赞者列表(头像+姓名)
|
||||
- 评论区:评论数量 + 评论列表(头像+姓名+内容+时间)
|
||||
- 无数据时显示"暂无点赞"/"暂无评论"
|
||||
|
||||
### 8. 评优模态框
|
||||
|
||||
#### 功能说明
|
||||
- 开启评优时弹出(`a-switch` 切换为 `true`)
|
||||
- 使用 `teleport` 渲染到 `body`
|
||||
- 标题:评为优秀笔记
|
||||
- 副标题:为 XXX 的笔记添加点评
|
||||
|
||||
#### 内容区域
|
||||
1. **已选图片预览**:最多显示4张缩略图,超过4张显示"+N"
|
||||
2. **点评内容输入**:
|
||||
- `a-textarea`,4行,最大200字,带字数统计
|
||||
- 必填项,校验失败时显示红色错误提示
|
||||
- 占位符:请输入对该笔记的点评内容
|
||||
|
||||
#### 操作按钮
|
||||
- 取消:关闭模态框,不保存
|
||||
- 确认评优:校验点评内容,通过后设置评优状态
|
||||
|
||||
#### 评优确认逻辑
|
||||
```
|
||||
confirmExcellent():
|
||||
1. 校验点评内容不为空
|
||||
2. 设置 student.isExcellent = true
|
||||
3. 设置 student.excellentComment = 输入的点评
|
||||
4. 设置 student.excellentTime = 当前时间(HH:mm)
|
||||
5. 关闭模态框
|
||||
```
|
||||
|
||||
### 9. 取消评优确认对话框
|
||||
|
||||
#### 功能说明
|
||||
- 取消评优时弹出(`a-switch` 切换为 `false`)
|
||||
- 使用 `teleport` 渲染到 `body`
|
||||
- 警告图标 + 标题"确认取消评优?"
|
||||
- 说明文字:取消后优秀标记及点评内容将被清除,此操作不可撤销
|
||||
- 操作按钮:
|
||||
- 再想想:关闭对话框
|
||||
- 确认取消:执行取消评优操作
|
||||
|
||||
#### 取消评优逻辑
|
||||
```
|
||||
doRemoveExcellent():
|
||||
1. 设置 student.isExcellent = false
|
||||
2. 清空 student.excellentComment
|
||||
3. 清空 student.excellentTime
|
||||
4. 关闭对话框
|
||||
```
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `go-back` | 点击返回按钮 | 无 |
|
||||
|
||||
## 数据说明
|
||||
|
||||
- 模拟数据:100名学生,每名有1~8张随机图片
|
||||
- 每7名学生中有1名预设为已评优状态
|
||||
- 点赞和评论数据随机生成
|
||||
- 头像使用 `api.dicebear.com` 生成
|
||||
- 图片使用 `picsum.photos` 生成
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- Ant Design Vue(`a-tabs`、`a-tab-pane`、`a-row`、`a-col`、`a-radio-group`、`a-input`、`a-switch`、`a-textarea`、`a-select`、`a-modal`)
|
||||
- @ant-design/icons-vue(`CaretLeftOutlined`、`CaretRightOutlined`、`StarFilled`)
|
||||
- dayjs(日期处理)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# 门户首页
|
||||
|
||||
## 页面概述
|
||||
|
||||
门户首页是用户登录后的主入口页面,展示系统的主要功能模块。页面分为两大模块区域:双师课堂和爱学蝶变,每个模块以卡片形式展示功能入口。
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 1. 顶部导航栏
|
||||
- 高度 60px,毛玻璃效果(`backdrop-filter: blur(20px)`)
|
||||
- 粘性定位(`position: sticky`),始终置顶
|
||||
- 左侧:Logo 图标(来源于 `../Assets/logo.png`)
|
||||
- 右侧:用户信息区域
|
||||
- 用户头像:蓝色渐变圆角矩形,显示姓氏首字"杨"
|
||||
- 用户名:显示"杨某某"
|
||||
- 下拉箭头:悬停时旋转180度
|
||||
- 悬停效果:背景变为浅蓝色
|
||||
|
||||
#### 用户下拉菜单
|
||||
- 鼠标移入用户信息区域时显示
|
||||
- 菜单项:退出登录(带图标)
|
||||
- 悬停效果:背景渐变,文字变蓝
|
||||
- 过渡动画:`dropdown` 动画(淡入 + 上移 + 缩放)
|
||||
- 点击"退出登录":触发 `logout` 事件
|
||||
|
||||
### 2. 主内容区
|
||||
|
||||
#### 双师课堂模块
|
||||
- 模块标识:蓝色圆点 + "双师课堂"标题
|
||||
- 功能卡片:
|
||||
|
||||
| 卡片ID | 名称 | 图标颜色 | 点击行为 |
|
||||
|--------|------|----------|----------|
|
||||
| `schedule` | 课表管理 | 蓝色 `#1677ff` | 弹出提示框"跳转之前的课表管理界面" |
|
||||
|
||||
#### 爱学蝶变模块
|
||||
- 模块标识:紫色圆点 + "爱学蝶变"标题
|
||||
- 功能卡片:
|
||||
|
||||
| 卡片ID | 名称 | 图标颜色 | 点击行为 |
|
||||
|--------|------|----------|----------|
|
||||
| `schedule2` | 课表管理 | 蓝色 `#1677ff` | 弹出提示框,说明与双师课堂课表管理的差异 |
|
||||
| `notes` | 笔记评优 | 橙色 `#fa8c16` | 触发 `card-click` 事件,参数 `'note_evaluation'` |
|
||||
| `report` | 报告中心 | 绿色 `#52c41a` | 触发 `card-click` 事件,参数 `'report'` |
|
||||
|
||||
#### 爱学蝶变课表管理与双师课堂课表管理的差异
|
||||
点击爱学蝶变的"课表管理"时,弹窗提示以下差异:
|
||||
1. 没有单双周功能
|
||||
2. 没有临时调课功能
|
||||
3. 新建课表没有创建科目信息这一步,默认九大科目:语文、数学、英语、物理、化学、生物、政治、历史、地理
|
||||
4. 增加课表的课程需要增加:预习、复习
|
||||
|
||||
### 3. 底部版权
|
||||
- 内容:`Copyright © 2025 QuanXue. All Rights Reserved. 渝ICP备05088888号`
|
||||
|
||||
### 4. 提示弹窗
|
||||
- 用于展示功能说明信息(如课表管理差异说明)
|
||||
- 点击遮罩层可关闭
|
||||
- 包含信息图标、消息文本和"确定"按钮
|
||||
|
||||
## 交互逻辑
|
||||
|
||||
### 卡片点击处理
|
||||
```
|
||||
handleCardClick(cardId):
|
||||
- 'schedule' → 显示提示弹窗"跳转之前的课表管理界面"
|
||||
- 'schedule2' → 显示提示弹窗,列出爱学蝶变课表管理的4项差异
|
||||
- 'notes' → 触发 card-click 事件,参数 'note_evaluation'
|
||||
- 'report' → 触发 card-click 事件,参数 'report'
|
||||
```
|
||||
|
||||
### 退出登录
|
||||
```
|
||||
handleLogout():
|
||||
1. 关闭下拉菜单
|
||||
2. 触发 logout 事件
|
||||
```
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 触发时机 | 参数 |
|
||||
|--------|----------|------|
|
||||
| `navigate` | 保留事件 | - |
|
||||
| `card-click` | 点击笔记评优或报告中心卡片 | `'note_evaluation'` 或 `'report'` |
|
||||
| `logout` | 点击退出登录 | 无 |
|
||||
|
||||
## 页面样式
|
||||
|
||||
- 整体背景:灰蓝渐变 `linear-gradient(165deg, #f5f7fc, #eef2fa, #e8ecf8)`
|
||||
- 模块区块:白色毛玻璃卡片,圆角20px,多层阴影
|
||||
- 功能卡片:白色渐变背景,圆角18px,悬停时边框变蓝、阴影加深
|
||||
- 最大内容宽度:1120px,居中显示
|
||||
|
||||
## 组件依赖
|
||||
|
||||
- 无外部组件库依赖,纯 Vue 3 组件
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeNav from '../components/HomeNav.vue'
|
||||
import LoginPage from '../components/LoginPage.vue'
|
||||
import MobilePrototypePage from '../components/MobilePrototypePage.vue'
|
||||
import NoteEvaluationPage from '../components/NoteEvaluationPage.vue'
|
||||
import HomePage from '../components/HomePage.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'HomeNav',
|
||||
component: HomeNav
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'LoginPage',
|
||||
component: LoginPage
|
||||
},
|
||||
{
|
||||
path: '/mb',
|
||||
name: 'MobilePrototype',
|
||||
component: MobilePrototypePage
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'Admin',
|
||||
component: () => import('../components/PortalHomePage.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin-dashboard',
|
||||
name: 'AdminDashboard',
|
||||
component: () => import('../components/AdminDashboardPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/note-evaluation',
|
||||
name: 'NoteEvaluation',
|
||||
component: () => import('../components/NoteEvaluationPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/report-home',
|
||||
name: 'ReportHome',
|
||||
component: () => import('../components/HomePage.vue')
|
||||
},
|
||||
{
|
||||
path: '/cloud-school-report',
|
||||
name: 'CloudSchoolReport',
|
||||
component: () => import('../components/CloudSchoolReport.vue')
|
||||
},
|
||||
{
|
||||
path: '/learning-behavior-report',
|
||||
name: 'LearningBehaviorReport',
|
||||
component: () => import('../components/LearningBehaviorReport.vue')
|
||||
},
|
||||
{
|
||||
path: '/leaderboard',
|
||||
name: 'Leaderboard',
|
||||
component: () => import('../components/LeaderboardPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/english-word-report',
|
||||
name: 'EnglishWordReport',
|
||||
component: () => import('../components/EnglishWordReport.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 将以下样式添加到 src/components/CloudSchoolReport.vue 的 <style scoped> 标签中
|
||||
* 位置:在 :::deep(.ant-table-cell) 样式之后,@media 查询之前
|
||||
*/
|
||||
|
||||
/* 列拖拽样式 */
|
||||
:::deep(.ant-table-thead > tr > th) {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:::deep(.ant-table-thead > tr > th:hover) {
|
||||
background: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
background: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
background: #e6f7ff !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
opacity: 0.8;
|
||||
background: #fff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
assetsInclude: ['**/*.md'],
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue