feat(路由): 添加在线学习监控和学习总览页面路由

feat(首页): 新增学习情况总览表入口和在线学习监控卡片

refactor(班级分配): 重构班级分配页面,增加学习官查询功能

chore: 删除无用的Git清理脚本和文档文件

style: 调整样式和图标,优化用户体验
This commit is contained in:
YangQiang 2026-04-17 15:49:45 +08:00
parent 37d15fdda9
commit b67fc03c01
11 changed files with 7225 additions and 403 deletions

View File

@ -2,9 +2,9 @@
"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",
"previewUrl": "https://aixuediebian-kanban-3fzkam9s.edgeone.cool?eo_token=500f76b21d03fd0693ba2f52f2757888&eo_time=1776404056",
"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
}
"deploymentUrl": "https://console.cloud.tencent.com/edgeone/pages/project/pages-eip23arpzoar/deployment/4dxuaw1yre",
"deployId": "4dxuaw1yre",
"lastDeployTime": 1776404056
}

View File

@ -1,107 +0,0 @@
# 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 -- <文件路径>
# 如果文件未提交,但还在工作区
# 可以使用系统回收站或备份恢复
```

View File

@ -1,42 +0,0 @@
@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

View File

@ -1,60 +0,0 @@
# 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 ""

View File

@ -1,47 +0,0 @@
#!/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 ""

View File

@ -2,91 +2,242 @@
<div class="class-allocation-page">
<!-- 左侧区域 -->
<div class="left-panel">
<!-- 学校选择下拉框 -->
<div class="school-select-wrapper">
<a-select
v-model:value="selectedSchool"
placeholder="请选择学校"
show-search
:filter-option="filterSchoolOption"
@change="handleSchoolChange"
class="school-select"
>
<a-select-option v-for="school in schoolList" :key="school.id" :value="school.id">
{{ school.name }}
</a-select-option>
</a-select>
</div>
<!-- 树形结构控件 -->
<div class="tree-wrapper">
<a-tree
:tree-data="treeData"
:expanded-keys="expandedKeys"
:selected-keys="selectedKeys"
@expand="handleExpand"
@select="handleSelect"
class="class-tree"
>
<template #title="{ data }">
<div
class="tree-node"
:class="{
'has-officer': data.hasOfficer,
'no-officer': data.isClass && !data.hasOfficer,
'selected': selectedKeys.includes(data.key)
}"
>
<!-- 学校节点图标 -->
<template v-if="data.type === 'school'">
<span class="node-icon school-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L21 10.09V17H23V9L12 3Z" fill="#1890ff"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18L12 17L5 13.18Z" fill="#1890ff"/>
</svg>
</span>
</template>
<!-- 年级节点图标 -->
<template v-else-if="data.type === 'grade'">
<span class="node-icon grade-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L23 9L12 3Z" stroke="#52c41a" stroke-width="2" stroke-linejoin="round"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18" stroke="#52c41a" stroke-width="2" stroke-linejoin="round"/>
</svg>
</span>
</template>
<!-- 班级节点图标 -->
<template v-else-if="data.type === 'class'">
<span class="node-icon class-icon" :class="{ assigned: data.hasOfficer, unassigned: !data.hasOfficer }">
<svg v-if="data.hasOfficer" width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="#52c41a" stroke-width="2"/>
<path d="M8 10H16M8 14H12" stroke="#52c41a" stroke-width="2" stroke-linecap="round"/>
<circle cx="17" cy="14" r="2" fill="#52c41a"/>
</svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="#ff4d4f" stroke-width="2"/>
<path d="M8 10H16M8 14H12" stroke="#ff4d4f" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
</template>
<span class="node-title">{{ data.title }}</span>
<!-- 年级节点显示待分配班级数量 -->
<span v-if="data.type === 'grade' && data.unassignedCount > 0" class="unassigned-count-badge">
{{ data.unassignedCount }}个班待分配
</span>
<!-- 班级节点显示学习官信息 -->
<span v-if="data.type === 'class' && data.hasOfficer" class="officer-tag">
{{ data.officerName }}
</span>
<span v-else-if="data.type === 'class' && !data.hasOfficer" class="officer-tag unassigned-tag">
待分配
</span>
<!-- 页签切换 -->
<div class="tabs-wrapper">
<a-tabs v-model:activeKey="activeTabKey" type="card" class="custom-tabs">
<a-tab-pane key="school" tab="学校班级">
<!-- 学校选择下拉框 -->
<div class="school-select-wrapper">
<a-select
v-model:value="selectedSchool"
placeholder="请选择学校"
show-search
:filter-option="filterSchoolOption"
@change="handleSchoolChange"
class="school-select"
>
<a-select-option v-for="school in schoolList" :key="school.id" :value="school.id">
{{ school.name }}
</a-select-option>
</a-select>
</div>
</template>
</a-tree>
<!-- 树形结构控件 -->
<div class="tree-wrapper">
<!-- 未选择学校时的提示 -->
<div v-if="!selectedSchool" class="tree-placeholder">
<div class="placeholder-content">
<div class="placeholder-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L21 10.09V17H23V9L12 3Z" stroke="#d9d9d9" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18" stroke="#d9d9d9" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
</div>
<p class="placeholder-title">请先选择学校</p>
<p class="placeholder-desc">选择学校后将加载相关班级数据</p>
<div class="placeholder-arrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 5v14M5 12l7 7 7-7" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
<a-tree
v-else
:tree-data="treeData"
:expanded-keys="expandedKeys"
:selected-keys="selectedKeys"
@expand="handleExpand"
@select="handleSelect"
class="class-tree"
>
<template #title="{ data }">
<div
class="tree-node"
:class="{
'has-officer': data.hasOfficer,
'no-officer': data.isClass && !data.hasOfficer,
'selected': selectedKeys.includes(data.key)
}"
>
<!-- 学校节点图标 -->
<template v-if="data.type === 'school'">
<span class="node-icon school-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L21 10.09V17H23V9L12 3Z" fill="#1890ff"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18L12 17L5 13.18Z" fill="#1890ff"/>
</svg>
</span>
</template>
<!-- 年级节点图标 -->
<template v-else-if="data.type === 'grade'">
<span class="node-icon grade-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L23 9L12 3Z" stroke="#52c41a" stroke-width="2" stroke-linejoin="round"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18" stroke="#52c41a" stroke-width="2" stroke-linejoin="round"/>
</svg>
</span>
</template>
<!-- 班级节点图标 -->
<template v-else-if="data.type === 'class'">
<span class="node-icon class-icon" :class="{ assigned: data.hasOfficer, unassigned: !data.hasOfficer }">
<svg v-if="data.hasOfficer" width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="#52c41a" stroke-width="2"/>
<path d="M8 10H16M8 14H12" stroke="#52c41a" stroke-width="2" stroke-linecap="round"/>
<circle cx="17" cy="14" r="2" fill="#52c41a"/>
</svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="#ff4d4f" stroke-width="2"/>
<path d="M8 10H16M8 14H12" stroke="#ff4d4f" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
</template>
<span class="node-title">{{ data.title }}</span>
<!-- 年级节点显示待分配班级数量 -->
<span v-if="data.type === 'grade' && data.unassignedCount > 0" class="unassigned-count-badge">
{{ data.unassignedCount }}个班待分配
</span>
<!-- 班级节点显示学习官信息 -->
<span v-if="data.type === 'class' && data.hasOfficer" class="officer-tag">
{{ data.officerName }}
</span>
<span v-else-if="data.type === 'class' && !data.hasOfficer" class="officer-tag unassigned-tag">
待分配
</span>
</div>
</template>
</a-tree>
</div>
</a-tab-pane>
<a-tab-pane key="officer" tab="学习官查询">
<!-- 学习官查询区域 -->
<div class="officer-query-section">
<!-- 搜索输入框 -->
<div class="query-input-wrapper">
<a-input-search
v-model:value="officerQuery.searchText"
placeholder="请输入学习官姓名或手机号"
allow-clear
:loading="officerQuery.loading"
@change="handleOfficerQueryChange"
@search="handleOfficerQuerySearch"
class="officer-query-input"
>
<template #prefix>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="#999" stroke-width="2"/>
<path d="M21 21l-4.35-4.35" stroke="#999" stroke-width="2" stroke-linecap="round"/>
</svg>
</template>
</a-input-search>
</div>
<!-- 加载状态 -->
<div v-if="officerQuery.loading" class="query-loading">
<a-spin size="large" tip="查询中..." />
</div>
<!-- 无结果提示 -->
<div v-else-if="officerQuery.searched && !officerQuery.hasResults && officerQuery.searchText" class="query-empty">
<a-empty description="未找到匹配的学习官">
<template #image>
<svg width="64" height="64" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="#d9d9d9" stroke-width="2"/>
<path d="M21 21l-4.35-4.35" stroke="#d9d9d9" stroke-width="2" stroke-linecap="round"/>
<path d="M8 11h6" stroke="#d9d9d9" stroke-width="2" stroke-linecap="round"/>
</svg>
</template>
</a-empty>
<p class="empty-hint">请检查输入的姓名或手机号是否正确</p>
</div>
<!-- 查询结果树 -->
<div v-else-if="officerQuery.hasResults" class="query-result-wrapper">
<div class="result-header">
<span class="result-count">找到 <strong>{{ officerQuery.resultCount }}</strong> 位学习官</span>
</div>
<a-tree
:tree-data="officerQuery.treeData"
:expanded-keys="officerQuery.expandedKeys"
:selected-keys="officerQuery.selectedKeys"
@expand="handleOfficerQueryExpand"
@select="handleOfficerQuerySelect"
class="officer-query-tree"
>
<template #title="{ data }">
<div
class="query-tree-node"
:class="{
'officer-node': data.type === 'officer',
'school-node': data.type === 'school',
'grade-node': data.type === 'grade',
'class-node': data.type === 'class'
}"
>
<!-- 学习官节点 -->
<template v-if="data.type === 'officer'">
<span class="node-icon officer-icon">
<div class="officer-avatar-small" :style="{ background: getAvatarColor(data.name) }">
{{ data.name.charAt(0) }}
</div>
</span>
<span class="node-title officer-name">{{ data.title }}</span>
<span class="officer-phone-tag">{{ data.phone }}</span>
</template>
<!-- 学校节点 -->
<template v-else-if="data.type === 'school'">
<span class="node-icon school-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L21 10.09V17H23V9L12 3Z" fill="#1890ff"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18L12 17L5 13.18Z" fill="#1890ff"/>
</svg>
</span>
<span class="node-title">{{ data.title }}</span>
</template>
<!-- 年级节点 -->
<template v-else-if="data.type === 'grade'">
<span class="node-icon grade-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L12 15L23 9L12 3Z" stroke="#52c41a" stroke-width="2" stroke-linejoin="round"/>
<path d="M5 13.18V17.18L12 21L19 17.18V13.18" stroke="#52c41a" stroke-width="2" stroke-linejoin="round"/>
</svg>
</span>
<span class="node-title">{{ data.title }}</span>
</template>
<!-- 班级节点 -->
<template v-else-if="data.type === 'class'">
<span class="node-icon class-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="#52c41a" stroke-width="2"/>
<path d="M8 10H16M8 14H12" stroke="#52c41a" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
<span class="node-title">{{ data.title }}</span>
<span class="class-student-count">{{ data.studentCount }}</span>
</template>
</div>
</template>
</a-tree>
</div>
<!-- 初始提示 -->
<div v-else class="query-hint">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="#d9d9d9" stroke-width="2"/>
<path d="M21 21l-4.35-4.35" stroke="#d9d9d9" stroke-width="2" stroke-linecap="round"/>
</svg>
<p>请输入学习官姓名或手机号进行查询</p>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</div>
@ -283,58 +434,7 @@
class="filter-input"
/>
</div>
<div class="filter-item">
<span class="filter-label">状态</span>
<a-select
v-model:value="officerFilter.status"
placeholder="全部状态"
allowClear
class="filter-select-modal"
>
<a-select-option value="available">空闲</a-select-option>
<a-select-option value="busy">忙碌</a-select-option>
<a-select-option value="leave">请假</a-select-option>
</a-select>
</div>
<div class="filter-item">
<span class="filter-label">区域</span>
<a-select
v-model:value="officerFilter.area"
placeholder="全部区域"
allowClear
class="filter-select-modal"
>
<a-select-option value="east">东区</a-select-option>
<a-select-option value="west">西区</a-select-option>
<a-select-option value="south">南区</a-select-option>
<a-select-option value="north">北区</a-select-option>
<a-select-option value="central">中心区</a-select-option>
</a-select>
</div>
</div>
<div class="filter-row">
<div class="filter-item">
<span class="filter-label">管理班级数</span>
<a-select
v-model:value="officerFilter.classCount"
placeholder="不限"
allowClear
class="filter-select-modal"
>
<a-select-option value="0">无管理班级</a-select-option>
<a-select-option value="1-3">1~3</a-select-option>
<a-select-option value="4-6">4~6</a-select-option>
<a-select-option value="7+">7个以上</a-select-option>
</a-select>
</div>
<div class="filter-item">
<span class="filter-label">入职时间</span>
<a-range-picker
v-model:value="officerFilter.dateRange"
class="filter-date"
format="YYYY-MM-DD"
/>
</div>
<div class="filter-actions">
<a-button type="primary" @click="handleFilterOfficers">
<template #icon>
@ -420,7 +520,172 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
//
const activeTabKey = ref('school')
// ========== ==========
const officerQuery = ref({
searchText: '',
loading: false,
searched: false,
hasResults: false,
resultCount: 0,
treeData: [],
expandedKeys: [],
selectedKeys: []
})
let queryTimeout = null
// 500ms
const handleOfficerQueryChange = () => {
//
if (queryTimeout) {
clearTimeout(queryTimeout)
}
const searchText = officerQuery.value.searchText?.trim()
if (!searchText) {
officerQuery.value.searched = false
officerQuery.value.hasResults = false
officerQuery.value.treeData = []
return
}
// 500ms
queryTimeout = setTimeout(() => {
performOfficerQuery(searchText)
}, 500)
}
//
const performOfficerQuery = (searchText) => {
officerQuery.value.loading = true
officerQuery.value.searched = true
//
setTimeout(() => {
//
const results = generateOfficerQueryResults(searchText)
officerQuery.value.treeData = results
officerQuery.value.hasResults = results.length > 0
officerQuery.value.resultCount = results.length
officerQuery.value.expandedKeys = results.map(r => r.key)
officerQuery.value.loading = false
}, 300)
}
//
const generateOfficerQueryResults = (searchText) => {
const results = []
const surnames = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄', '周', '吴']
const names = ['明', '华', '强', '伟', '磊', '静', '敏', '丽', '婷', '娜']
//
for (let i = 1; i <= 5; i++) {
const surname = surnames[Math.floor(Math.random() * surnames.length)]
const name = names[Math.floor(Math.random() * names.length)]
const fullName = surname + name
const phone = `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`
//
const nameMatch = fullName.includes(searchText)
const phoneMatch = phone.includes(searchText)
if (nameMatch || phoneMatch || searchText.length < 2) {
//
const officerNode = {
key: `officer-${i}`,
title: fullName,
name: fullName,
phone: phone,
type: 'officer',
children: generateOfficerSchoolTree(i)
}
results.push(officerNode)
}
}
return results
}
//
const generateOfficerSchoolTree = (officerId) => {
const schools = []
const schoolCount = Math.floor(Math.random() * 2) + 1
for (let s = 0; s < schoolCount; s++) {
const school = schoolList.value[Math.floor(Math.random() * schoolList.value.length)]
const schoolData = classDataMap[school.id]
if (schoolData) {
const schoolNode = {
key: `officer-${officerId}-school-${s}`,
title: school.name,
type: 'school',
children: []
}
//
schoolData.forEach((grade, gradeIdx) => {
const gradeNode = {
key: `officer-${officerId}-school-${s}-grade-${gradeIdx}`,
title: grade.gradeName,
type: 'grade',
children: []
}
// 1-3
const classCount = Math.min(Math.floor(Math.random() * 3) + 1, grade.classes.length)
const shuffled = [...grade.classes].sort(() => Math.random() - 0.5)
shuffled.slice(0, classCount).forEach((cls, classIdx) => {
gradeNode.children.push({
key: `officer-${officerId}-school-${s}-grade-${gradeIdx}-class-${classIdx}`,
title: cls.className,
type: 'class',
studentCount: cls.studentCount,
isLeaf: true
})
})
if (gradeNode.children.length > 0) {
schoolNode.children.push(gradeNode)
}
})
if (schoolNode.children.length > 0) {
schools.push(schoolNode)
}
}
}
return schools
}
//
const handleOfficerQuerySearch = () => {
handleOfficerQueryChange()
}
// /
const handleOfficerQueryExpand = (keys) => {
officerQuery.value.expandedKeys = keys
}
//
const handleOfficerQuerySelect = (selectedKeysValue, { node }) => {
officerQuery.value.selectedKeys = selectedKeysValue
//
if (node.type === 'class') {
//
}
}
//
const schoolList = ref([
@ -491,11 +756,9 @@ const officerCandidates = ref([
const officerColumns = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 140 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
{ title: '区域', dataIndex: 'areaName', key: 'areaName', width: 90 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 90, align: 'center' },
{ title: '管理班级', dataIndex: 'classCount', key: 'classCount', width: 100, align: 'center', sorter: (a, b) => a.classCount - b.classCount },
{ title: '入职时间', dataIndex: 'joinDate', key: 'joinDate', width: 110, sorter: (a, b) => a.joinDate.localeCompare(b.joinDate) },
{ title: '经验', dataIndex: 'experience', key: 'experience', width: 80 },
{ title: '操作', key: 'action', width: 90, align: 'center', fixed: 'right' }
]
@ -935,6 +1198,167 @@ const handleSelect = (selectedKeysValue, { node }) => {
background-color: #fafafa;
}
/* 页签样式 */
.tabs-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.custom-tabs {
height: 100%;
}
.custom-tabs :deep(.ant-tabs-nav) {
margin-bottom: 0;
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
}
.custom-tabs :deep(.ant-tabs-content) {
height: calc(100% - 46px);
}
.custom-tabs :deep(.ant-tabs-tabpane) {
height: 100%;
overflow-y: auto;
}
/* 学习官查询区域样式 */
.officer-query-section {
height: 100%;
display: flex;
flex-direction: column;
background-color: #fafafa;
}
.query-input-wrapper {
padding: 16px;
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
}
.officer-query-input {
width: 100%;
}
.query-loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.query-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.empty-hint {
margin-top: 8px;
font-size: 13px;
color: #999;
}
.query-hint {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #999;
}
.query-hint p {
margin-top: 16px;
font-size: 14px;
}
.query-result-wrapper {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.result-header {
padding: 8px 12px;
margin-bottom: 12px;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%);
border-radius: 6px;
border: 1px solid #d6e4ff;
}
.result-count {
font-size: 13px;
color: #666;
}
.result-count strong {
color: #1890ff;
font-weight: 600;
}
.officer-query-tree {
background: transparent;
}
.query-tree-node {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.query-tree-node.officer-node {
background: linear-gradient(135deg, #f6ffed 0%, #fcfff7 100%);
border-radius: 6px;
padding: 8px 12px;
margin: 4px 0;
border: 1px solid #d9f7be;
}
.officer-avatar-small {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 12px;
font-weight: 600;
}
.officer-name {
font-weight: 600;
color: #333;
}
.officer-phone-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background-color: #f0f0f0;
color: #666;
margin-left: auto;
}
.class-student-count {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background-color: #f5f5f5;
color: #999;
margin-left: auto;
}
.school-select-wrapper {
padding: 16px;
border-bottom: 1px solid #e8e8e8;
@ -949,6 +1373,69 @@ const handleSelect = (selectedKeysValue, { node }) => {
flex: 1;
overflow-y: auto;
padding: 8px;
position: relative;
}
/* 未选择学校时的提示样式 */
.tree-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 300px;
}
.placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 40px 20px;
}
.placeholder-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f5f5 0%, #fafafa 100%);
border-radius: 50%;
margin-bottom: 20px;
border: 2px dashed #e8e8e8;
}
.placeholder-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
}
.placeholder-desc {
font-size: 13px;
color: #999;
margin: 0 0 20px 0;
}
.placeholder-arrow {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%);
border-radius: 50%;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
/* 树节点样式 */

View File

@ -136,6 +136,13 @@ const reports = [
icon: 'layers',
iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', //
description: '查看英语单词学情统计、正确率排名、进度分布等报告'
},
{
id: 'learning_overview',
name: '学习情况总览表',
icon: 'table',
iconBg: 'linear-gradient(135deg, #722ed1 0%, #531dab 100%)', //
description: '全面查看学生学习数据统计与分析'
}
]
@ -151,7 +158,8 @@ const handleReportClick = (report) => {
'cloud_school': '/cloud-school-report',
'school': '/learning-behavior-report',
'leaderboard': '/leaderboard',
'english_word': '/english-word-report'
'english_word': '/english-word-report',
'learning_overview': '/learning-overview'
}
const routePath = routeMap[report.id]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -108,6 +108,20 @@
<span class="card-label">笔记评优</span>
<span class="card-arrow"></span>
</div>
<div class="feature-card" @click="handleCardClick('online-monitor')">
<div class="card-icon-wrap card-icon-cyan">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="2" y="3" width="20" height="14" rx="2" stroke="#13c2c2" stroke-width="1.8"/>
<path d="M8 21h8M12 17v4" stroke="#13c2c2" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="12" cy="10" r="2" stroke="#13c2c2" stroke-width="1.6"/>
<path d="M9 7.5a4.5 4.5 0 0 1 6 0" stroke="#13c2c2" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7 5.5a7 7 0 0 1 10 0" stroke="#13c2c2" stroke-width="1.5" stroke-linecap="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">
@ -180,6 +194,14 @@ const handleCardClick = (cardId) => {
router.push('/report-home')
return
}
if (cardId === 'online-monitor') {
router.push('/online-learning-monitor')
return
}
if (cardId === 'learning-overview') {
router.push('/learning-overview')
return
}
emit('card-click', cardId)
}
@ -571,6 +593,16 @@ const handleLogout = () => {
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.card-icon-cyan {
background: linear-gradient(135deg, #e6fffb 0%, #b5f5ec 100%);
box-shadow: 0 2px 8px rgba(19, 194, 194, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.card-icon-purple {
background: linear-gradient(135deg, #f9f0ff 0%, #efdbff 100%);
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.card-label {
font-size: 15px;
color: #1a1a2e;

View File

@ -60,6 +60,16 @@ const routes = [
path: '/english-word-report',
name: 'EnglishWordReport',
component: () => import('../components/EnglishWordReport.vue')
},
{
path: '/online-learning-monitor',
name: 'OnlineLearningMonitor',
component: () => import('../components/OnlineLearningMonitorPage.vue')
},
{
path: '/learning-overview',
name: 'LearningOverview',
component: () => import('../components/LearningOverviewPage.vue')
}
]