1465 lines
41 KiB
HTML
1465 lines
41 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|||
|
|
<title>学生数据分析 - 荣光课堂</title>
|
|||
|
|
<link rel="stylesheet" href="../css/indexHome.css" />
|
|||
|
|
<link rel="stylesheet" href="../css/userDisplay.css" />
|
|||
|
|
<link
|
|||
|
|
rel="stylesheet"
|
|||
|
|
href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
|
|||
|
|
/>
|
|||
|
|
<style>
|
|||
|
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
|
|||
|
|
|
|||
|
|
:root {
|
|||
|
|
--primary-color: #6366f1;
|
|||
|
|
--primary-light: #818cf8;
|
|||
|
|
--primary-dark: #4f46e5;
|
|||
|
|
--secondary-color: #06b6d4;
|
|||
|
|
--success-color: #10b981;
|
|||
|
|
--warning-color: #f59e0b;
|
|||
|
|
--error-color: #ef4444;
|
|||
|
|
--purple-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
--blue-gradient: linear-gradient(135deg, #6366f1 0%, #06b6d4 100%);
|
|||
|
|
--green-gradient: linear-gradient(135deg, #10b981 0%, #34d399 100%);
|
|||
|
|
--orange-gradient: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
|
|||
|
|
--red-gradient: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
|
|||
|
|
--purple-light-gradient: linear-gradient(
|
|||
|
|
135deg,
|
|||
|
|
#a855f7 0%,
|
|||
|
|
#c084fc 100%
|
|||
|
|
);
|
|||
|
|
--background-color: #f8fafc;
|
|||
|
|
--card-background: #ffffff;
|
|||
|
|
--text-primary: #1e293b;
|
|||
|
|
--text-secondary: #64748b;
|
|||
|
|
--text-muted: #94a3b8;
|
|||
|
|
--border-color: #e2e8f0;
|
|||
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|||
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1),
|
|||
|
|
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|||
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
|||
|
|
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|||
|
|
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1),
|
|||
|
|
0 8px 10px -6px rgb(0 0 0 / 0.1);
|
|||
|
|
--radius-sm: 0.375rem;
|
|||
|
|
--radius-md: 0.5rem;
|
|||
|
|
--radius-lg: 0.75rem;
|
|||
|
|
--radius-xl: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
* {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
padding-top: 80px;
|
|||
|
|
background: var(--background-color);
|
|||
|
|
font-family: "Inter", "Noto Sans SC", sans-serif;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
overflow-x: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.analytics-container {
|
|||
|
|
max-width: 1400px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
padding: 2rem;
|
|||
|
|
animation: fadeInUp 0.6s ease-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fadeInUp {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(30px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes slideInLeft {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateX(-30px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateX(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes slideInRight {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateX(30px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateX(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes scaleIn {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: scale(0.9);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: scale(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header {
|
|||
|
|
background: var(--purple-gradient);
|
|||
|
|
color: white;
|
|||
|
|
padding: 3rem 2rem;
|
|||
|
|
border-radius: var(--radius-xl);
|
|||
|
|
margin-bottom: 2.5rem;
|
|||
|
|
text-align: center;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
box-shadow: var(--shadow-xl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 100" fill="white" opacity="0.1"><polygon points="0,0 1000,100 1000,0"/></svg>');
|
|||
|
|
background-size: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header-content {
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header h1 {
|
|||
|
|
margin: 0 0 1rem 0;
|
|||
|
|
font-size: 3rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: -0.025em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header h1 i {
|
|||
|
|
margin-right: 1rem;
|
|||
|
|
background: linear-gradient(45deg, #fff, #e0e7ff);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header p {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 1.25rem;
|
|||
|
|
opacity: 0.95;
|
|||
|
|
font-weight: 400;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-section {
|
|||
|
|
background: var(--card-background);
|
|||
|
|
padding: 2rem;
|
|||
|
|
border-radius: var(--radius-lg);
|
|||
|
|
box-shadow: var(--shadow-sm);
|
|||
|
|
margin-bottom: 2.5rem;
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
animation: slideInLeft 0.6s ease-out 0.1s both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-row {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 2rem;
|
|||
|
|
align-items: end;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-group {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
min-width: 200px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-label {
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-select {
|
|||
|
|
padding: 0.75rem 1rem;
|
|||
|
|
border: 2px solid var(--border-color);
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
background: white;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
font-family: inherit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-select:focus {
|
|||
|
|
outline: none;
|
|||
|
|
border-color: var(--primary-color);
|
|||
|
|
box-shadow: 0 0 0 3px rgb(99 102 241 / 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.refresh-btn {
|
|||
|
|
background: var(--blue-gradient);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 0.75rem 2rem;
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
box-shadow: var(--shadow-md);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.refresh-btn:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: var(--shadow-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.refresh-btn:active {
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stats-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|||
|
|
gap: 2rem;
|
|||
|
|
margin-bottom: 2.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-card {
|
|||
|
|
background: var(--card-background);
|
|||
|
|
padding: 2rem;
|
|||
|
|
border-radius: var(--radius-xl);
|
|||
|
|
box-shadow: var(--shadow-sm);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
text-align: center;
|
|||
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
animation: scaleIn 0.6s ease-out var(--delay, 0s) both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-card:nth-child(1) {
|
|||
|
|
--delay: 0.1s;
|
|||
|
|
}
|
|||
|
|
.stat-card:nth-child(2) {
|
|||
|
|
--delay: 0.2s;
|
|||
|
|
}
|
|||
|
|
.stat-card:nth-child(3) {
|
|||
|
|
--delay: 0.3s;
|
|||
|
|
}
|
|||
|
|
.stat-card:nth-child(4) {
|
|||
|
|
--delay: 0.4s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-card::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
height: 4px;
|
|||
|
|
background: var(--gradient);
|
|||
|
|
transition: height 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-card:hover {
|
|||
|
|
transform: translateY(-8px);
|
|||
|
|
box-shadow: var(--shadow-xl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-card:hover::before {
|
|||
|
|
height: 100%;
|
|||
|
|
opacity: 0.05;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-card:nth-child(1) {
|
|||
|
|
--gradient: var(--green-gradient);
|
|||
|
|
}
|
|||
|
|
.stat-card:nth-child(2) {
|
|||
|
|
--gradient: var(--blue-gradient);
|
|||
|
|
}
|
|||
|
|
.stat-card:nth-child(3) {
|
|||
|
|
--gradient: var(--orange-gradient);
|
|||
|
|
}
|
|||
|
|
.stat-card:nth-child(4) {
|
|||
|
|
--gradient: var(--purple-light-gradient);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-icon {
|
|||
|
|
width: 4rem;
|
|||
|
|
height: 4rem;
|
|||
|
|
margin: 0 auto 1.5rem;
|
|||
|
|
background: var(--gradient);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
color: white;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-number {
|
|||
|
|
font-size: 3rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
background: var(--gradient);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-label {
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
font-size: 1rem;
|
|||
|
|
font-weight: 500;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chart-container {
|
|||
|
|
background: var(--card-background);
|
|||
|
|
padding: 2rem;
|
|||
|
|
border-radius: var(--radius-xl);
|
|||
|
|
box-shadow: var(--shadow-sm);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
margin-bottom: 2.5rem;
|
|||
|
|
animation: slideInRight 0.6s ease-out 0.2s both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chart-title {
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
margin-bottom: 2rem;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.75rem;
|
|||
|
|
padding-bottom: 1rem;
|
|||
|
|
border-bottom: 2px solid var(--border-color);
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chart-title::after {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: -2px;
|
|||
|
|
left: 0;
|
|||
|
|
width: 4rem;
|
|||
|
|
height: 2px;
|
|||
|
|
background: var(--primary-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chart-title i {
|
|||
|
|
background: var(--blue-gradient);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ai-recommendations {
|
|||
|
|
margin-bottom: 2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 2rem;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
background: var(--card-background);
|
|||
|
|
border-radius: var(--radius-lg);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
border-left: 4px solid var(--accent-color);
|
|||
|
|
box-shadow: var(--shadow-sm);
|
|||
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-item::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background: linear-gradient(135deg, var(--accent-color), transparent);
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-item:hover {
|
|||
|
|
transform: translateX(8px);
|
|||
|
|
box-shadow: var(--shadow-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-item:hover::before {
|
|||
|
|
opacity: 0.03;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-item:nth-child(1) {
|
|||
|
|
--accent-color: var(--success-color);
|
|||
|
|
animation: slideInLeft 0.6s ease-out 0.3s both;
|
|||
|
|
}
|
|||
|
|
.recommendation-item:nth-child(2) {
|
|||
|
|
--accent-color: var(--warning-color);
|
|||
|
|
animation: slideInLeft 0.6s ease-out 0.4s both;
|
|||
|
|
}
|
|||
|
|
.recommendation-item:nth-child(3) {
|
|||
|
|
--accent-color: var(--error-color);
|
|||
|
|
animation: slideInLeft 0.6s ease-out 0.5s both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-icon {
|
|||
|
|
width: 4rem;
|
|||
|
|
height: 4rem;
|
|||
|
|
background: linear-gradient(
|
|||
|
|
135deg,
|
|||
|
|
var(--accent-color),
|
|||
|
|
var(--accent-color) dd
|
|||
|
|
);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-right: 2rem;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-content {
|
|||
|
|
flex: 1;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-title {
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
font-size: 1.125rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-reason {
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
font-size: 0.95rem;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-tags {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-tag {
|
|||
|
|
background: linear-gradient(
|
|||
|
|
135deg,
|
|||
|
|
var(--accent-color) 15,
|
|||
|
|
var(--accent-color) 25
|
|||
|
|
);
|
|||
|
|
color: var(--accent-color);
|
|||
|
|
padding: 0.25rem 0.75rem;
|
|||
|
|
border-radius: 9999px;
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
border: 1px solid var(--accent-color) 30;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-action {
|
|||
|
|
margin-left: 2rem;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.watch-btn {
|
|||
|
|
background: linear-gradient(
|
|||
|
|
135deg,
|
|||
|
|
var(--accent-color),
|
|||
|
|
var(--accent-color) dd
|
|||
|
|
);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 0.75rem 1.5rem;
|
|||
|
|
border-radius: 9999px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-weight: 600;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
box-shadow: var(--shadow-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.watch-btn:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: var(--shadow-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-stats {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|||
|
|
gap: 2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-card {
|
|||
|
|
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
|||
|
|
padding: 2rem;
|
|||
|
|
border-radius: var(--radius-lg);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
animation: scaleIn 0.6s ease-out var(--game-delay, 0s) both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-card:nth-child(1) {
|
|||
|
|
--game-delay: 0.1s;
|
|||
|
|
}
|
|||
|
|
.game-card:nth-child(2) {
|
|||
|
|
--game-delay: 0.2s;
|
|||
|
|
}
|
|||
|
|
.game-card:nth-child(3) {
|
|||
|
|
--game-delay: 0.3s;
|
|||
|
|
}
|
|||
|
|
.game-card:nth-child(4) {
|
|||
|
|
--game-delay: 0.4s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-card::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
height: 4px;
|
|||
|
|
background: var(--blue-gradient);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-card:hover {
|
|||
|
|
transform: translateY(-4px);
|
|||
|
|
box-shadow: var(--shadow-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-title {
|
|||
|
|
font-weight: 700;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
font-size: 1.125rem;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-title::before {
|
|||
|
|
content: "🎮";
|
|||
|
|
font-size: 1.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-metrics {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, 1fr);
|
|||
|
|
gap: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.metric {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 1rem;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.metric-value {
|
|||
|
|
font-size: 2rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
background: var(--blue-gradient);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.metric-label {
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
font-weight: 500;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recent-activities {
|
|||
|
|
background: var(--card-background);
|
|||
|
|
padding: 2rem;
|
|||
|
|
border-radius: var(--radius-xl);
|
|||
|
|
box-shadow: var(--shadow-sm);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
animation: slideInUp 0.6s ease-out 0.3s both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-item:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-item:hover {
|
|||
|
|
background: #f8fafc;
|
|||
|
|
transform: translateX(4px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-icon {
|
|||
|
|
width: 3rem;
|
|||
|
|
height: 3rem;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-right: 1.5rem;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 1.25rem;
|
|||
|
|
background: var(--activity-color);
|
|||
|
|
box-shadow: var(--shadow-sm);
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-content {
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-title {
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activity-time {
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 3rem;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
font-size: 1.125rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading::after {
|
|||
|
|
content: "";
|
|||
|
|
display: inline-block;
|
|||
|
|
width: 2rem;
|
|||
|
|
height: 2rem;
|
|||
|
|
border: 3px solid var(--border-color);
|
|||
|
|
border-top: 3px solid var(--primary-color);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
animation: spin 1s linear infinite;
|
|||
|
|
margin-left: 1rem;
|
|||
|
|
vertical-align: middle;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
0% {
|
|||
|
|
transform: rotate(0deg);
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
transform: rotate(360deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error {
|
|||
|
|
background: linear-gradient(135deg, #fef2f2, #fee2e2);
|
|||
|
|
color: var(--error-color);
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
border-radius: var(--radius-lg);
|
|||
|
|
margin: 2rem 0;
|
|||
|
|
border: 1px solid #fecaca;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 1rem;
|
|||
|
|
animation: shake 0.5s ease-in-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes shake {
|
|||
|
|
0%,
|
|||
|
|
100% {
|
|||
|
|
transform: translateX(0);
|
|||
|
|
}
|
|||
|
|
25% {
|
|||
|
|
transform: translateX(-5px);
|
|||
|
|
}
|
|||
|
|
75% {
|
|||
|
|
transform: translateX(5px);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error::before {
|
|||
|
|
content: "⚠️";
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 1024px) {
|
|||
|
|
.analytics-container {
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header {
|
|||
|
|
padding: 2rem 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header h1 {
|
|||
|
|
font-size: 2.5rem;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.analytics-container {
|
|||
|
|
padding: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stats-grid {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
gap: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-row {
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: stretch;
|
|||
|
|
gap: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-group {
|
|||
|
|
min-width: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-stats {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-item {
|
|||
|
|
flex-direction: column;
|
|||
|
|
text-align: center;
|
|||
|
|
gap: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommendation-action {
|
|||
|
|
margin-left: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header h1 {
|
|||
|
|
font-size: 2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header p {
|
|||
|
|
font-size: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chart-container,
|
|||
|
|
.filter-section,
|
|||
|
|
.recent-activities {
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 自定义滚动条 */
|
|||
|
|
::-webkit-scrollbar {
|
|||
|
|
width: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::-webkit-scrollbar-track {
|
|||
|
|
background: var(--background-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::-webkit-scrollbar-thumb {
|
|||
|
|
background: var(--primary-color);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::-webkit-scrollbar-thumb:hover {
|
|||
|
|
background: var(--primary-dark);
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container-header">
|
|||
|
|
<div class="container-header-logo">
|
|||
|
|
<img src="../asset/logo.png" alt="logo" />
|
|||
|
|
</div>
|
|||
|
|
<nav class="container-header-nav">
|
|||
|
|
<div data-url="indexHome">首页</div>
|
|||
|
|
<div data-url="indexDetail">导航</div>
|
|||
|
|
<div data-url="courseHome">课程</div>
|
|||
|
|
<div class="active">数据分析</div>
|
|||
|
|
</nav>
|
|||
|
|
<div id="userDisplay" class="user-display"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="analytics-container">
|
|||
|
|
<div class="page-header">
|
|||
|
|
<div class="page-header-content">
|
|||
|
|
<h1><i class="fa fa-bar-chart"></i> 学生数据分析</h1>
|
|||
|
|
<p>🚀 全面了解学习进度和答题情况,助力个性化教学</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 筛选区域 -->
|
|||
|
|
<div class="filter-section">
|
|||
|
|
<div class="filter-row">
|
|||
|
|
<div class="filter-group">
|
|||
|
|
<label class="filter-label">选择学生</label>
|
|||
|
|
<select id="studentSelect" class="filter-select">
|
|||
|
|
<option value="">加载中...</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="filter-group">
|
|||
|
|
<label class="filter-label">时间范围</label>
|
|||
|
|
<select id="timeRange" class="filter-select">
|
|||
|
|
<option value="7">最近7天</option>
|
|||
|
|
<option value="30" selected>最近30天</option>
|
|||
|
|
<option value="90">最近90天</option>
|
|||
|
|
<option value="all">全部时间</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="filter-group">
|
|||
|
|
<label class="filter-label">课程分类</label>
|
|||
|
|
<select id="courseCategory" class="filter-select">
|
|||
|
|
<option value="">全部分类</option>
|
|||
|
|
<option value="1">生活</option>
|
|||
|
|
<option value="2">语文</option>
|
|||
|
|
<option value="3">数学</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<button class="refresh-btn" onclick="loadAnalytics()">
|
|||
|
|
<i class="fa fa-refresh"></i> 刷新数据
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 统计卡片 -->
|
|||
|
|
<div class="stats-grid">
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">
|
|||
|
|
<i class="fa fa-book"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-number" id="totalCourses">0</div>
|
|||
|
|
<div class="stat-label">学习课程数</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">
|
|||
|
|
<i class="fa fa-clock-o"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-number" id="totalTime">0</div>
|
|||
|
|
<div class="stat-label">学习时长(小时)</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">
|
|||
|
|
<i class="fa fa-gamepad"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-number" id="totalGames">0</div>
|
|||
|
|
<div class="stat-label">游戏次数</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">
|
|||
|
|
<i class="fa fa-trophy"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-number" id="avgScore">0</div>
|
|||
|
|
<div class="stat-label">平均得分</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- AI智能推荐 -->
|
|||
|
|
<div class="chart-container">
|
|||
|
|
<h2 class="chart-title"><i class="fa fa-magic"></i> AI智能视频推荐</h2>
|
|||
|
|
<div id="aiRecommendations" class="ai-recommendations">
|
|||
|
|
<div class="loading">🤖 正在分析学习数据,生成个性化推荐</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 游戏统计 -->
|
|||
|
|
<div class="chart-container">
|
|||
|
|
<h2 class="chart-title">
|
|||
|
|
<i class="fa fa-pie-chart"></i> 游戏答题统计
|
|||
|
|
</h2>
|
|||
|
|
<div class="game-stats" id="gameStats">
|
|||
|
|
<div class="loading">📊 正在加载游戏数据</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 最近活动 -->
|
|||
|
|
<div class="recent-activities">
|
|||
|
|
<h2 class="chart-title"><i class="fa fa-history"></i> 最近学习活动</h2>
|
|||
|
|
<div id="recentActivities">
|
|||
|
|
<div class="loading">📈 正在加载活动记录</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 引入后端集成脚本 -->
|
|||
|
|
<script src="../js/apiService.js"></script>
|
|||
|
|
<script src="../js/dataManager.js"></script>
|
|||
|
|
<script src="../js/userManager.js"></script>
|
|||
|
|
<script src="../js/accessTracker.js"></script>
|
|||
|
|
<script src="../js/gameTracker.js"></script>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
let currentStudentId = null;
|
|||
|
|
let analyticsData = {};
|
|||
|
|
|
|||
|
|
// 页面加载完成后初始化
|
|||
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|||
|
|
initializePage();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 初始化页面
|
|||
|
|
async function initializePage() {
|
|||
|
|
try {
|
|||
|
|
// 初始化后端集成
|
|||
|
|
await Promise.all([
|
|||
|
|
window.userManager.init(),
|
|||
|
|
window.dataManager.init(),
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// 创建用户显示
|
|||
|
|
window.userManager.createUserDisplay();
|
|||
|
|
|
|||
|
|
// 加载学生列表
|
|||
|
|
await loadStudents();
|
|||
|
|
|
|||
|
|
// 加载分析数据
|
|||
|
|
await loadAnalytics();
|
|||
|
|
|
|||
|
|
console.log("🎉 学生数据分析页面初始化完成");
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("页面初始化失败:", error);
|
|||
|
|
showError("页面初始化失败,请刷新重试");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载学生列表
|
|||
|
|
async function loadStudents() {
|
|||
|
|
try {
|
|||
|
|
const result = await window.apiService.getUsers();
|
|||
|
|
const studentSelect = document.getElementById("studentSelect");
|
|||
|
|
|
|||
|
|
studentSelect.innerHTML = '<option value="">请选择学生</option>';
|
|||
|
|
|
|||
|
|
if (result.data && result.data.length > 0) {
|
|||
|
|
result.data.forEach((user) => {
|
|||
|
|
const option = document.createElement("option");
|
|||
|
|
option.value = user.id;
|
|||
|
|
option.textContent = `${user.realName || user.username} (${
|
|||
|
|
user.className || "未知班级"
|
|||
|
|
})`;
|
|||
|
|
studentSelect.appendChild(option);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
studentSelect.innerHTML = '<option value="">暂无学生数据</option>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听选择变化
|
|||
|
|
studentSelect.addEventListener("change", function () {
|
|||
|
|
currentStudentId = this.value;
|
|||
|
|
if (currentStudentId) {
|
|||
|
|
loadAnalytics();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("加载学生列表失败:", error);
|
|||
|
|
document.getElementById("studentSelect").innerHTML =
|
|||
|
|
'<option value="">加载失败</option>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载分析数据
|
|||
|
|
async function loadAnalytics() {
|
|||
|
|
if (!currentStudentId) {
|
|||
|
|
showError("请先选择学生");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
showLoading();
|
|||
|
|
|
|||
|
|
// 获取筛选条件
|
|||
|
|
const timeRange = document.getElementById("timeRange").value;
|
|||
|
|
const courseCategory =
|
|||
|
|
document.getElementById("courseCategory").value;
|
|||
|
|
|
|||
|
|
// 加载真实数据(优先使用后端API,失败时使用模拟数据)
|
|||
|
|
await loadRealData(currentStudentId, timeRange, courseCategory);
|
|||
|
|
|
|||
|
|
// 更新页面显示
|
|||
|
|
updateStatsDisplay();
|
|||
|
|
updateAIRecommendations();
|
|||
|
|
updateGameStats();
|
|||
|
|
updateRecentActivities();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("加载分析数据失败:", error);
|
|||
|
|
showError("加载数据失败,请重试");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载真实数据
|
|||
|
|
async function loadRealData(studentId, timeRange, category) {
|
|||
|
|
try {
|
|||
|
|
// 并行加载所有数据
|
|||
|
|
const [statsResult, coursesResult, gamesResult, activitiesResult] =
|
|||
|
|
await Promise.all([
|
|||
|
|
window.apiService.getStudentStats(studentId, timeRange, category),
|
|||
|
|
window.apiService.getStudentCourseProgress(
|
|||
|
|
studentId,
|
|||
|
|
timeRange,
|
|||
|
|
category
|
|||
|
|
),
|
|||
|
|
window.apiService.getStudentGameStats(
|
|||
|
|
studentId,
|
|||
|
|
timeRange,
|
|||
|
|
category
|
|||
|
|
),
|
|||
|
|
window.apiService.getStudentRecentActivities(studentId, 10),
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// 处理统计数据
|
|||
|
|
if (statsResult.data) {
|
|||
|
|
analyticsData.stats = statsResult.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理课程数据
|
|||
|
|
if (coursesResult.data && coursesResult.data.courses) {
|
|||
|
|
analyticsData.courses = coursesResult.data.courses.map(
|
|||
|
|
(course) => ({
|
|||
|
|
id: course.id,
|
|||
|
|
name: course.name,
|
|||
|
|
category: course.category,
|
|||
|
|
progress: course.progress,
|
|||
|
|
timeSpent: course.timeSpent,
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理游戏数据
|
|||
|
|
if (gamesResult.data && gamesResult.data.games) {
|
|||
|
|
analyticsData.games = gamesResult.data.games.map((game) => ({
|
|||
|
|
name: game.name,
|
|||
|
|
correct: game.correctCount,
|
|||
|
|
incorrect: game.totalAttempts - game.correctCount,
|
|||
|
|
avgTime: game.avgTime,
|
|||
|
|
attempts: game.totalAttempts,
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理活动数据
|
|||
|
|
if (activitiesResult.data && activitiesResult.data.activities) {
|
|||
|
|
analyticsData.activities = activitiesResult.data.activities.map(
|
|||
|
|
(activity) => ({
|
|||
|
|
type: activity.type,
|
|||
|
|
title: activity.title,
|
|||
|
|
time: activity.timeFormatted,
|
|||
|
|
icon: activity.icon,
|
|||
|
|
color: activity.color,
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载AI推荐数据
|
|||
|
|
try {
|
|||
|
|
const recommendationsResult =
|
|||
|
|
await window.apiService.getAIRecommendations(
|
|||
|
|
studentId,
|
|||
|
|
analyticsData.games,
|
|||
|
|
analyticsData.courses
|
|||
|
|
);
|
|||
|
|
if (recommendationsResult.success) {
|
|||
|
|
analyticsData.recommendations =
|
|||
|
|
recommendationsResult.data.recommendations;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("加载AI推荐失败:", error);
|
|||
|
|
// 使用模拟推荐数据
|
|||
|
|
analyticsData.recommendations =
|
|||
|
|
window.apiService.getMockRecommendations(
|
|||
|
|
analyticsData.games,
|
|||
|
|
analyticsData.courses
|
|||
|
|
).data.recommendations;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("加载真实数据失败:", error);
|
|||
|
|
// 如果真实数据加载失败,使用模拟数据
|
|||
|
|
await loadMockData(studentId, timeRange, category);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟数据加载(备用方案)
|
|||
|
|
async function loadMockData(studentId, timeRange, category) {
|
|||
|
|
// 模拟API调用延迟
|
|||
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|||
|
|
|
|||
|
|
// 模拟数据
|
|||
|
|
analyticsData = {
|
|||
|
|
stats: {
|
|||
|
|
totalCourses: Math.floor(Math.random() * 20) + 10,
|
|||
|
|
totalTime: Math.floor(Math.random() * 50) + 20,
|
|||
|
|
totalGames: Math.floor(Math.random() * 100) + 50,
|
|||
|
|
avgScore: Math.floor(Math.random() * 30) + 70,
|
|||
|
|
},
|
|||
|
|
courses: [
|
|||
|
|
{
|
|||
|
|
id: 1,
|
|||
|
|
name: "生活技能训练",
|
|||
|
|
category: "生活",
|
|||
|
|
progress: 85,
|
|||
|
|
timeSpent: 120,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 2,
|
|||
|
|
name: "语文基础",
|
|||
|
|
category: "语文",
|
|||
|
|
progress: 60,
|
|||
|
|
timeSpent: 90,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 3,
|
|||
|
|
name: "数学计算",
|
|||
|
|
category: "数学",
|
|||
|
|
progress: 45,
|
|||
|
|
timeSpent: 75,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 4,
|
|||
|
|
name: "社交技能",
|
|||
|
|
category: "生活",
|
|||
|
|
progress: 90,
|
|||
|
|
timeSpent: 150,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 5,
|
|||
|
|
name: "阅读理解",
|
|||
|
|
category: "语文",
|
|||
|
|
progress: 30,
|
|||
|
|
timeSpent: 45,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
games: [
|
|||
|
|
{
|
|||
|
|
name: "数字识别游戏",
|
|||
|
|
correct: 45,
|
|||
|
|
incorrect: 5,
|
|||
|
|
avgTime: 12,
|
|||
|
|
attempts: 50,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "颜色配对游戏",
|
|||
|
|
correct: 38,
|
|||
|
|
incorrect: 12,
|
|||
|
|
avgTime: 8,
|
|||
|
|
attempts: 50,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "形状分类游戏",
|
|||
|
|
correct: 42,
|
|||
|
|
incorrect: 8,
|
|||
|
|
avgTime: 15,
|
|||
|
|
attempts: 50,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "字母学习游戏",
|
|||
|
|
correct: 35,
|
|||
|
|
incorrect: 15,
|
|||
|
|
avgTime: 20,
|
|||
|
|
attempts: 50,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
activities: [
|
|||
|
|
{
|
|||
|
|
type: "course",
|
|||
|
|
title: "完成了生活技能训练课程",
|
|||
|
|
time: "2小时前",
|
|||
|
|
icon: "fa-book",
|
|||
|
|
color: "linear-gradient(135deg, #10b981, #34d399)",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: "game",
|
|||
|
|
title: "在数字识别游戏中获得高分",
|
|||
|
|
time: "3小时前",
|
|||
|
|
icon: "fa-trophy",
|
|||
|
|
color: "linear-gradient(135deg, #f59e0b, #fbbf24)",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: "course",
|
|||
|
|
title: "开始学习语文基础课程",
|
|||
|
|
time: "1天前",
|
|||
|
|
icon: "fa-play",
|
|||
|
|
color: "linear-gradient(135deg, #6366f1, #06b6d4)",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: "game",
|
|||
|
|
title: "完成了颜色配对游戏",
|
|||
|
|
time: "1天前",
|
|||
|
|
icon: "fa-check",
|
|||
|
|
color: "linear-gradient(135deg, #10b981, #34d399)",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: "course",
|
|||
|
|
title: "复习了数学计算课程",
|
|||
|
|
time: "2天前",
|
|||
|
|
icon: "fa-refresh",
|
|||
|
|
color: "linear-gradient(135deg, #a855f7, #c084fc)",
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
recommendations: [
|
|||
|
|
{
|
|||
|
|
id: "rec_1",
|
|||
|
|
type: "video",
|
|||
|
|
title: "🎯 数字识别游戏 - 基础讲解视频",
|
|||
|
|
reason:
|
|||
|
|
"根据您的答题情况,在数字识别游戏中正确率为90%,表现优秀!建议观看进阶讲解视频来进一步提升。",
|
|||
|
|
tags: ["进阶讲解", "数学概念", "提升技能"],
|
|||
|
|
videoUrl: "video/数学-2下-视频--认识数字/认识数字课.mp4",
|
|||
|
|
priority: "low",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "rec_2",
|
|||
|
|
type: "course",
|
|||
|
|
title: "📚 语文基础 - 深入学习",
|
|||
|
|
reason:
|
|||
|
|
"您在语文基础课程中的学习进度为60%,建议继续深入学习相关概念。",
|
|||
|
|
tags: ["深入学习", "课程内容", "巩固知识"],
|
|||
|
|
videoUrl: "video/生活-什么是情绪/happy.mp4",
|
|||
|
|
priority: "medium",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "rec_3",
|
|||
|
|
type: "video",
|
|||
|
|
title: "🔥 数学计算 - 基础巩固",
|
|||
|
|
reason:
|
|||
|
|
"您在数学计算课程中的学习进度为45%,建议观看基础讲解视频来巩固数学概念。",
|
|||
|
|
tags: ["基础讲解", "数学概念", "知识巩固"],
|
|||
|
|
videoUrl: "video/数学-2下-视频--饮品(和是4的加法)/教学片段.mp4",
|
|||
|
|
priority: "high",
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新统计显示
|
|||
|
|
function updateStatsDisplay() {
|
|||
|
|
const stats = analyticsData.stats;
|
|||
|
|
animateNumber(
|
|||
|
|
document.getElementById("totalCourses"),
|
|||
|
|
stats.totalCourses
|
|||
|
|
);
|
|||
|
|
animateNumber(document.getElementById("totalTime"), stats.totalTime);
|
|||
|
|
animateNumber(document.getElementById("totalGames"), stats.totalGames);
|
|||
|
|
animateNumber(document.getElementById("avgScore"), stats.avgScore);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 数字动画效果
|
|||
|
|
function animateNumber(element, targetNumber) {
|
|||
|
|
const duration = 1000;
|
|||
|
|
const startNumber = 0;
|
|||
|
|
const increment = targetNumber / (duration / 16);
|
|||
|
|
let currentNumber = startNumber;
|
|||
|
|
|
|||
|
|
const timer = setInterval(() => {
|
|||
|
|
currentNumber += increment;
|
|||
|
|
if (currentNumber >= targetNumber) {
|
|||
|
|
currentNumber = targetNumber;
|
|||
|
|
clearInterval(timer);
|
|||
|
|
}
|
|||
|
|
element.textContent = Math.floor(currentNumber);
|
|||
|
|
}, 16);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新AI推荐
|
|||
|
|
function updateAIRecommendations() {
|
|||
|
|
const container = document.getElementById("aiRecommendations");
|
|||
|
|
const recommendations = analyticsData.recommendations || [];
|
|||
|
|
|
|||
|
|
container.innerHTML = "";
|
|||
|
|
|
|||
|
|
if (recommendations.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="loading">🤔 暂无推荐内容</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
recommendations.forEach((rec) => {
|
|||
|
|
const recItem = document.createElement("div");
|
|||
|
|
recItem.className = "recommendation-item";
|
|||
|
|
|
|||
|
|
// 根据优先级设置不同的图标和颜色
|
|||
|
|
let icon = "fa-play-circle";
|
|||
|
|
if (rec.priority === "high") {
|
|||
|
|
icon = "fa-exclamation-triangle";
|
|||
|
|
} else if (rec.priority === "medium") {
|
|||
|
|
icon = "fa-info-circle";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
recItem.innerHTML = `
|
|||
|
|
<div class="recommendation-icon">
|
|||
|
|
<i class="fa ${icon}"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="recommendation-content">
|
|||
|
|
<div class="recommendation-title">${rec.title}</div>
|
|||
|
|
<div class="recommendation-reason">${rec.reason}</div>
|
|||
|
|
<div class="recommendation-tags">
|
|||
|
|
${rec.tags
|
|||
|
|
.map(
|
|||
|
|
(tag) => `<span class="recommendation-tag">${tag}</span>`
|
|||
|
|
)
|
|||
|
|
.join("")}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="recommendation-action">
|
|||
|
|
<button class="watch-btn" onclick="watchRecommendation('${
|
|||
|
|
rec.videoUrl
|
|||
|
|
}')">
|
|||
|
|
<i class="fa fa-play"></i> 观看
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
container.appendChild(recItem);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 观看推荐视频
|
|||
|
|
function watchRecommendation(videoUrl) {
|
|||
|
|
// 记录用户点击推荐
|
|||
|
|
console.log("🎯 用户点击AI推荐视频:", videoUrl);
|
|||
|
|
|
|||
|
|
// 跳转到视频页面
|
|||
|
|
window.open(videoUrl, "_blank");
|
|||
|
|
|
|||
|
|
// 可以在这里添加推荐点击统计
|
|||
|
|
if (window.accessTracker) {
|
|||
|
|
window.accessTracker.recordAccess(null, null, "recommendation", 0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新游戏统计
|
|||
|
|
function updateGameStats() {
|
|||
|
|
const container = document.getElementById("gameStats");
|
|||
|
|
const games = analyticsData.games;
|
|||
|
|
|
|||
|
|
container.innerHTML = "";
|
|||
|
|
|
|||
|
|
games.forEach((game, index) => {
|
|||
|
|
const gameCard = document.createElement("div");
|
|||
|
|
gameCard.className = "game-card";
|
|||
|
|
gameCard.style.setProperty("--game-delay", `${(index + 1) * 0.1}s`);
|
|||
|
|
|
|||
|
|
const accuracy = Math.round((game.correct / game.attempts) * 100);
|
|||
|
|
|
|||
|
|
gameCard.innerHTML = `
|
|||
|
|
<div class="game-title">${game.name}</div>
|
|||
|
|
<div class="game-metrics">
|
|||
|
|
<div class="metric">
|
|||
|
|
<div class="metric-value">${accuracy}%</div>
|
|||
|
|
<div class="metric-label">正确率</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="metric">
|
|||
|
|
<div class="metric-value">${game.avgTime}s</div>
|
|||
|
|
<div class="metric-label">平均用时</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="metric">
|
|||
|
|
<div class="metric-value">${game.attempts}</div>
|
|||
|
|
<div class="metric-label">总次数</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
container.appendChild(gameCard);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新最近活动
|
|||
|
|
function updateRecentActivities() {
|
|||
|
|
const container = document.getElementById("recentActivities");
|
|||
|
|
const activities = analyticsData.activities;
|
|||
|
|
|
|||
|
|
container.innerHTML = "";
|
|||
|
|
|
|||
|
|
activities.forEach((activity) => {
|
|||
|
|
const activityItem = document.createElement("div");
|
|||
|
|
activityItem.className = "activity-item";
|
|||
|
|
activityItem.innerHTML = `
|
|||
|
|
<div class="activity-icon" style="background: ${activity.color};">
|
|||
|
|
<i class="fa ${activity.icon}"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="activity-content">
|
|||
|
|
<div class="activity-title">${activity.title}</div>
|
|||
|
|
<div class="activity-time">${activity.time}</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
container.appendChild(activityItem);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示加载状态
|
|||
|
|
function showLoading() {
|
|||
|
|
document.getElementById("aiRecommendations").innerHTML =
|
|||
|
|
'<div class="loading">🤖 正在分析学习数据,生成个性化推荐</div>';
|
|||
|
|
document.getElementById("gameStats").innerHTML =
|
|||
|
|
'<div class="loading">📊 正在加载游戏数据</div>';
|
|||
|
|
document.getElementById("recentActivities").innerHTML =
|
|||
|
|
'<div class="loading">📈 正在加载活动记录</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示错误信息
|
|||
|
|
function showError(message) {
|
|||
|
|
const errorDiv = document.createElement("div");
|
|||
|
|
errorDiv.className = "error";
|
|||
|
|
errorDiv.textContent = message;
|
|||
|
|
|
|||
|
|
const container = document.querySelector(".analytics-container");
|
|||
|
|
container.insertBefore(errorDiv, container.firstChild);
|
|||
|
|
|
|||
|
|
// 3秒后自动移除错误信息
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (errorDiv.parentNode) {
|
|||
|
|
errorDiv.parentNode.removeChild(errorDiv);
|
|||
|
|
}
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|