Files
ClassFeedback/课评系统.html
2026-06-02 23:01:58 +08:00

1412 lines
61 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小Q老师 - 课评生成系统 v6.0</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #2d5a3d;
--primary-dark: #1a3a2a;
--accent: #4a8c5c;
--bg: #fdf6ee;
--card: #ffffff;
--text: #1a3a2a;
--muted: #666;
--border: #e8ddd0;
--danger: #e74c3c;
--warning: #f39c12;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background: linear-gradient(135deg, var(--bg) 0%, #f5e6d3 100%);
min-height: 100vh;
padding: 12px;
color: var(--text);
}
.container { max-width: 1300px; margin: 0 auto; }
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: 16px;
padding: 20px 24px;
color: white;
margin-bottom: 12px;
box-shadow: 0 6px 24px rgba(26,58,42,0.25);
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.header h1 { font-size: 22px; font-weight: 900; }
.header .sub { font-size: 13px; opacity: 0.85; margin-left: auto; }
/* ==================== AI 配置面板 ==================== */
.ai-config-bar {
background: #1a1a2e;
border-radius: 12px;
padding: 12px 16px;
margin-bottom: 12px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.ai-config-bar label { color: #94a3b8; font-size: 12px; font-weight: 600; white-space: nowrap; }
.ai-config-bar select, .ai-config-bar input {
padding: 7px 10px;
border-radius: 8px;
border: 1px solid #333;
background: #16213e;
color: #e2e8f0;
font-size: 13px;
font-family: inherit;
}
.ai-config-bar select:focus, .ai-config-bar input:focus {
outline: none;
border-color: #6366f1;
}
.ai-config-bar input { min-width: 200px; }
.ai-config-bar input[type="password"] { min-width: 150px; }
.ai-config-bar .model-input { min-width: 140px; }
.ai-config-bar .toggle-btn {
padding: 7px 12px;
border-radius: 8px;
border: 1px solid #6366f1;
background: transparent;
color: #6366f1;
cursor: pointer;
font-size: 12px;
font-weight: 600;
font-family: inherit;
white-space: nowrap;
}
.ai-config-bar .toggle-btn:hover { background: #6366f120; }
.ai-config-bar .test-btn {
padding: 7px 12px;
border-radius: 8px;
border: none;
background: #6366f1;
color: white;
cursor: pointer;
font-size: 12px;
font-weight: 600;
font-family: inherit;
white-space: nowrap;
}
.ai-config-bar .test-btn:hover { background: #5558e6; }
.ai-config-bar .status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #666;
flex-shrink: 0;
}
.ai-config-bar .status-dot.ok { background: #10b981; }
.ai-config-bar .status-dot.err { background: #ef4444; }
/* ==================== 快速跳转 ==================== */
.quick-jump {
background: var(--card);
border-radius: 14px;
padding: 12px 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: flex;
gap: 10px;
align-items: center;
}
.quick-jump input {
flex: 1;
padding: 10px 14px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 14px;
font-family: inherit;
background: #faf8f5;
}
.quick-jump input:focus { outline: none; border-color: var(--primary); background: white; }
.quick-jump button {
padding: 10px 20px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
.quick-jump button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(45,90,61,0.3); }
/* ==================== 主布局 ==================== */
.main-layout {
display: flex;
gap: 12px;
align-items: flex-start;
}
/* ==================== 选择器面板 ==================== */
.selector-panel {
background: var(--card);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: flex-end;
}
.selector-group { flex: 1; min-width: 140px; }
.selector-label {
font-size: 11px; font-weight: 700; color: var(--muted);
margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px;
}
.selector-select {
width: 100%;
padding: 8px 12px;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
background: #faf8f5;
cursor: pointer;
transition: all 0.2s;
}
.selector-select:focus { outline: none; border-color: var(--primary); background: white; }
.date-display { font-size: 13px; color: var(--primary); font-weight: 600; padding: 8px 0; }
/* ==================== 课程面板 ==================== */
.course-panel {
background: var(--card);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: none;
}
.course-panel.show { display: block; }
.course-info { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.course-item { flex: 1; min-width: 150px; }
.course-item-label { font-size: 10px; font-weight: 700; color: var(--muted); margin-bottom: 1px; }
.course-item-value { font-size: 15px; font-weight: 700; color: var(--primary); }
.custom-course {
border-top: 1px dashed var(--border);
margin-top: 10px; padding-top: 10px; display: none;
}
.custom-course.show { display: block; }
.custom-input {
width: 100%; padding: 8px 12px; border: 2px solid var(--border);
border-radius: 8px; font-size: 14px; font-family: inherit;
margin-bottom: 6px; background: #faf8f5;
}
.custom-input:focus { outline: none; border-color: var(--primary); background: white; }
/* ==================== 进度条 ==================== */
.progress-wrap {
background: var(--card);
border-radius: 14px;
padding: 10px 14px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.progress-track { flex:1; min-width:100px; height:6px; background:#eee; border-radius:3px; overflow:hidden; }
.progress-fill { height:100%; background:linear-gradient(90deg,var(--primary),var(--accent)); border-radius:3px; transition:width 0.3s; width:0%; }
.progress-label { font-size:13px; color:var(--muted); white-space:nowrap; }
.progress-label strong { color:var(--primary); }
/* ==================== 学生卡片 ==================== */
.student-card {
background: var(--card);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
transition: box-shadow 0.2s;
}
.student-card.absent { opacity: 0.55; }
.student-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.student-avatar {
width: 38px; height: 38px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
.student-name { font-size: 15px; font-weight: 800; }
.student-trait { font-size: 11px; color: var(--muted); margin-left: auto; max-width: 200px; text-align: right; }
.status-buttons { display: flex; gap: 6px; margin-bottom: 8px; }
.status-btn {
padding: 4px 12px; border-radius: 16px; font-size: 12px; font-weight: 600;
border: 2px solid var(--border); background: #f5f5f5;
cursor: pointer; transition: all 0.2s; font-family: inherit;
}
.status-btn:hover { transform: translateY(-1px); }
.status-btn.active-present { background: var(--primary); border-color: var(--primary); color: white; }
.status-btn.active-leave { background: var(--danger); border-color: var(--danger); color: white; }
.textarea {
width: 100%; min-height: 60px;
padding: 10px 12px; border-radius: 10px;
border: 2px solid var(--border);
font-size: 14px; font-family: inherit;
resize: vertical; transition: border-color 0.2s;
background: #faf8f5; line-height: 1.6;
}
.textarea:focus { outline: none; border-color: var(--primary); background: white; }
.textarea.filled { border-color: var(--accent); background: #f0faf2; }
.textarea::placeholder { color: #bbb; }
/* ==================== 速记参考 ==================== */
.shorthand {
background: var(--card); border-radius: 14px; padding: 10px 14px;
margin-bottom: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);
font-size: 11px; color: var(--muted); line-height: 1.7;
}
.shorthand code { background:#f0ece6; padding:1px 6px; border-radius:3px; font-size:11px; color:#444; }
/* ==================== 补课学生区 ==================== */
.makeup-card {
background: #fff5f5; border-radius: 14px; padding: 14px 16px;
margin-bottom: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border: 2px dashed var(--danger);
}
.makeup-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.makeup-icon { font-size: 20px; }
.makeup-title { font-weight: 800; color: var(--danger); }
.makeup-entry { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #ffd0d0; }
.makeup-entry:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
.makeup-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.makeup-input {
flex: 1; min-width: 100px; padding: 8px 12px;
border-radius: 8px; border: 2px solid #ffd0d0;
font-size: 13px; font-family: inherit; background: #fff8f8;
}
.makeup-input:focus { outline: none; border-color: var(--danger); background: white; }
.makeup-status-btns { display: flex; gap: 6px; }
.btn-small {
padding: 4px 10px; border-radius: 14px; font-size: 12px; font-weight: 600;
border: 2px solid var(--border); background: #f5f5f5;
cursor: pointer; font-family: inherit;
}
.btn-small.makeup { background: var(--danger); border-color: var(--danger); color: white; }
.btn-small.trial { background: var(--warning); border-color: var(--warning); color: white; }
.btn-remove { background: transparent; border-color: #ddd; color: #999; font-size: 16px; padding: 2px 8px; }
.btn-add {
width: 100%; padding: 8px;
background: white; border: 2px dashed var(--danger); color: var(--danger);
border-radius: 10px; cursor: pointer; font-family: inherit; font-weight: 700; transition: all 0.2s;
}
.btn-add:hover { background: #fff5f5; }
/* ==================== 标签库面板 ==================== */
.tag-sidebar {
flex: 0 0 300px;
background: var(--card);
border-radius: 14px;
padding: 16px 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
position: sticky;
top: 12px;
max-height: calc(100vh - 24px);
overflow-y: auto;
}
.tag-sidebar h3 {
font-size: 15px; font-weight: 800; margin-bottom: 10px;
color: var(--primary); display: flex; align-items: center; gap: 6px;
}
.tag-category {
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid #f0ece6;
}
.tag-category:last-child { border-bottom: none; }
.tag-cat-header {
font-size: 13px; font-weight: 700; margin-bottom: 8px;
padding: 6px 8px; border-radius: 8px;
display: flex; align-items: center; gap: 6px;
cursor: pointer; user-select: none;
transition: background 0.15s;
}
.tag-cat-header:hover { background: #f5f5f5; }
.tag-cat-header .arrow { transition: transform 0.2s; font-size: 10px; margin-right: 2px; }
.tag-cat-header.collapsed .arrow { transform: rotate(-90deg); }
.tag-items { display: flex; flex-wrap: wrap; gap: 6px; padding-left: 4px; }
.tag-cat-header.collapsed + .tag-items { display: none; }
.tag-chip {
display: inline-block; padding: 5px 12px; border-radius: 14px;
font-size: 13px; cursor: pointer; transition: all 0.15s;
border: 2px solid transparent; white-space: nowrap;
user-select: none; line-height: 1.4;
}
.tag-chip:hover { transform: translateY(-2px); box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
.tag-chip:active { transform: scale(0.95); }
.tag-chip.selected { border-color: currentColor; box-shadow: 0 2px 8px rgba(0,0,0,0.18); font-weight: 600; }
.tag-cat-creative .tag-chip { background: #ebf5ff; color: #2563eb; }
.tag-cat-creative .tag-chip.selected { background: #dbeafe; }
.tag-cat-skill .tag-chip { background: #ecfdf5; color: #059669; }
.tag-cat-skill .tag-chip.selected { background: #d1fae5; }
.tag-cat-attitude .tag-chip { background: #fefce8; color: #ca8a04; }
.tag-cat-attitude .tag-chip.selected { background: #fef08a; }
.tag-cat-thinking .tag-chip { background: #f0f9ff; color: #0891b2; }
.tag-cat-thinking .tag-chip.selected { background: #cffafe; }
.tag-cat-state .tag-chip { background: #fff7ed; color: #ea580c; }
.tag-cat-state .tag-chip.selected { background: #fed7aa; }
.tag-cat-resilience .tag-chip { background: #fdf2f8; color: #db2777; }
.tag-cat-resilience .tag-chip.selected { background: #fce7f3; }
.tag-cat-social .tag-chip { background: #eef2ff; color: #4f46e5; }
.tag-cat-social .tag-chip.selected { background: #e0e7ff; }
.tag-cat-issue .tag-chip { background: #fef2f2; color: #dc2626; }
.tag-cat-issue .tag-chip.selected { background: #fee2e2; }
.tag-cat-suggest .tag-chip { background: #faf5ff; color: #7c3aed; }
.tag-cat-suggest .tag-chip.selected { background: #ede9fe; }
.tag-reset { font-size: 11px; color: var(--muted); cursor: pointer; float: right; background: none; border: none; }
.tag-reset:hover { color: var(--danger); }
/* 标签目标提示 */
.tag-target-hint {
font-size: 11px; color: var(--muted); margin-top: 4px;
display: flex; align-items: center; gap: 4px;
}
.tag-target-indicator {
font-size: 10px; padding: 1px 6px; border-radius: 8px;
background: #f0ece6; cursor: pointer; font-weight: 600;
}
.tag-target-indicator.active { background: var(--primary); color: white; }
/* ==================== 右侧表单 ==================== */
.form-panel { flex: 1; min-width: 0; }
/* ==================== 操作按钮 ==================== */
.actions {
display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap;
}
.btn-primary {
flex: 1; min-width: 160px; padding: 14px 24px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white; border: none; border-radius: 12px;
font-size: 15px; font-weight: 800; cursor: pointer;
transition: all 0.2s; font-family: inherit; letter-spacing: 0.5px;
box-shadow: 0 4px 14px rgba(45,90,61,0.35);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(45,90,61,0.45); }
.btn-primary:disabled { opacity: 0.6; cursor: wait; }
.btn-ai {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
box-shadow: 0 4px 14px rgba(99,102,241,0.4);
}
.btn-ai:hover { box-shadow: 0 6px 20px rgba(99,102,241,0.5); }
.btn-secondary {
padding: 14px 20px;
background: var(--card); color: var(--primary);
border: 2px solid var(--border); border-radius: 12px;
font-size: 14px; font-weight: 600; cursor: pointer;
font-family: inherit; transition: all 0.2s;
}
.btn-secondary:hover { border-color: var(--primary); background: #faf8f5; }
/* ==================== 输出区域 ==================== */
.output {
display: none; margin-top: 16px;
background: var(--primary-dark); border-radius: 14px; padding: 18px;
}
.output.show { display: block; }
.output h3 { color: white; font-size: 14px; font-weight: 600; margin-bottom: 8px; opacity: 0.9; }
.output-content {
background: #0f2519; border-radius: 8px; padding: 14px;
color: #b8ffcc; font-family: 'Fira Code','Cascadia Code',Consolas,monospace;
font-size: 13px; line-height: 1.7; white-space: pre-wrap;
word-break: break-all; max-height: 350px; overflow-y: auto;
}
.copy-btn {
margin-top: 10px; padding: 8px 20px;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px; color: white; font-size: 13px;
cursor: pointer; font-family: inherit; transition: all 0.2s;
}
.copy-btn:hover { background: rgba(255,255,255,0.2); }
.copy-btn.copied { background: var(--accent); border-color: var(--accent); }
.save-hint { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 6px; }
/* ==================== 提示消息 ==================== */
.toast {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
padding: 14px 28px; border-radius: 12px; color: white;
font-weight: 600; z-index: 1000; display: none;
animation: fadeIn 0.3s; font-size: 14px;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.toast.success { background: var(--accent); }
.toast.error { background: var(--danger); }
.toast.info { background: #6366f1; }
@keyframes fadeIn { from{opacity:0;transform:translateX(-50%) translateY(-10px)} to{opacity:1;transform:translateX(-50%) translateY(0)} }
.empty {
background: var(--card); border-radius: 14px; padding: 32px 16px;
text-align: center; color: var(--muted);
}
/* ==================== 响应式 ==================== */
@media (max-width: 920px) {
.main-layout { flex-direction: column; }
.tag-sidebar {
flex: none; position: static; max-height: none;
width: 100%; max-height: 300px;
}
.header { padding: 16px; }
.header h1 { font-size: 18px; }
.ai-config-bar { gap: 6px; }
.ai-config-bar input { min-width: 120px; }
.student-trait { display: none; }
.actions { flex-direction: column; }
}
@media (max-width: 600px) {
body { padding: 8px; }
.header { padding: 14px 12px; border-radius: 12px; }
.student-card { padding: 10px 12px; border-radius: 10px; }
}
/* 滚动条美化 */
.tag-sidebar::-webkit-scrollbar { width: 4px; }
.tag-sidebar::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
</style>
</head>
<body>
<div class="toast" id="toast"></div>
<div class="container">
<!-- ========== 头部 ========== -->
<div class="header">
<h1>小Q老师 · 课评系统</h1>
<div class="sub">2026春季学期 | Form-First模式 | v6.0</div>
</div>
<!-- ========== AI 提供商配置面板 ========== -->
<div class="ai-config-bar" id="aiConfigBar">
<span class="status-dot" id="aiStatusDot" title="未配置"></span>
<label>提供商</label>
<select id="aiProvider" onchange="onAiProviderChange()" style="min-width:100px;">
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="kimi">Kimi (月之暗面)</option>
<option value="custom">自定义</option>
</select>
<label>API地址</label>
<input id="aiApiUrl" placeholder="https://api.deepseek.com/v1" onchange="saveAiConfig()" />
<label>Key</label>
<input id="aiApiKey" type="password" placeholder="输入API Key..." onchange="saveAiConfig()" />
<label>模型</label>
<input id="aiModel" class="model-input" placeholder="deepseek-chat" onchange="saveAiConfig()" />
<button class="toggle-btn" id="aiConfigToggle" onclick="toggleAiConfig()">🔒 隐藏Key</button>
<button class="test-btn" onclick="testAiConnection()">测试连接</button>
</div>
<!-- ========== 快速跳转 ========== -->
<div class="quick-jump">
<input type="text" id="quickInput" placeholder="输入 /周六 16点 快速跳转..." onkeydown="handleQuickJump(event)" />
<button onclick="doQuickJump()">跳转</button>
</div>
<!-- ========== 主布局:标签库 + 表单 ========== -->
<div class="main-layout">
<!-- ===== 左侧:标签库 ===== -->
<div class="tag-sidebar" id="tagSidebar">
<h3>🏷️ 课堂观察标签</h3>
<div class="tag-target-hint">
标签目标:
<span class="tag-target-indicator" id="tagTargetLabel" onclick="cycleTagTarget()">当前学生</span>
<span style="font-size:10px;color:#aaa;">(点击切换)</span>
</div>
<div id="tagLibrary"></div>
<button class="tag-reset" onclick="resetAllTags()" style="margin-top:8px;">🔄 重置所有标签</button>
</div>
<!-- ===== 右侧:表单面板 ===== -->
<div class="form-panel">
<!-- 选择器 -->
<div class="selector-panel">
<div class="selector-group">
<div class="selector-label">1. 选择周数</div>
<select class="selector-select" id="weekSelect" onchange="onWeekChange()">
<option value="">-- 请选择 --</option>
</select>
</div>
<div class="selector-group">
<div class="selector-label">2. 选择周几</div>
<select class="selector-select" id="weekdaySelect" disabled onchange="onWeekdayChange()">
<option value="">-- 先选周数 --</option>
</select>
</div>
<div class="selector-group">
<div class="selector-label">3. 选择班级</div>
<select class="selector-select" id="classSelect" disabled onchange="onClassChange()">
<option value="">-- 先选周几 --</option>
</select>
</div>
<div class="selector-group" id="customClassGroup" style="display:none;">
<div class="selector-label">输入班级名称</div>
<input type="text" class="selector-select" id="customClassInput" placeholder="如:体验班-张三" style="background:#faf8f5;" />
</div>
<div class="selector-group" id="customClassTypeGroup" style="display:none;">
<div class="selector-label">班级类型</div>
<select class="selector-select" id="customClassType">
<option value="DISC">发现世界 (DISC)</option>
<option value="CREATE">Wedo创造世界 (CREATE)</option>
<option value="SPIKE">SPIKE (SPIKE)</option>
<option value="AICODE03">AICODE03 (AICODE03)</option>
<option value="CUSTOM">自定义课程</option>
</select>
</div>
<div class="date-display" id="dateDisplay">📅</div>
</div>
<!-- 课程信息 -->
<div class="course-panel" id="coursePanel">
<div class="course-info">
<div class="course-item">
<div class="course-item-label">课程主题</div>
<div class="course-item-value" id="courseTheme">-</div>
</div>
<div class="course-item">
<div class="course-item-label">课程代码</div>
<div class="course-item-value" id="courseCode">-</div>
</div>
<div class="course-item">
<div class="course-item-label">核心知识点</div>
<div class="course-item-value" id="courseKnowledge">-</div>
</div>
</div>
<div class="custom-course" id="customCourseArea">
<input type="text" class="custom-input" id="customTheme" placeholder="输入课程主题(如"蜥蜴"" />
<input type="text" class="custom-input" id="customKnowledge" placeholder="输入核心知识点(可选)" />
</div>
</div>
<!-- 学生卡片容器 -->
<div id="cardsContainer"></div>
<!-- 速记参考 -->
<div class="shorthand">
<strong>📌 速记符号参考:</strong><br />
<code>gj#</code> 观望#分钟 ·
<code>zd#</code> 主动搭#层 ·
<code>zt</code> 自己调整 ·
<code>zz#</code> 专注#分钟 ·
<code>bz</code> 帮助同学 ·
<code>tw</code> 提问 ·
<code>wc</code> 完成 ·
<code>cx</code> 创新 ·
<code></code> 开心 ·
<code></code> 稳定 ·
<code></code> 低落 ·
<code>++</code> 比上周进步
</div>
<!-- 补课/体验学生 -->
<div class="makeup-card" id="makeupCard">
<div class="makeup-header">
<span class="makeup-icon">🔄</span>
<span class="makeup-title">补课/体验学生</span>
</div>
<div id="makeupContainer">
<div class="makeup-entry" id="makeup-0">
<div class="makeup-row">
<input class="makeup-input" id="mname-0" placeholder="输入补课学生姓名" />
<div class="makeup-status-btns">
<button class="btn-small makeup" id="mstatus-makeup-0" onclick="setMakeupStatus(0,'makeup')">补课</button>
<button class="btn-small trial" id="mstatus-trial-0" onclick="setMakeupStatus(0,'trial')" style="opacity:0.5;">体验</button>
<button class="btn-small btn-remove" onclick="removeMakeup(0)"></button>
</div>
</div>
<textarea class="textarea" id="mdesc-0" placeholder="输入补课/体验学生的表现..." oninput="updateProgress()" onclick="setMakeupTagTarget(0)"></textarea>
</div>
</div>
<button class="btn-add" onclick="addMakeup()"> 添加补课/体验学生</button>
</div>
<!-- 底部按钮 -->
<div class="actions">
<button class="btn-primary" id="genFormBtn" onclick="generate()">📋 生成表单输出</button>
<button class="btn-primary btn-ai" id="genAiBtn" onclick="generateWithAI()">🤖 AI一键生成课评</button>
<button class="btn-secondary" onclick="clearAll()">🗑️ 清空</button>
</div>
<!-- 输出区域 -->
<div class="output" id="output">
<h3 id="outputTitle">⬇️ 输出结果复制保存或交给Claude生成课评</h3>
<div class="output-content" id="outputContent"></div>
<button class="copy-btn" id="copyBtn" onclick="copyOutput()">📋 复制到剪贴板</button>
<div class="save-hint">💡 将内容粘贴给Claude使用keping-advanced技能生成并自动保存课评</div>
</div>
</div>
</div>
</div>
<script src=".claude/memory/config/class-data.js?v=20260527"></script>
<script>
// ===============================================
// 状态管理
// ===============================================
let selectedWeek = '';
let currentWeekday = '';
let currentClass = null;
let currentWeek = 1;
let students = [];
let statuses = [];
let makeupCounter = 1;
let makeupData = [];
let tagTargetIndex = 0; // 标签填充到哪个学生0=第1个学生
let selectedTags = []; // 当前选中标签数组
let tagTargetMode = 'student'; // 'student' 或 'makeup' — 标签填充目标模式
let tagTargetMakeupIdx = 0; // 补课/体验学生的索引
// ===============================================
// 周数选项填充
// ===============================================
function fillWeekOptions() {
const sel = document.getElementById('weekSelect');
for (let w=1; w<=21; w++) {
const opt = document.createElement('option');
opt.value = String(w); opt.textContent = `${w}`;
sel.appendChild(opt);
}
}
// ===============================================
// 日期计算
// ===============================================
function getDateForWeekAndWeekday(weekNum, weekday) {
const start = new Date('2026-03-02');
const weekMap = {'周一':1,'周二':2,'周三':3,'周四':4,'周五':5,'周六':6,'周日':0};
let targetDay = weekMap[weekday];
const weekStart = new Date(start);
weekStart.setDate(start.getDate()+(weekNum-1)*7);
const diff = (targetDay-weekStart.getDay()+7)%7;
const result = new Date(weekStart);
result.setDate(weekStart.getDate()+diff);
return result;
}
function formatDate(date) {
const y=date.getFullYear(), m=String(date.getMonth()+1).padStart(2,'0'), d=String(date.getDate()).padStart(2,'0');
return `${y}-${m}-${d}`;
}
function formatDateShort(date) {
return `${date.getMonth()+1}${date.getDate()}`;
}
// ===============================================
// 选择器事件
// ===============================================
function onWeekChange() {
const weekNum = document.getElementById('weekSelect').value;
selectedWeek = weekNum;
const ws = document.getElementById('weekdaySelect');
ws.innerHTML = '<option value="">-- 请选择 --</option>';
ws.disabled = true;
const cs = document.getElementById('classSelect');
cs.innerHTML = '<option value="">-- 先选周几 --</option>';
cs.disabled = true;
document.getElementById('dateDisplay').textContent = '📅';
document.getElementById('coursePanel').classList.remove('show');
document.getElementById('cardsContainer').innerHTML = '';
if (!weekNum) return;
['周一','周二','周三','周四','周五','周六','周日'].forEach(wd => {
const opt = document.createElement('option'); opt.value=wd; opt.textContent=wd; ws.appendChild(opt);
});
ws.disabled = false;
}
function onWeekdayChange() {
const weekday = document.getElementById('weekdaySelect').value;
currentWeekday = weekday;
const cs = document.getElementById('classSelect');
cs.innerHTML = '<option value="">-- 请选择班级 --</option>';
document.getElementById('customClassGroup').style.display='none';
document.getElementById('customClassTypeGroup').style.display='none';
if (!weekday) { cs.disabled=true; resetDisplay(); return; }
const date = getDateForWeekAndWeekday(parseInt(selectedWeek), weekday);
currentWeek = parseInt(selectedWeek);
document.getElementById('dateDisplay').textContent = `📅 ${formatDateShort(date)} · 第${selectedWeek}`;
const filtered = CONFIG.classes.filter(c=>c.weekday===weekday);
filtered.forEach(c=>{const o=document.createElement('option');o.value=c.id;o.textContent=c.name;cs.appendChild(o);});
['───────────','🌟 体验班级','🔄 补课班级',' 自定义新班级'].forEach((label,i)=>{
const o=document.createElement('option');
if(i===0){o.disabled=true;o.textContent=label;}
else{o.value=['__TRIAL__','__MAKEUP__','__CUSTOM__'][i-1];o.textContent=label;}
cs.appendChild(o);
});
cs.disabled=false;
resetDisplay();
}
function resetDisplay() {
document.getElementById('coursePanel').classList.remove('show');
document.getElementById('cardsContainer').innerHTML='';
document.getElementById('output').classList.remove('show');
selectedTags = [];
renderTagLibrary();
}
function onClassChange() {
const classId = document.getElementById('classSelect').value;
if (classId==='__TRIAL__'||classId==='__MAKEUP__'||classId==='__CUSTOM__') {
document.getElementById('customClassGroup').style.display='block';
document.getElementById('customClassTypeGroup').style.display='block';
const ci=document.getElementById('customClassInput');
const labels={__TRIAL__:'体验班',__MAKEUP__:'补课班',__CUSTOM__:'自定义班'};
ci.placeholder=`如:${labels[classId]}-张三`;
ci.value=''; document.getElementById('customTheme').value=''; document.getElementById('customKnowledge').value='';
document.getElementById('coursePanel').classList.remove('show');
document.getElementById('cardsContainer').innerHTML='';
ci.oninput=handleCustomClass;
document.getElementById('customClassType').onchange=handleCustomClass;
setTimeout(handleCustomClass,0);
return;
}
document.getElementById('customClassGroup').style.display='none';
document.getElementById('customClassTypeGroup').style.display='none';
currentClass = CONFIG.classes.find(c=>c.id===classId);
if(!currentClass){document.getElementById('coursePanel').classList.remove('show');document.getElementById('cardsContainer').innerHTML='';return;}
const courseType=currentClass.courseType;
const cd=CONFIG.courses[courseType]?.[currentWeek];
const theme=cd?.theme||'待定';
const code=cd?.code||`${currentClass.coursePrefix}-${String(currentWeek).padStart(2,'0')}`;
const knowledge=cd?.knowledge||'';
document.getElementById('courseTheme').textContent=theme;
document.getElementById('courseCode').textContent=code;
document.getElementById('courseKnowledge').textContent=knowledge;
const ca=document.getElementById('customCourseArea');
ca.classList.toggle('show',!cd||theme==='待定');
document.getElementById('coursePanel').classList.add('show');
selectedTags=[];
renderStudents();
renderTagLibrary();
}
function handleCustomClass() {
const ci=document.getElementById('customClassInput');
let cn=ci.value.trim();
const ct=document.getElementById('customClassType').value;
const clv=document.getElementById('classSelect').value;
let tp='新班级';
if(clv==='__TRIAL__')tp='体验班';if(clv==='__MAKEUP__')tp='补课班';if(clv==='__CUSTOM__')tp='自定义班';
if(!cn)cn=tp;
currentClass={id:`${tp}-${cn}`,name:`${tp}-${cn}`,courseType:ct,coursePrefix:ct==='CUSTOM'?'CUSTOM':ct,weekday:currentWeekday,time:'',students:[],isCustom:true};
const cd=ct==='CUSTOM'?null:CONFIG.courses[ct]?.[currentWeek];
document.getElementById('courseTheme').textContent=cd?.theme||'待定';
document.getElementById('courseCode').textContent=cd?.code||`${currentClass.coursePrefix}-${String(currentWeek).padStart(2,'0')}`;
document.getElementById('courseKnowledge').textContent=cd?.knowledge||'';
document.getElementById('customCourseArea').classList.add('show');
const cti=document.getElementById('customTheme'), cki=document.getElementById('customKnowledge');
if(ct==='CUSTOM'){cti.value='';cki.value='';}
else if(!cti.value&&cd&&cd.theme&&cd.theme!=='待定'){cti.value=cd.theme;cki.value=cd.knowledge||'';}
document.getElementById('coursePanel').classList.add('show');
selectedTags=[];
renderCustomStudents();
renderTagLibrary();
}
// ===============================================
// 学生渲染
// ===============================================
function renderStudents() {
students=[...currentClass.students];
statuses=students.map(()=>'present');
tagTargetIndex=0;
const c=document.getElementById('cardsContainer');
c.innerHTML=students.map((s,i)=>`
<div class="student-card" id="card-${i}">
<div class="student-top">
<div class="student-avatar" style="background:${s.color}">${s.emoji}</div>
<div class="student-name">${s.name}</div>
<div class="student-trait">${s.trait}</div>
</div>
<div class="status-buttons">
<button class="status-btn active-present" id="present-${i}" onclick="setStatus(${i},'present');setTagTarget(${i})">✅ 出勤</button>
<button class="status-btn" id="leave-${i}" onclick="setStatus(${i},'leave')">❌ 请假</button>
</div>
<textarea class="textarea" id="input-${i}" placeholder="输入速记,如 gj3→zd2→zt ↑++ \n或输入详细描述...\n💡 也可点击左侧标签快速填充" oninput="onInput(${i})" onclick="setTagTarget(${i})"></textarea>
</div>
`).join('');
updateProgress();
renderTagLibrary();
}
function renderCustomStudents() {
const c=document.getElementById('cardsContainer');
c.innerHTML=`
<div class="student-card" id="custom-students-card">
<div class="student-top">
<div class="student-avatar" style="background:#e8f5e9">👥</div>
<div class="student-name">自定义学生列表</div>
<div class="student-trait">请输入学生姓名,每行一个</div>
</div>
<textarea class="textarea" id="customStudentsInput" placeholder="输入学生姓名,每行一个,如:\n张三\n李四\n王五" oninput="updateCustomStudents()"></textarea>
</div>
<div id="customStudentCards"></div>
`;
updateProgress();
}
function updateCustomStudents() {
const inp=document.getElementById('customStudentsInput');
if(!inp)return;
const names=inp.value.trim().split('\n').filter(n=>n.trim());
students=names.map((n,i)=>({name:n.trim(),emoji:i%2===0?'👧':'🧒',color:['#e8f5e9','#e3f2fd','#fff3e0','#fce4ec','#f3e5f5'][i%5],trait:'待观察'}));
statuses=students.map(()=>'present');
tagTargetIndex=0;
const cc=document.getElementById('customStudentCards');
if(cc)cc.innerHTML=students.map((s,i)=>`
<div class="student-card" id="card-${i}">
<div class="student-top">
<div class="student-avatar" style="background:${s.color}">${s.emoji}</div>
<div class="student-name">${s.name}</div>
<div class="student-trait">${s.trait}</div>
</div>
<div class="status-buttons">
<button class="status-btn active-present" id="present-${i}" onclick="setStatus(${i},'present');setTagTarget(${i})">✅ 出勤</button>
<button class="status-btn" id="leave-${i}" onclick="setStatus(${i},'leave')">❌ 请假</button>
</div>
<textarea class="textarea" id="input-${i}" placeholder="输入速记或描述...\n💡 也可点击左侧标签快速填充" oninput="onInput(${i})" onclick="setTagTarget(${i})"></textarea>
</div>
`).join('');
updateProgress();
renderTagLibrary();
}
function setStatus(i,status) {
statuses[i]=status;
const pre=document.getElementById(`present-${i}`),lea=document.getElementById(`leave-${i}`),card=document.getElementById(`card-${i}`);
pre.classList.toggle('active-present',status==='present');
lea.classList.toggle('active-leave',status==='leave');
card.classList.toggle('absent',status==='leave');
updateProgress();
}
function onInput(i) {
const ta=document.getElementById(`input-${i}`);
ta.classList.toggle('filled',ta.value.trim().length>0);
updateProgress();
}
// ===============================================
// 补课学生
// ===============================================
function addMakeup() {
const idx=makeupCounter++;
const mc=document.getElementById('makeupContainer');
const div=document.createElement('div');
div.className='makeup-entry'; div.id=`makeup-${idx}`;
div.innerHTML=`
<div class="makeup-row">
<input class="makeup-input" id="mname-${idx}" placeholder="输入补课学生姓名" />
<div class="makeup-status-btns">
<button class="btn-small makeup" id="mstatus-makeup-${idx}" onclick="setMakeupStatus(${idx},'makeup')">补课</button>
<button class="btn-small trial" id="mstatus-trial-${idx}" onclick="setMakeupStatus(${idx},'trial')" style="opacity:0.5;">体验</button>
<button class="btn-small btn-remove" onclick="removeMakeup(${idx})">✕</button>
</div>
</div>
<textarea class="textarea" id="mdesc-${idx}" placeholder="输入补课/体验学生的表现..." oninput="updateProgress()" onclick="setMakeupTagTarget(${idx})"></textarea>
`;
mc.appendChild(div);
updateProgress();
}
function removeMakeup(idx) {
const el=document.getElementById(`makeup-${idx}`);
if(el){el.remove();updateProgress();}
}
function setMakeupStatus(idx,type) {
const mb=document.getElementById(`mstatus-makeup-${idx}`),tb=document.getElementById(`mstatus-trial-${idx}`);
if(!mb||!tb)return;
mb.style.opacity=type==='makeup'?'1':'0.5';
tb.style.opacity=type==='makeup'?'0.5':'1';
}
function collectMakeupData() {
const entries=document.querySelectorAll('.makeup-entry');
const data=[];
entries.forEach(entry=>{
const idx=entry.id.replace('makeup-','');
const ni=document.getElementById(`mname-${idx}`), di=document.getElementById(`mdesc-${idx}`);
const mb=document.getElementById(`mstatus-makeup-${idx}`);
if(!ni||!di)return;
const name=ni.value.trim(),desc=di.value.trim();
const isMakeup=mb&&mb.style.opacity==='1';
if(name)data.push({name,desc,type:isMakeup?'补课':'体验'});
});
return data;
}
function updateProgress() {
let filled=0,total=students.length;
students.forEach((_,i)=>{
const inp=document.getElementById(`input-${i}`);
if(inp){const v=inp.value.trim();if(v.length>0||statuses[i]==='leave')filled++;}
});
const makeup=collectMakeupData();
makeup.forEach(m=>{if(m.name&&m.desc){filled++;total++;}else if(m.name){total++;}});
document.getElementById('filledCount').textContent=filled;
document.getElementById('totalCount').textContent=total;
const pct=total>0?(filled/total)*100:0;
document.getElementById('progressFill').style.width=pct+'%';
const ph=document.getElementById('pendingHint'),p=total-filled;
ph.textContent=p>0?`⚠️ ${p} 人待填写`:'✅ 全部完成';
ph.style.color=p>0?'#e74c3c':'#4a8c5c';
}
// ===============================================
// AI 配置管理
// ===============================================
const AI_PROVIDERS = {
deepseek: { url: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat' },
openai: { url: 'https://api.openai.com/v1/chat/completions', model: 'gpt-4o' },
kimi: { url: 'https://api.moonshot.cn/v1/chat/completions', model: 'moonshot-v1-8k' },
custom: { url: '', model: '' }
};
function loadAiConfig() {
const cfg=JSON.parse(localStorage.getItem('keping_ai_config')||'{}');
document.getElementById('aiProvider').value=cfg.provider||'deepseek';
document.getElementById('aiApiUrl').value=cfg.apiUrl||AI_PROVIDERS.deepseek.url;
document.getElementById('aiApiKey').value=cfg.apiKey||'';
document.getElementById('aiModel').value=cfg.model||AI_PROVIDERS.deepseek.model;
updateAiStatusDot();
}
function saveAiConfig() {
const cfg={provider:document.getElementById('aiProvider').value,apiUrl:document.getElementById('aiApiUrl').value,apiKey:document.getElementById('aiApiKey').value,model:document.getElementById('aiModel').value};
localStorage.setItem('keping_ai_config',JSON.stringify(cfg));
updateAiStatusDot();
}
function onAiProviderChange() {
const p=document.getElementById('aiProvider').value;
const prov=AI_PROVIDERS[p];
document.getElementById('aiApiUrl').value=prov.url||'';
document.getElementById('aiModel').value=prov.model||'';
if(p==='custom'){document.getElementById('aiApiUrl').placeholder='输入自定义API地址';document.getElementById('aiModel').placeholder='输入模型名称';}
saveAiConfig();
}
function updateAiStatusDot() {
const key=document.getElementById('aiApiKey').value;
const dot=document.getElementById('aiStatusDot');
dot.className='status-dot '+(key?'ok':'err');
dot.title=key?'API Key 已配置':'未配置 API Key';
}
function toggleAiConfig() {
const keyInput=document.getElementById('aiApiKey');
const btn=document.getElementById('aiConfigToggle');
if(keyInput.type==='password'){keyInput.type='text';btn.textContent='🔒 隐藏Key';}
else{keyInput.type='password';btn.textContent='👁️ 显示Key';}
}
async function testAiConnection() {
const key=document.getElementById('aiApiKey').value.trim();
const url=document.getElementById('aiApiUrl').value.trim();
const model=document.getElementById('aiModel').value.trim();
if(!key){showToast('请先填写 API Key','error');return;}
if(!url){showToast('请先填写 API 地址','error');return;}
showToast('正在测试连接...','info');
try {
const resp=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json','Authorization':`Bearer ${key}`},body:JSON.stringify({model:model||'deepseek-chat',messages:[{role:'user',content:'测试'}],max_tokens:10})});
if(resp.ok){showToast('✅ 连接成功!','success');document.getElementById('aiStatusDot').className='status-dot ok';}
else{const err=await resp.json();showToast('❌ 连接失败: '+(err.error?.message||resp.status),'error');document.getElementById('aiStatusDot').className='status-dot err';}
}catch(e){showToast('❌ 网络错误: '+e.message,'error');document.getElementById('aiStatusDot').className='status-dot err';}
}
// ===============================================
// Toast 提示
// ===============================================
function showToast(msg, type) {
const t=document.getElementById('toast');
t.textContent=msg; t.className='toast '+type; t.style.display='block';
setTimeout(()=>{t.style.display='none';},3000);
}
// ===============================================
// 标签库
// ===============================================
const TAG_LIBRARY = [
{name:'💡 创意设计', colorClass:'tag-cat-creative',tags:['结构有创意','功能设计新颖','造型独特','构思巧妙','有想象力','不拘泥于示范','加入个性化装饰','功能性创新突出','整体美感强','用AI激发创意','AI辅助构思新颖']},
{name:'🛠️ 搭建技能', colorClass:'tag-cat-skill',tags:['结构稳固','零件搭配合理','传动精准','齿轮啮合良好','底盘设计合理','电机安装规范','传感器使用恰当','连接件使用熟练','整体结构紧凑','机械原理理解透彻','互锁结构掌握熟练','管道连接稳固','螺丝刀使用灵活','杠杆原理应用恰当','重心调整合理','结构对称性好','力传导理解清晰','结构加固方法得当','直角传动掌握良好','惰轮使用正确','轴固定牢固不松动','滑轮应用恰当','减速结构理解透彻','加速结构运用熟练','连贯搭建动作流畅','作品布局合理美观','手眼协调能力强','空间建构能力好','颜色搭配美观','尺寸比例协调','造型还原度高','齿轮变速理解到位','履带传动运用灵活','铰链结构运用巧妙','复式搭建技巧熟练']},
{name:'📋 学习态度', colorClass:'tag-cat-attitude',tags:['认真专注','积极主动','乐于尝试','精益求精','遵守课堂规则','高效率完成任务','主动整理零件','有责任心','对AI学习热情高','主动探索新功能','粗心大意','敷衍了事','注意力不集中']},
{name:'🧠 思维特点', colorClass:'tag-cat-thinking',tags:['逻辑清晰','独立思考','善于分析','举一反三','空间想象力强','善于规划步骤','编程思维较好','计算思维突出','能拆解复杂问题','善于发现规律','思路混乱','需要引导','喜欢模仿']},
{name:'🔥 课堂状态', colorClass:'tag-cat-state',tags:['专注度高','沉浸搭建','踊跃发言','高效完成任务','积极参与讨论','动手速度快','课堂纪律好','积极体验AI工具','乐于分享AI发现','小动作较多','容易分心','需要提醒']},
{name:'💪 情绪韧性', colorClass:'tag-cat-resilience',tags:['抗挫力强','耐心调试','不轻易放弃','敢于面对失败','情绪稳定','心态积极','AI出错不慌张','反复尝试优化','容易焦虑','急于求成','遇挫容易放弃']},
{name:'🤝 合作沟通', colorClass:'tag-cat-social',tags:['乐于分享','帮助同学','善于表达','沟通顺畅','团队协作意识强','能与同伴讨论方案','会向AI清晰提问','能用语言描述创作思路','独占材料','不愿交流']},
{name:'⚠️ 待优化问题', colorClass:'tag-cat-issue',tags:['结构松散','程序逻辑有误','不够牢固','功能不稳定','完成度有待提高','零件脱落频繁','过度依赖AI生成','提示词不够清晰','不检查AI输出结果','不知如何修改AI作品','缺乏调试耐心']},
{name:'🚀 下节课建议', colorClass:'tag-cat-suggest',tags:['加强结构稳定性','优化程序逻辑','尝试新功能','挑战更高难度','关注底盘设计','改进传动系统','增加传感器反馈','练习精准提问技巧','尝试AI独立创作','学习调整AI参数','多与同学交流分享']},
{name:'🤖 AI素养', colorClass:'tag-cat-skill',tags:['能理解AI基本原理','知道AI会出错需验证','善用AI辅助学习','会修改优化AI产出','能比较AI与自己创作的差异','了解AI工具的优势与局限','主动学习AI新功能','能表达对AI的疑问','合理使用AI不盲目依赖','尊重原创和版权意识']}
];
function renderTagLibrary() {
const container=document.getElementById('tagLibrary');
let html='';
TAG_LIBRARY.forEach((cat,ci)=>{
const collapsed=false;
html+=`<div class="tag-category ${cat.colorClass}">`;
html+=`<div class="tag-cat-header" onclick="toggleCategory(this)" title="点击折叠/展开"><span class="arrow">▼</span> ${cat.name} (${cat.tags.length})</div>`;
html+=`<div class="tag-items">`;
cat.tags.forEach(tag=>{
const sel=selectedTags.includes(tag)?' selected':'';
html+=`<span class="tag-chip${sel}" onclick="toggleTag('${tag.replace(/'/g,"\\'")}')">${tag}</span>`;
});
html+=`</div></div>`;
});
container.innerHTML=html;
updateTagTargetIndicator();
}
function toggleCategory(header) {
header.classList.toggle('collapsed');
}
function toggleTag(tag) {
const idx=selectedTags.indexOf(tag);
if(idx!==-1){selectedTags.splice(idx,1);}
else{selectedTags.push(tag);}
renderTagLibrary();
applyTagsToStudent();
}
function resetAllTags() {
selectedTags=[];
renderTagLibrary();
// 不调用 applyTagsToStudent(),保留右侧学生框内已有【标签观察】内容
}
function setTagTarget(i) {
tagTargetIndex=i;
tagTargetMode='student';
updateTagTargetIndicator();
}
function setMakeupTagTarget(idx) {
tagTargetMode='makeup';
tagTargetMakeupIdx=idx;
updateTagTargetIndicator();
}
function updateTagTargetIndicator() {
const label=document.getElementById('tagTargetLabel');
if(tagTargetMode==='makeup'){
const input=document.getElementById(`mname-${tagTargetMakeupIdx}`);
const name=input?input.value.trim()||'补课/体验学生':'补课/体验学生';
label.textContent=name;
label.className='tag-target-indicator active';
return;
}
if(students.length===0){label.textContent='无学生';label.className='tag-target-indicator';return;}
const s=students[tagTargetIndex];
if(!s){label.textContent='无学生';label.className='tag-target-indicator';return;}
label.textContent=s.name||'当前学生';
label.className='tag-target-indicator active';
}
function cycleTagTarget() {
if(tagTargetMode==='makeup'){
// 从补课模式切回学生模式
tagTargetMode='student';
if(tagTargetIndex>=students.length)tagTargetIndex=0;
updateTagTargetIndicator();
const card=document.getElementById(`card-${tagTargetIndex}`);
if(card)card.scrollIntoView({behavior:'smooth',block:'center'});
return;
}
// 先收集有填写姓名的补课/体验条目
const makeups=[];
document.querySelectorAll('.makeup-entry').forEach(entry=>{
const idx=entry.id.replace('makeup-','');
const ni=document.getElementById(`mname-${idx}`);
if(ni&&ni.value.trim())makeups.push(parseInt(idx));
});
// 若当前是最后一个学生且有补课条目,切到补课模式
if(tagTargetIndex>=students.length-1&&makeups.length>0){
tagTargetMode='makeup';
tagTargetMakeupIdx=makeups[0];
updateTagTargetIndicator();
const el=document.getElementById(`makeup-${tagTargetMakeupIdx}`);
if(el)el.scrollIntoView({behavior:'smooth',block:'center'});
return;
}
// 否则继续循环学生
if(students.length===0)return;
tagTargetIndex=(tagTargetIndex+1)%students.length;
updateTagTargetIndicator();
const card=document.getElementById(`card-${tagTargetIndex}`);
if(card)card.scrollIntoView({behavior:'smooth',block:'center'});
}
function applyTagsToStudent() {
const tagText=selectedTags.length>0?('\n【标签观察】'+selectedTags.join('、')):'';
if(tagTargetMode==='makeup'){
const ta=document.getElementById(`mdesc-${tagTargetMakeupIdx}`);
if(!ta)return;
const existing=ta.value.replace(/【标签观察】[^]*/,'').trim();
ta.value=existing+tagText;
ta.classList.toggle('filled',ta.value.trim().length>0);
updateProgress();
return;
}
if(students.length===0)return;
const ta=document.getElementById(`input-${tagTargetIndex}`);
if(!ta)return;
// 保留已有的速记文字,在末尾追加标签文本
const existing=ta.value.replace(/【标签观察】[^]*/,'').trim();
ta.value=existing+tagText;
ta.classList.toggle('filled',ta.value.trim().length>0);
updateProgress();
}
// ===============================================
// 表单生成输出
// ===============================================
function generate() {
if(!currentClass){showToast('请先选择班级','error');return;}
const date=getDateForWeekAndWeekday(currentWeek,currentWeekday);
const dateStr=formatDate(date);
let theme=document.getElementById('courseTheme').textContent;
let knowledge=document.getElementById('courseKnowledge').textContent;
let code=document.getElementById('courseCode').textContent;
const ct=document.getElementById('customTheme').value.trim();
const ck=document.getElementById('customKnowledge').value.trim();
if(ct){theme=ct;knowledge=ck||'自定义课程';code=`${currentClass.coursePrefix}-${String(currentWeek).padStart(2,'0')}`;}
let output=`时间:${dateStr}\n班级:${currentClass.id}\n主题:${theme}\n`;
if(knowledge&&knowledge!=='-')output+=`目标/知识:${knowledge}\n`;
output+=`---\n课评\n---\n`;
students.forEach((s,i)=>{
const status=statuses[i];
const input=document.getElementById(`input-${i}`).value.trim();
if(status==='leave')output+=`${s.name}[请假]\n`;
else if(input)output+=`${s.name}${input}\n`;
else if(currentClass&&currentClass.isCustom)output+=`${s.name}\n`;
});
const makeup=collectMakeupData();
makeup.forEach(m=>{if(m.desc)output+=`${m.name}[${m.type}]${m.desc}\n`;else if(m.name)output+=`${m.name}[${m.type}]\n`;});
document.getElementById('outputContent').textContent=output;
document.getElementById('outputTitle').textContent='⬇️ 输出结果复制保存或交给Claude生成课评';
document.getElementById('output').classList.add('show');
document.getElementById('output').scrollIntoView({behavior:'smooth',block:'start'});
}
function copyOutput() {
const text=document.getElementById('outputContent').textContent;
navigator.clipboard.writeText(text).then(()=>{
const btn=document.getElementById('copyBtn');
btn.textContent='✅ 已复制'; btn.classList.add('copied');
setTimeout(()=>{btn.textContent='📋 复制到剪贴板';btn.classList.remove('copied');},2000);
});
}
function clearAll() {
if(!confirm('确定要清空所有内容吗?'))return;
const csi=document.getElementById('customStudentsInput');
if(csi){csi.value='';const csc=document.getElementById('customStudentCards');if(csc)csc.innerHTML='';}
students.forEach((_,i)=>{const el=document.getElementById(`input-${i}`);if(el){el.value='';el.classList.remove('filled');setStatus(i,'present');}});
document.getElementById('customClassInput').value='';
document.getElementById('customTheme').value='';
document.getElementById('customKnowledge').value='';
document.getElementById('makeupContainer').innerHTML=`
<div class="makeup-entry" id="makeup-0">
<div class="makeup-row">
<input class="makeup-input" id="mname-0" placeholder="输入补课学生姓名" />
<div class="makeup-status-btns">
<button class="btn-small makeup" id="mstatus-makeup-0" onclick="setMakeupStatus(0,'makeup')">补课</button>
<button class="btn-small trial" id="mstatus-trial-0" onclick="setMakeupStatus(0,'trial')" style="opacity:0.5;">体验</button>
<button class="btn-small btn-remove" onclick="removeMakeup(0)">✕</button>
</div>
</div>
<textarea class="textarea" id="mdesc-0" placeholder="输入补课/体验学生的表现..." oninput="updateProgress()" onclick="setMakeupTagTarget(0)"></textarea>
</div>
`;
makeupCounter=1;
document.getElementById('output').classList.remove('show');
selectedTags=[]; renderTagLibrary();
updateProgress();
}
// ===============================================
// AI 一键生成课评
// ===============================================
async function generateWithAI() {
const key=document.getElementById('aiApiKey').value.trim();
const url=document.getElementById('aiApiUrl').value.trim();
const model=document.getElementById('aiModel').value.trim();
if(!key){showToast('请先配置 AI API Key','error');document.getElementById('aiApiKey').focus();return;}
if(!url){showToast('请先配置 AI API 地址','error');return;}
if(!currentClass){showToast('请先选择班级','error');return;}
const btn=document.getElementById('genAiBtn');
const origText=btn.textContent;
btn.disabled=true; btn.textContent='⏳ AI生成中...';
// 收集数据
const date=getDateForWeekAndWeekday(currentWeek,currentWeekday);
const dateStr=formatDate(date);
let theme=document.getElementById('courseTheme').textContent;
let knowledge=document.getElementById('courseKnowledge').textContent;
let code=document.getElementById('courseCode').textContent;
const ct=document.getElementById('customTheme').value.trim();
const ck=document.getElementById('customKnowledge').value.trim();
if(ct){theme=ct;knowledge=ck||'自定义课程';code=`${currentClass.coursePrefix}-${String(currentWeek).padStart(2,'0')}`;}
// 构建学生表现描述
let studentRecords='';
students.forEach((s,i)=>{
if(statuses[i]==='leave')return;
const input=document.getElementById(`input-${i}`).value.trim();
if(input)studentRecords+=`${s.name}${input}\n`;
});
const makeup=collectMakeupData();
makeup.forEach(m=>{if(m.desc)studentRecords+=`${m.name}[${m.type}]${m.desc}\n`;});
if(!studentRecords.trim()){showToast('请先填写至少一位学生的表现','error');btn.disabled=false;btn.textContent=origText;return;}
// 构建 Prompt
const systemPrompt=`你是穹狼乐高编程教育的课评助手。请根据以下课堂观察记录,为每位学生生成个性化课评。
课评要求:
1. 约150-250字2-3自然段纯文本
2. 将课堂观察标签自然融入,不模板化
3. 有问题的话用温柔语气表达为"成长点"
4. 每句话结尾加匹配的 emoji
5. 结尾固定附上「【温馨提示】」段落(关于下节课准备建议)
6. 课程类型:${currentClass.courseType||'编程搭建'}类,主题为${theme}
7. 称呼学生时使用亲切语气,注意正向激励`;
const userPrompt=`课程信息:
- 日期:${dateStr}
- 班级:${currentClass.id}
- 课程主题:${theme}
- 核心知识:${knowledge&&knowledge!=='-'?knowledge:'乐高搭建与编程'}
- 课程代码:${code}
以下学生的课堂表现记录:
${studentRecords}
请为以上每位出勤学生生成个性化课评。格式要求:
- 每位学生以"【学生姓名】"开头
- 课评正文约150-250字
- 每句话末尾加匹配的 emoji
- 末尾固定【温馨提示】段落`;
try {
const messages=[{role:'system',content:systemPrompt},{role:'user',content:userPrompt}];
const resp=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json','Authorization':`Bearer ${key}`},body:JSON.stringify({model:model||'deepseek-chat',messages:messages,max_tokens:4000,temperature:0.7})});
if(!resp.ok){const err=await resp.json();throw new Error(err.error?.message||`HTTP ${resp.status}`);}
const data=await resp.json();
const aiContent=data.choices?.[0]?.message?.content||JSON.stringify(data);
document.getElementById('outputContent').textContent=aiContent;
document.getElementById('outputTitle').textContent='🤖 AI 生成课评';
document.getElementById('output').classList.add('show');
document.getElementById('copyBtn').textContent='📋 复制到剪贴板';
document.getElementById('copyBtn').classList.remove('copied');
showToast('✅ AI 课评生成成功!','success');
document.getElementById('output').scrollIntoView({behavior:'smooth',block:'start'});
}catch(e){
showToast('❌ AI生成失败: '+e.message,'error');
document.getElementById('outputContent').textContent=`错误:${e.message}\n\n请检查 API 配置是否正确,或点击"测试连接"验证。`;
document.getElementById('outputTitle').textContent='❌ 生成失败';
document.getElementById('output').classList.add('show');
}finally{
btn.disabled=false; btn.textContent=origText;
}
}
// ===============================================
// 快速跳转
// ===============================================
function handleQuickJump(e){if(e.key==='Enter')doQuickJump();}
function doQuickJump(){
const inp=document.getElementById('quickInput').value.trim();
if(!inp)return;
const match=inp.match(/\/?(周[一二三四五六日])\s*(\d+)[点:]?/);
if(!match){showToast('格式不对哦,请用:/周六 16点','error');return;}
const weekday=match[1], hour=parseInt(match[2]);
const mc=CONFIG.classes.find(c=>{if(c.weekday!==weekday)return false;const ch=parseInt(c.time.split(':')[0]);return ch===hour;});
if(!mc){showToast(`没有找到 ${weekday} ${hour}点的班级`,'error');return;}
const cwn=getCurrentWeekNumber();
document.getElementById('weekSelect').value=cwn; onWeekChange();
document.getElementById('weekdaySelect').value=weekday; onWeekdayChange();
document.getElementById('classSelect').value=mc.id; onClassChange();
document.getElementById('quickInput').value='';
setTimeout(()=>document.getElementById('cardsContainer').scrollIntoView({behavior:'smooth',block:'start'}),100);
}
// ===============================================
// 初始化
// ===============================================
function getCurrentWeekNumber() {
const start=new Date('2026-03-02');
const today=new Date();
// 使用本地日期计算,避免 UTC 时区偏差
const startLocal=Date.UTC(start.getFullYear(),start.getMonth(),start.getDate());
const todayLocal=Date.UTC(today.getFullYear(),today.getMonth(),today.getDate());
const diff=(todayLocal-startLocal)/(1000*60*60*24);
return Math.max(1,Math.min(21,Math.floor(diff/7)+1));
}
function init() {
fillWeekOptions();
loadAiConfig();
updateAiStatusDot();
const today=new Date();
const weekMap=['周日','周一','周二','周三','周四','周五','周六'];
const cwn=getCurrentWeekNumber();
const tw=weekMap[today.getDay()];
// 1. 自动选择当前周数
document.getElementById('weekSelect').value=String(cwn);
onWeekChange();
// 2. 延迟一帧后自动选择今天周几(确保 weekdaySelect DOM 已就绪)
requestAnimationFrame(()=>{
const ws=document.getElementById('weekdaySelect');
if(!ws.disabled){
ws.value=tw;
onWeekdayChange();
}
});
// 3. 在日期显示区标记当前周
const dd=document.getElementById('dateDisplay');
dd.textContent=dd.textContent+' (当前周)';
document.getElementById('quickInput').focus();
renderTagLibrary();
}
init();
// 监听进度更新
setInterval(()=>{try{updateProgress()}catch(e){}},2000);
</script>
</body>
</html>