Initial commit

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-13 15:38:41 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit 71db82393a
180 changed files with 170640 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# LLM 配置模板:复制为 .env 后填入真实值(.env 不要提交到版本库)
LLM_BASE_URL=https://cdr.digiman.live/v1
LLM_API_KEY=your-api-key-here
LLM_MODEL=claude-sonnet-4-6
LLM_MAX_CONCURRENCY=5
+22
View File
@@ -0,0 +1,22 @@
# Secrets
.env
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
# Node
node_modules/
# OS
.DS_Store
# Caches / generated
output/llm_cache/
output/pca_cache/
output/scoring_cache/
output/trace_cache/
frontend/dist.7z
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+872
View File
@@ -0,0 +1,872 @@
"""
Era2 API 路由: 全市266校报告生成系统
区选择 → 学校选择 → 报告生成 → 历史查看 → LLM助理
"""
import asyncio
import json
import logging
import time
from pathlib import Path
from typing import Optional
import numpy as np
from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
from pydantic import BaseModel
from .era2_state import era2_state, OUTPUT_DIR
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/era2")
# ==================== Models ====================
class GenerateRequest(BaseModel):
school: str
district: str
use_cache: bool = True
skip_llm: bool = False
enable_agent: bool = False # AI助理已统一由管理平台前端ChatFab+后端/era2/chat提供,静态HTML不再内嵌
lang: str = "zh" # "zh" | "en" | "both"
class BatchGenerateRequest(BaseModel):
district: str
schools: Optional[list[str]] = None
use_cache: bool = True
skip_llm: bool = False
enable_agent: bool = False # AI助理已统一由管理平台前端ChatFab+后端/era2/chat提供,静态HTML不再内嵌
lang: str = "zh" # "zh" | "en" | "both"
class ChatRequest(BaseModel):
message: str
school: Optional[str] = None
district: Optional[str] = None
history: list[dict] = []
lang: str = "zh" # "zh" | "en"
# ==================== Helpers ====================
def _report_filename(school: str, lang: str) -> str:
"""根据 lang 返回报告 HTML 文件名(与 04_generate_report.py 一致)"""
if lang == "en":
return f"{school}_report_en.html"
return f"{school}_报告.html"
def _normalize_langs(lang: str) -> list[str]:
"""'zh' / 'en' / 'both' 标准化成 ['zh'] / ['en'] / ['zh','en']"""
if lang == "both":
return ["zh", "en"]
if lang == "en":
return ["en"]
return ["zh"]
# ==================== 区和学校 ====================
@router.get("/districts")
async def list_districts():
"""获取所有区的摘要"""
return {
"districts": era2_state.get_districts_summary(),
"total": len(era2_state.districts),
}
@router.get("/districts/{district}/schools")
async def list_schools_in_district(district: str):
"""获取某区的学校列表"""
if district not in era2_state.districts:
raise HTTPException(404, f"'{district}' 不存在")
schools = era2_state.get_schools_in_district(district)
return {
"district": district,
"schools": schools,
"total": len(schools),
}
# ==================== 报告生成 ====================
_generation_tasks = {}
def _clean_for_json(obj):
if isinstance(obj, dict):
return {k: _clean_for_json(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [_clean_for_json(v) for v in obj]
elif isinstance(obj, (np.integer,)):
return int(obj)
elif isinstance(obj, (np.floating,)):
return round(float(obj), 4)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, float):
if np.isnan(obj) or np.isinf(obj):
return None
return round(obj, 4)
return obj
@router.post("/reports/generate")
async def generate_report(req: GenerateRequest, background_tasks: BackgroundTasks):
"""异步生成单校报告"""
school = req.school
district = req.district
if district not in era2_state.districts:
raise HTTPException(404, f"'{district}' 不存在")
district_schools = era2_state.district_schools.get(district, [])
if school not in district_schools:
raise HTTPException(404, f"学校 '{school}' 不在 {district}")
langs = _normalize_langs(req.lang)
task_id = f"era2_{school}_{int(time.time())}"
_generation_tasks[task_id] = {
"status": "pending",
"school": school,
"district": district,
"lang": req.lang,
"langs": langs,
"progress": 0,
"total": 29 * len(langs),
"current_segment": "",
"current_lang": langs[0] if langs else "zh",
"started_at": time.time(),
}
background_tasks.add_task(
_do_generate, task_id, school, district,
req.use_cache, req.skip_llm, req.enable_agent, langs
)
return {
"task_id": task_id, "school": school, "district": district,
"status": "started", "lang": req.lang, "langs": langs,
}
def _do_generate(task_id: str, school: str, district: str,
use_cache: bool, skip_llm: bool, enable_agent: bool,
langs: list[str]):
"""后台执行era2报告生成(支持中/英/双语)"""
import sys
ERA2_SCRIPTS = Path(__file__).parent.parent.parent.parent / "scripts" / "era2"
sys.path.insert(0, str(ERA2_SCRIPTS))
status = _generation_tasks[task_id]
status["status"] = "running"
try:
start = time.time()
# 1. 获取报告数据(与语言无关)
report_data = era2_state.get_report_data(school, district)
from engines.report_renderer_era2 import ReportRendererEra2
renderer = ReportRendererEra2()
output_dir = OUTPUT_DIR / district
output_dir.mkdir(parents=True, exist_ok=True)
# 单语段数(用于多语言进度合并)
per_lang_total = 29
outputs = {}
# 2. 按语言依次生成
for lang_idx, lang in enumerate(langs):
status["current_lang"] = lang
if skip_llm:
llm_sections = {}
# 进度推到该语言段末尾
status["progress"] = (lang_idx + 1) * per_lang_total
status["total"] = len(langs) * per_lang_total
else:
from app.engines.llm_engine import LLMEngine
llm_engine = LLMEngine()
def progress_cb(completed, total, segment_id, _lang_idx=lang_idx, _lang=lang):
# 累计进度 = 之前语言已完成段数 + 当前段数
status["progress"] = _lang_idx * total + completed
status["total"] = len(langs) * total
status["current_segment"] = segment_id
status["current_lang"] = _lang
llm_engine.set_progress_callback(progress_cb)
llm_sections = llm_engine.generate_report_segments(
report_data, use_cache=use_cache, lang=lang,
)
# 3. 渲染HTML(按语言区分文件名)
output_path = output_dir / _report_filename(school, lang)
renderer.render_to_file(report_data, llm_sections, output_path,
enable_agent=enable_agent, lang=lang)
outputs[lang] = str(output_path)
# 4. 保存 JSON(与语言无关,覆盖即可)
json_path = output_dir / f"{school}_report_data.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(_clean_for_json(report_data), f, ensure_ascii=False, indent=2)
elapsed = time.time() - start
status["status"] = "completed"
status["elapsed"] = round(elapsed, 1)
status["score"] = report_data["overall"]["score"]
status["rank"] = report_data["overall"]["rank_in_district"]
status["outputs"] = outputs
logger.info(f"✅ [Era2] {district}/{school} 报告生成完成 ({','.join(langs)}), {elapsed:.1f}s")
except Exception as e:
status["status"] = "failed"
status["error"] = str(e)
logger.error(f"❌ [Era2] {district}/{school} 报告生成失败: {e}", exc_info=True)
@router.get("/reports/generate/{task_id}/status")
async def get_task_status(task_id: str):
"""查询生成状态"""
if task_id not in _generation_tasks:
raise HTTPException(404, f"任务 '{task_id}' 不存在")
return _generation_tasks[task_id]
@router.get("/reports/generate/{task_id}/stream")
async def stream_task_progress(task_id: str):
"""SSE 实时进度"""
if task_id not in _generation_tasks:
raise HTTPException(404, f"任务 '{task_id}' 不存在")
async def event_generator():
while True:
status = _generation_tasks.get(task_id, {})
data = json.dumps(status, ensure_ascii=False, default=str)
yield f"data: {data}\n\n"
if status.get("status") in ("completed", "failed"):
break
await asyncio.sleep(0.5)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
# ==================== 批量生成 ====================
_batch_tasks = {}
@router.post("/reports/batch")
async def batch_generate(req: BatchGenerateRequest, background_tasks: BackgroundTasks):
"""批量生成某区报告"""
district = req.district
if district not in era2_state.districts:
raise HTTPException(404, f"'{district}' 不存在")
all_in_d = era2_state.district_schools.get(district, [])
schools = req.schools or all_in_d
invalid = [s for s in schools if s not in all_in_d]
if invalid:
raise HTTPException(400, f"无效学校: {invalid}")
langs = _normalize_langs(req.lang)
batch_id = f"era2_batch_{int(time.time())}"
_batch_tasks[batch_id] = {
"status": "pending",
"district": district,
"schools": schools,
"lang": req.lang,
"langs": langs,
"total": len(schools),
"completed": 0,
"current_school": None,
"current_lang": langs[0] if langs else "zh",
"results": [],
"started_at": time.time(),
}
background_tasks.add_task(
_do_batch, batch_id, district, schools,
req.use_cache, req.skip_llm, req.enable_agent, langs
)
return {
"batch_id": batch_id, "district": district,
"total": len(schools), "lang": req.lang, "langs": langs,
}
def _do_batch(batch_id: str, district: str, schools: list,
use_cache: bool, skip_llm: bool, enable_agent: bool,
langs: list[str]):
"""后台批量生成(支持中/英/双语)"""
import sys
ERA2_SCRIPTS = Path(__file__).parent.parent.parent.parent / "scripts" / "era2"
sys.path.insert(0, str(ERA2_SCRIPTS))
status = _batch_tasks[batch_id]
status["status"] = "running"
llm_engine = None
if not skip_llm:
from app.engines.llm_engine import LLMEngine
llm_engine = LLMEngine()
from engines.report_renderer_era2 import ReportRendererEra2
renderer = ReportRendererEra2()
output_dir = OUTPUT_DIR / district
output_dir.mkdir(parents=True, exist_ok=True)
for idx, school in enumerate(schools):
school_start = time.time()
status["current_school"] = school
try:
report_data = era2_state.get_report_data(school, district)
outputs = {}
for lang in langs:
status["current_lang"] = lang
if skip_llm or llm_engine is None:
llm_sections = {}
else:
llm_sections = llm_engine.generate_report_segments(
report_data, use_cache=use_cache, lang=lang,
)
output_path = output_dir / _report_filename(school, lang)
renderer.render_to_file(report_data, llm_sections, output_path,
enable_agent=enable_agent, lang=lang)
outputs[lang] = str(output_path)
json_path = output_dir / f"{school}_report_data.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(_clean_for_json(report_data), f, ensure_ascii=False, indent=2)
elapsed = time.time() - school_start
status["results"].append({
"school": school,
"status": "success",
"score": report_data["overall"]["score"],
"rank": report_data["overall"]["rank_in_district"],
"langs": langs,
"outputs": outputs,
"time": round(elapsed, 1),
})
except Exception as e:
elapsed = time.time() - school_start
status["results"].append({
"school": school,
"status": "failed",
"error": str(e),
"time": round(elapsed, 1),
})
logger.error(f"❌ [Era2] 批量 {district}/{school} 失败: {e}", exc_info=True)
status["completed"] = idx + 1
status["status"] = "completed"
status["elapsed"] = round(time.time() - status["started_at"], 1)
status["current_school"] = None
@router.get("/reports/batch/{batch_id}/status")
async def get_batch_status(batch_id: str):
if batch_id not in _batch_tasks:
raise HTTPException(404, f"批次 '{batch_id}' 不存在")
return _batch_tasks[batch_id]
# ==================== 报告预览/下载/历史 ====================
@router.get("/reports/{district}/{school}/preview")
async def preview_report(district: str, school: str, lang: str = "zh"):
"""预览HTML报告(支持 ?lang=zh|en"""
path = OUTPUT_DIR / district / _report_filename(school, lang)
if not path.exists():
raise HTTPException(404, f"报告不存在: {district}/{school} (lang={lang})")
return HTMLResponse(path.read_text("utf-8"))
@router.get("/reports/{district}/{school}/download")
async def download_report(district: str, school: str, lang: str = "zh"):
"""下载HTML报告(支持 ?lang=zh|en"""
path = OUTPUT_DIR / district / _report_filename(school, lang)
if not path.exists():
raise HTTPException(404, f"报告不存在: {district}/{school} (lang={lang})")
if lang == "en":
download_name = f"{school}_Curriculum_Implementation_Monitoring_Report.html"
else:
download_name = f"{school}_课程实施监测报告.html"
return FileResponse(
str(path),
filename=download_name,
media_type="text/html; charset=utf-8",
)
@router.get("/reports/{district}/{school}/data")
async def get_report_json(district: str, school: str):
"""获取报告JSON数据"""
path = OUTPUT_DIR / district / f"{school}_report_data.json"
if not path.exists():
raise HTTPException(404, f"报告数据不存在: {district}/{school}")
return json.loads(path.read_text("utf-8"))
@router.get("/reports/history")
async def get_report_history():
"""获取所有已生成报告的历史"""
return {
"reports": era2_state.get_report_history(),
}
# ==================== LLM 聊天助理 ====================
def _build_chat_system_prompt(school: str = None, district: str = None, lang: str = "zh") -> str:
"""
构建AI聊天助理的系统prompt,注入完整报告数据上下文。
对齐原 chat_widget.html 内嵌版的上下文丰富度:
- 总体得分、区均值、排名、聚类
- 七大维度:得分、区均值、差值、排名、聚类
- 二十个三级维度:得分、区均值、差值、水平、排名
- 回答规范
支持 lang='en' 输出英文 system prompt。
"""
if lang == "en":
return _build_chat_system_prompt_en(school, district)
ctx = f"你是上海市高中课程实施监测数据分析助理。\n"
ctx += f"当前系统覆盖上海市{len(era2_state.districts)}个区、{len(era2_state.schools)}所高中。\n\n"
if not school or not district:
ctx += ("你可以回答关于学校课程实施监测的各种问题,"
"包括七大维度(课程领导力、教学变革力、学生发展指导力、教师发展支持力、"
"教育质量评估力、教育条件保障力、数字化赋能力)的解读、对比分析和改进建议。\n")
return ctx
# 尝试加载完整报告数据
data = None
try:
json_path = OUTPUT_DIR / district / f"{school}_report_data.json"
if json_path.exists():
data = json.loads(json_path.read_text("utf-8"))
except Exception as e:
logger.warning(f"加载学校JSON失败: {e}")
# JSON不存在时实时计算
if data is None:
try:
data = era2_state.get_report_data(school, district)
except Exception as e:
logger.warning(f"实时计算报告数据失败: {e}")
ctx += f"当前上下文学校: {school}{district}),数据加载失败。\n"
return ctx
o = data.get("overall", {})
dims = data.get("dimensions", {})
sub_dims = data.get("sub_dimensions", {})
ctx += f"## 报告核心数据\n\n"
ctx += f"### 总体表现\n"
ctx += f"- 当前学校: {school}{district}\n"
ctx += f"- 总体得分: {o.get('score')}\n"
ctx += f"- 区均值: {o.get('district_avg')}\n"
ctx += f"- 区内排名: 第{o.get('rank_in_district')}/{o.get('total_schools')}\n"
if o.get('rank_in_city') is not None and o.get('total_schools_in_city'):
ctx += f"- 全市排名: 第{o.get('rank_in_city')}/{o.get('total_schools_in_city')}\n"
ctx += f"- 聚类类型: 课程实施{o.get('cluster', '')}\n"
ctx += f"- 学校类型: {o.get('school_type', '')}\n\n"
total_city = o.get('total_schools_in_city')
# 七大维度表格
ctx += "### 七大维度得分\n"
if total_city:
ctx += "| 维度 | 得分 | 区均值 | 差值 | 区排名 | 全市排名 | 聚类 |\n"
ctx += "|------|------|--------|------|--------|----------|------|\n"
else:
ctx += "| 维度 | 得分 | 区均值 | 差值 | 区排名 | 聚类 |\n"
ctx += "|------|------|--------|------|--------|------|\n"
for dim_name, d in dims.items():
score = d.get('score')
davg = d.get('district_avg')
diff = d.get('diff_district')
rank = d.get('rank_in_district')
rank_city = d.get('rank_in_city')
cluster = d.get('cluster', '')
score_s = f"{score:.2f}" if isinstance(score, (int, float)) else str(score)
davg_s = f"{davg:.2f}" if isinstance(davg, (int, float)) else str(davg)
if isinstance(diff, (int, float)):
diff_s = f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}"
else:
diff_s = str(diff)
if total_city:
rc_s = f"{rank_city}/{total_city}" if rank_city is not None else "-"
ctx += f"| {dim_name} | {score_s} | {davg_s} | {diff_s} | {rank}/{o.get('total_schools')} | {rc_s} | {cluster} |\n"
else:
ctx += f"| {dim_name} | {score_s} | {davg_s} | {diff_s} | {rank}/{o.get('total_schools')} | {cluster} |\n"
ctx += "\n"
# 二十个三级维度表格
ctx += "### 二十个三级维度详情\n"
if total_city:
ctx += "| 三级维度 | 得分 | 区均值 | 差值 | 水平 | 区排名 | 全市排名 |\n"
ctx += "|----------|------|--------|------|------|--------|----------|\n"
else:
ctx += "| 三级维度 | 得分 | 区均值 | 差值 | 水平 | 区排名 |\n"
ctx += "|----------|------|--------|------|------|--------|\n"
for sd_name, sd in sub_dims.items():
score = sd.get('score')
davg = sd.get('district_avg')
diff = sd.get('diff_district')
level = sd.get('level')
rank = sd.get('rank_in_district')
rank_city = sd.get('rank_in_city')
score_s = f"{score:.2f}" if isinstance(score, (int, float)) else "N/A"
davg_s = f"{davg:.2f}" if isinstance(davg, (int, float)) else "N/A"
if isinstance(diff, (int, float)):
diff_s = f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}"
else:
diff_s = "N/A"
if total_city:
rc_s = f"{rank_city}/{total_city}" if rank_city is not None else "-"
ctx += f"| {sd_name} | {score_s} | {davg_s} | {diff_s} | 水平{level} | {rank}/{o.get('total_schools')} | {rc_s} |\n"
else:
ctx += f"| {sd_name} | {score_s} | {davg_s} | {diff_s} | 水平{level} | {rank}/{o.get('total_schools')} |\n"
ctx += "\n"
# 回答规范
ctx += "## 回答规范\n"
ctx += "1. 始终基于上述数据回答,引用具体数值\n"
ctx += '2. 称呼被分析学校为"贵校"\n'
ctx += "3. 语言风格:专业、客观、平实\n"
ctx += '4. 使用"高于/低于XX均值X.XX分"句式进行对比\n'
ctx += "5. 给出改进建议时要具体可操作\n"
ctx += "6. 如果用户问的内容不在数据范围内,诚实告知\n"
ctx += "7. 回答控制在200-500字以内,避免冗长\n"
return ctx
# 中→英 维度名映射(用于 chat 上下文)
_DIM_EN = {
"课程领导力": "Curriculum Leadership",
"教学变革力": "Instructional Reform Capacity",
"学生发展指导力": "Student Development Guidance",
"教师发展支持力": "Teacher Development Support",
"教育质量评估力": "Educational Quality Assessment",
"教育条件保障力": "Educational Conditions and Resources",
"数字化赋能力": "Digital Empowerment",
}
_SUB_EN = {
"国家标准遵循": "National Standards Compliance",
"课程结构建设": "Curriculum Structure Design",
"课程规范落实": "Curriculum Governance Implementation",
"教学方式变革": "Pedagogical Reform",
"作业设计与管理变革": "Homework Design and Management",
"学科发展的个性化辅导": "Personalized Subject Tutoring",
"学生生涯发展指导": "Student Career Development Guidance",
"培训支持": "Professional Training Support",
"教研支持": "Teaching Research Support",
"项目支持": "Research Project Support",
"科学评价观": "Scientific Assessment Perspective",
"学业质量评估": "Academic Quality Assessment",
"综合素质评估": "Holistic Competency Assessment",
"实践活动评估": "Practice-Based Activity Assessment",
"区域推进": "District-Level Implementation Drive",
"环境支持": "Environmental Support",
"资源支持": "Resource Support",
"教学方式创新": "Innovative Instructional Methods",
"评价精准化与个性化": "Precise and Personalized Assessment",
"课程迭代优化": "Iterative Curriculum Optimization",
}
_CLUSTER_EN = {
"较好": "High-Performing",
"中等": "Mid-Tier",
"待提升": "Improvement-Needed",
}
_DISTRICT_EN = {
"长宁区": "Changning District",
"杨浦区": "Yangpu District",
"闵行区": "Minhang District",
"浦东新区": "Pudong New Area",
"嘉定区": "Jiading District",
"宝山区": "Baoshan District",
"金山区": "Jinshan District",
"静安区": "Jing'an District",
"奉贤区": "Fengxian District",
"普陀区": "Putuo District",
"徐汇区": "Xuhui District",
}
def _build_chat_system_prompt_en(school: str = None, district: str = None) -> str:
"""English version of chat system prompt (OECD/PISA register)."""
ctx = f"You are a data-analysis assistant for the Shanghai Senior Secondary School Curriculum Implementation Monitoring system.\n"
ctx += f"The system covers {len(era2_state.districts)} districts and {len(era2_state.schools)} senior secondary schools across Shanghai.\n\n"
if not school or not district:
ctx += (
"You may respond to questions on curriculum-implementation monitoring, "
"including interpretation, comparison, and improvement recommendations across the seven dimensions: "
"Curriculum Leadership, Instructional Reform Capacity, Student Development Guidance, "
"Teacher Development Support, Educational Quality Assessment, "
"Educational Conditions and Resources, and Digital Empowerment.\n"
)
return ctx
# 加载完整报告数据
data = None
try:
json_path = OUTPUT_DIR / district / f"{school}_report_data.json"
if json_path.exists():
data = json.loads(json_path.read_text("utf-8"))
except Exception as e:
logger.warning(f"加载学校JSON失败: {e}")
if data is None:
try:
data = era2_state.get_report_data(school, district)
except Exception as e:
logger.warning(f"实时计算报告数据失败: {e}")
ctx += f"Current school context: {school} ({_DISTRICT_EN.get(district, district)}); data load failed.\n"
return ctx
o = data.get("overall", {})
dims = data.get("dimensions", {})
sub_dims = data.get("sub_dimensions", {})
district_en = _DISTRICT_EN.get(district, district)
cluster_en = _CLUSTER_EN.get(o.get("cluster", ""), o.get("cluster", ""))
ctx += "## Core Report Data\n\n"
ctx += "### Overall Performance\n"
ctx += f"- School (analysis target): {school} ({district_en})\n"
ctx += f"- Overall score: {o.get('score')}\n"
ctx += f"- District average: {o.get('district_avg')}\n"
ctx += f"- District rank: {o.get('rank_in_district')} of {o.get('total_schools')}\n"
if o.get("rank_in_city") is not None and o.get("total_schools_in_city"):
ctx += f"- Municipal rank: {o.get('rank_in_city')} of {o.get('total_schools_in_city')}\n"
if cluster_en:
ctx += f"- Implementation cluster: {cluster_en}\n"
ctx += f"- School type: {o.get('school_type', '')}\n\n"
total_city = o.get("total_schools_in_city")
# 七大维度
ctx += "### Scores Across Seven Dimensions\n"
if total_city:
ctx += "| Dimension | Score | District Avg. | Δ | District Rank | Municipal Rank | Cluster |\n"
ctx += "|-----------|-------|---------------|---|----------------|-----------------|---------|\n"
else:
ctx += "| Dimension | Score | District Avg. | Δ | District Rank | Cluster |\n"
ctx += "|-----------|-------|---------------|---|----------------|---------|\n"
for dim_name, d in dims.items():
score = d.get("score")
davg = d.get("district_avg")
diff = d.get("diff_district")
rank = d.get("rank_in_district")
rank_city = d.get("rank_in_city")
cluster = _CLUSTER_EN.get(d.get("cluster", ""), d.get("cluster", ""))
score_s = f"{score:.2f}" if isinstance(score, (int, float)) else str(score)
davg_s = f"{davg:.2f}" if isinstance(davg, (int, float)) else str(davg)
diff_s = (f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}") if isinstance(diff, (int, float)) else str(diff)
dim_en = _DIM_EN.get(dim_name, dim_name)
if total_city:
rc_s = f"{rank_city}/{total_city}" if rank_city is not None else "-"
ctx += f"| {dim_en} | {score_s} | {davg_s} | {diff_s} | {rank}/{o.get('total_schools')} | {rc_s} | {cluster} |\n"
else:
ctx += f"| {dim_en} | {score_s} | {davg_s} | {diff_s} | {rank}/{o.get('total_schools')} | {cluster} |\n"
ctx += "\n"
# 二十个三级维度
ctx += "### Twenty Sub-Dimensions\n"
if total_city:
ctx += "| Sub-dimension | Score | District Avg. | Δ | Level | District Rank | Municipal Rank |\n"
ctx += "|---------------|-------|---------------|---|-------|----------------|-----------------|\n"
else:
ctx += "| Sub-dimension | Score | District Avg. | Δ | Level | District Rank |\n"
ctx += "|---------------|-------|---------------|---|-------|----------------|\n"
for sd_name, sd in sub_dims.items():
score = sd.get("score")
davg = sd.get("district_avg")
diff = sd.get("diff_district")
level = sd.get("level")
rank = sd.get("rank_in_district")
rank_city = sd.get("rank_in_city")
score_s = f"{score:.2f}" if isinstance(score, (int, float)) else "N/A"
davg_s = f"{davg:.2f}" if isinstance(davg, (int, float)) else "N/A"
diff_s = (f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}") if isinstance(diff, (int, float)) else "N/A"
sd_en = _SUB_EN.get(sd_name, sd_name)
if total_city:
rc_s = f"{rank_city}/{total_city}" if rank_city is not None else "-"
ctx += f"| {sd_en} | {score_s} | {davg_s} | {diff_s} | Level {level} | {rank}/{o.get('total_schools')} | {rc_s} |\n"
else:
ctx += f"| {sd_en} | {score_s} | {davg_s} | {diff_s} | Level {level} | {rank}/{o.get('total_schools')} |\n"
ctx += "\n"
ctx += "## Response Conventions\n"
ctx += "1. Ground every observation in the data above; cite numerical values explicitly.\n"
ctx += '2. Refer to the analysed school as "your school".\n'
ctx += "3. Maintain a formal, evidence-based academic register (OECD/PISA style).\n"
ctx += '4. Use phrasing such as "X.XX points above/below the district average" for comparisons.\n'
ctx += "5. Improvement recommendations must be specific and actionable.\n"
ctx += "6. If a question lies outside the supplied data, say so honestly.\n"
ctx += "7. Keep responses concise — typically 150 to 350 words. Output strictly in formal English; do NOT use Chinese characters.\n"
return ctx
@router.post("/chat")
async def chat_with_assistant(req: ChatRequest):
"""
LLM 聊天助理:真正的异步流式响应
如果指定了 school+district,会注入该校的数据上下文
修复说明(2024-03):
- 旧版使用同步 OpenAI client,在 async generator 中阻塞事件循环,
导致:(1) 所有 chunk 攒到最后才发出 (2) 长时间阻塞触发超时
- 新版使用 AsyncOpenAI,真正 async for 逐 chunk yield
"""
from openai import AsyncOpenAI
from ..config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL
lang = (req.lang or "zh").lower()
if lang not in ("zh", "en"):
lang = "zh"
# 构建系统prompt(对齐内嵌chat_widget版本的完整上下文)
system_prompt = _build_chat_system_prompt(req.school, req.district, lang=lang)
# 构建消息
# NOTE: cdr.digiman.live 等 API 代理会丢弃 system 角色消息,
# 所以将系统prompt伪装成 user+assistant 对话对来注入上下文。
school_name = req.school or ("the school" if lang == "en" else "该校")
if lang == "en":
sys_inject = (
f"[SYSTEM INSTRUCTIONS] {system_prompt}\n\n"
"Please confirm that you have read and understood the data and conventions above; "
"subsequent answers must be grounded in this data."
)
ack = (
f"I have reviewed the full curriculum-implementation monitoring data for your school "
f"({school_name}), including the overall score, the seven dimensions, and the twenty sub-dimensions. "
"I will respond strictly on the basis of this data, in a formal academic register. "
"What would you like to know?"
)
else:
sys_inject = f"[系统指令] {system_prompt}\n\n请确认你已了解以上数据和规范,后续将基于这些数据回答问题。"
ack = (
f"我已了解贵校({school_name})课程实施监测的全部数据,"
"包括总体得分、七大维度和二十个三级维度的详细数据。"
"我将严格基于这些数据,以专业、客观的风格回答您的问题。请问有什么想了解的?"
)
messages = [
{"role": "user", "content": sys_inject},
{"role": "assistant", "content": ack},
]
for h in req.history[-10:]: # 最多保留10轮历史
messages.append({"role": h.get("role", "user"), "content": h.get("content", "")})
user_msg = req.message
if lang == "en":
# 进一步追加输出语言指示,防止模型回中文
user_msg = (
user_msg
+ "\n\n(Please answer strictly in formal English following the OECD/PISA register; "
"do not include Chinese characters.)"
)
messages.append({"role": "user", "content": user_msg})
# 异步流式调用(不阻塞事件循环)
client = AsyncOpenAI(
base_url=LLM_BASE_URL,
api_key=LLM_API_KEY,
timeout=120.0, # 连接+读取总超时 120s
)
async def stream_response():
try:
response = await client.chat.completions.create(
model=LLM_MODEL,
messages=messages,
stream=True,
max_tokens=2000,
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
yield f"data: {json.dumps({'content': content}, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
logger.error(f"❌ Chat stream error: {e}")
yield f"data: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
finally:
await client.close()
return StreamingResponse(
stream_response(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Nginx 禁用代理缓冲
"Content-Type": "text/event-stream",
},
)
# ==================== 配置 ====================
@router.get("/config/framework")
async def get_framework():
"""测评框架"""
return {
"dimensions": {
name: {"sub_dimensions": info["sub_dimensions"]}
for name, info in DIMENSION_FRAMEWORK.items()
},
"level_descriptions": LEVEL_DESCRIPTIONS,
}
# ==================== 数据溯源 ====================
@router.get("/trace/{district}/{school}")
async def get_trace(district: str, school: str):
"""
获取单校的完整计算链路溯源数据(6阶段)
供前端 React Three Fiber 数据溯源可视化消费
"""
district_schools = era2_state.district_schools.get(district, [])
if not district_schools:
raise HTTPException(404, f"区域不存在: {district}")
if school not in era2_state.schools:
raise HTTPException(404, f"学校不存在: {school}")
import sys as _sys
_era2_path = str(Path(__file__).parent.parent.parent.parent / "scripts" / "era2")
if _era2_path not in _sys.path:
_sys.path.insert(0, _era2_path)
from engines.trace_engine import TraceEngine
trace_engine = TraceEngine(
data_engine=era2_state.data_engine,
pca_engine=era2_state.pca_engine,
stats_engine=era2_state.stats_engine,
sub_scores=era2_state.sub_scores,
dim_scores=era2_state.dim_scores,
)
result = trace_engine.compute_trace(school, district_schools)
return result
+246
View File
@@ -0,0 +1,246 @@
"""
Era2 全局状态:全市266校 数据引擎 + PCA赋分引擎 + 统计引擎
启动时一次性加载,后续API直接使用
"""
import logging
import time
import sys
from pathlib import Path
from typing import Dict, List, Optional
from datetime import datetime
import pandas as pd
import numpy as np
# era2 引擎路径
ERA2_SCRIPTS = Path(__file__).parent.parent.parent.parent / "scripts" / "era2"
sys.path.insert(0, str(ERA2_SCRIPTS))
from data_engine_era2 import DataEngineEra2
from engines.pca_scoring_engine_era2 import PcaScoringEngineEra2
from engines.stats_engine_era2 import StatsEngineEra2
from config_era2 import (
SCHOOL_TYPE_MAP, DIMENSION_FRAMEWORK, LEVEL_DESCRIPTIONS,
CLUSTER_CONFIG,
)
logger = logging.getLogger(__name__)
# 输出目录
OUTPUT_DIR = Path(__file__).parent.parent.parent.parent / "output" / "era2"
class Era2State:
"""Era2 全市数据的全局单例状态"""
def __init__(self):
self._initialized = False
self.data_engine: Optional[DataEngineEra2] = None
self.pca_engine: Optional[PcaScoringEngineEra2] = None
self.stats_engine: Optional[StatsEngineEra2] = None
self.sub_scores: Optional[pd.DataFrame] = None
self.dim_scores: Optional[pd.DataFrame] = None
self.schools: List[str] = []
self.districts: List[str] = []
self.district_schools: Dict[str, List[str]] = {}
self.school_meta: Dict[str, dict] = {}
def initialize(self):
"""启动时加载全市数据并计算分数(耗时约40-60秒)"""
if self._initialized:
return
start = time.time()
logger.info("🚀 [Era2] 加载全市数据...")
# 1. 加载全市数据
self.data_engine = DataEngineEra2(use_city_data=True)
self.data_engine.load_all()
self.schools = self.data_engine.schools
# 2. 构建区→学校映射
self.school_meta = SCHOOL_TYPE_MAP
self.district_schools = {}
for school, info in SCHOOL_TYPE_MAP.items():
district = info.get("district", "未知")
if district not in self.district_schools:
self.district_schools[district] = []
if school in self.schools:
self.district_schools[district].append(school)
self.districts = sorted(self.district_schools.keys())
# 3. PCA赋分(全市)
logger.info(f"🔢 [Era2] PCA赋分 ({len(self.schools)}校)...")
self.pca_engine = PcaScoringEngineEra2(self.data_engine)
pca_sub_scores = self.pca_engine.compute_all()
# 4. 统计引擎
self.stats_engine = StatsEngineEra2()
self.sub_scores = self.stats_engine.compute_dimension_scores_pca(pca_sub_scores)
self.dim_scores = self.stats_engine.compute_dimension_aggregates(self.sub_scores)
self._initialized = True
elapsed = time.time() - start
logger.info(f"✅ [Era2] 初始化完成: {len(self.schools)}校, {len(self.districts)}区, 耗时{elapsed:.1f}s")
def get_districts_summary(self) -> List[dict]:
"""返回所有区的摘要(中英两份报告分别计数)"""
result = []
for district in self.districts:
schools_in_d = self.district_schools.get(district, [])
# 区内平均分
if schools_in_d and self.dim_scores is not None:
valid = [s for s in schools_in_d if s in self.dim_scores.index]
avg = float(self.dim_scores.loc[valid, "总体得分"].mean()) if valid else 50.0
else:
avg = 50.0
# 已生成报告数(中文 / 英文)
district_dir = OUTPUT_DIR / district
if district_dir.exists():
report_count_zh = len(list(district_dir.glob("*_报告.html")))
report_count_en = len(list(district_dir.glob("*_report_en.html")))
else:
report_count_zh = 0
report_count_en = 0
# 任意一种语言已有视为 has_report,用于 UI 总数显示
report_count = report_count_zh
result.append({
"district": district,
"school_count": len(schools_in_d),
"avg_score": round(avg, 2),
"report_count": report_count,
"report_count_zh": report_count_zh,
"report_count_en": report_count_en,
})
return result
def get_schools_in_district(self, district: str) -> List[dict]:
"""返回某区所有学校的详细信息"""
schools_in_d = self.district_schools.get(district, [])
if not schools_in_d:
return []
# 排名(全市排名 + 区内排名)
sorted_all = self.dim_scores["总体得分"].sort_values(ascending=False)
dist_scores = self.dim_scores.loc[
[s for s in schools_in_d if s in self.dim_scores.index], "总体得分"
].sort_values(ascending=False)
result = []
for dist_rank, (school, score) in enumerate(dist_scores.items(), 1):
info = self.school_meta.get(school, {})
city_rank = int((sorted_all >= score).sum())
# 报告状态(中文 + 英文)
report_path_zh = OUTPUT_DIR / district / f"{school}_报告.html"
report_path_en = OUTPUT_DIR / district / f"{school}_report_en.html"
has_report_zh = report_path_zh.exists()
has_report_en = report_path_en.exists()
# 兼容老字段
has_report = has_report_zh or has_report_en
report_generated_at_zh = ""
report_size_zh = 0
report_generated_at_en = ""
report_size_en = 0
if has_report_zh:
stat = report_path_zh.stat()
report_size_zh = stat.st_size
report_generated_at_zh = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
if has_report_en:
stat = report_path_en.stat()
report_size_en = stat.st_size
report_generated_at_en = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
# 兼容老字段:优先用中文版,没有则用英文版
report_size = report_size_zh or report_size_en
report_generated_at = report_generated_at_zh or report_generated_at_en
# 聚类
dim_cols = [c for c in self.dim_scores.columns if c in DIMENSION_FRAMEWORK]
valid_schools = [s for s in schools_in_d if s in self.dim_scores.index]
cluster_result = self.stats_engine.cluster_analysis(
self.dim_scores.loc[valid_schools, dim_cols], dimension_name="总体"
)
cluster = cluster_result["school_clusters"].get(school, "")
result.append({
"name": school,
"district": district,
"type": info.get("type", ""),
"nature": info.get("nature", ""),
"area": info.get("area", ""),
"score": round(float(score), 2),
"district_rank": dist_rank,
"city_rank": city_rank,
"total_in_district": len(dist_scores),
"total_in_city": len(sorted_all),
"cluster": cluster,
# 兼容老字段
"has_report": has_report,
"report_size": report_size,
"report_generated_at": report_generated_at,
# 中英分开
"has_report_zh": has_report_zh,
"has_report_en": has_report_en,
"report_size_zh": report_size_zh,
"report_size_en": report_size_en,
"report_generated_at_zh": report_generated_at_zh,
"report_generated_at_en": report_generated_at_en,
})
return result
def get_report_data(self, school: str, district: str) -> dict:
"""生成单校报告数据包"""
district_schools = self.district_schools.get(district, [])
report_data = self.stats_engine.compute_school_report_data(
school, self.sub_scores, self.dim_scores,
district_schools=district_schools,
)
report_data["district"] = district
report_data["total_schools_in_district"] = len(district_schools)
return report_data
def get_report_history(self) -> List[dict]:
"""扫描所有已生成的报告,返回历史列表(合并中英两份,每校最多两条)"""
history = []
if not OUTPUT_DIR.exists():
return history
for district_dir in sorted(OUTPUT_DIR.iterdir()):
if not district_dir.is_dir():
continue
district = district_dir.name
# 同时收集中文 (_报告.html) 和英文 (_report_en.html) 报告
collected = []
for html_file in sorted(district_dir.glob("*_报告.html")):
school = html_file.stem.replace("_报告", "")
collected.append((school, html_file, "zh"))
for html_file in sorted(district_dir.glob("*_report_en.html")):
school = html_file.stem.replace("_report_en", "")
collected.append((school, html_file, "en"))
for school, html_file, lang in collected:
stat = html_file.stat()
json_path = district_dir / f"{school}_report_data.json"
score = None
if json_path.exists():
try:
import json
data = json.loads(json_path.read_text("utf-8"))
score = data.get("overall", {}).get("score")
except Exception:
pass
history.append({
"school": school,
"district": district,
"type": self.school_meta.get(school, {}).get("type", ""),
"score": score,
"file_size": stat.st_size,
"generated_at": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"file_name": html_file.name,
"lang": lang,
})
return history
# 全局单例
era2_state = Era2State()
+412
View File
@@ -0,0 +1,412 @@
"""
API 路由:所有 REST 端点
"""
import asyncio
import json
import logging
import time
from pathlib import Path
from typing import Optional
import numpy as np
from fastapi import APIRouter, HTTPException, BackgroundTasks, Query
from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
from pydantic import BaseModel
from .state import app_state
from ..config import OUTPUT_DIR, DIMENSION_FRAMEWORK, LEVEL_DESCRIPTIONS
from ..engines.llm_engine import LLMEngine
from ..engines.report_renderer import ReportRenderer
logger = logging.getLogger(__name__)
router = APIRouter()
# ==================== Pydantic Models ====================
class GenerateRequest(BaseModel):
school: str
use_cache: bool = True
skip_llm: bool = False
class BatchGenerateRequest(BaseModel):
schools: Optional[list[str]] = None # None = 全部
use_cache: bool = True
skip_llm: bool = False
# ==================== 学校相关 ====================
@router.get("/schools")
async def list_schools():
"""获取所有学校的摘要列表"""
return {
"schools": app_state.get_all_schools_summary(),
"total": len(app_state.schools),
}
@router.get("/schools/{school_name}")
async def get_school_detail(school_name: str):
"""获取单个学校的详细数据"""
if school_name not in app_state.schools:
raise HTTPException(404, f"学校 '{school_name}' 不存在")
report_data = app_state.get_report_data(school_name)
# JSON 安全序列化
def _safe(obj):
if isinstance(obj, (np.integer,)):
return int(obj)
if isinstance(obj, (np.floating,)):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
raise TypeError(f"Type {type(obj)} not serializable")
# 转为 JSON 安全格式
safe_data = json.loads(json.dumps(report_data, default=_safe, ensure_ascii=False))
return safe_data
@router.get("/schools/{school_name}/dimensions")
async def get_school_dimensions(school_name: str):
"""获取学校的维度得分摘要"""
if school_name not in app_state.schools:
raise HTTPException(404, f"学校 '{school_name}' 不存在")
report_data = app_state.get_report_data(school_name)
return {
"school": school_name,
"overall": report_data["overall"],
"dimensions": report_data["dimensions"],
"sub_dimensions": report_data["sub_dimensions"],
}
# ==================== 报告生成 ====================
# 全局生成状态追踪
_generation_status = {}
@router.post("/reports/generate")
async def generate_report(req: GenerateRequest, background_tasks: BackgroundTasks):
"""
生成单校报告(异步后台任务)
返回任务 ID,前端通过 SSE 监听进度
"""
school = req.school
if school not in app_state.schools:
raise HTTPException(404, f"学校 '{school}' 不存在")
task_id = f"gen_{school}_{int(time.time())}"
_generation_status[task_id] = {
"status": "pending",
"school": school,
"progress": 0,
"total": 29,
"current_segment": "",
"started_at": time.time(),
}
background_tasks.add_task(
_do_generate, task_id, school, req.use_cache, req.skip_llm
)
return {"task_id": task_id, "school": school, "status": "started"}
def _do_generate(task_id: str, school: str, use_cache: bool, skip_llm: bool):
"""后台执行报告生成"""
status = _generation_status[task_id]
status["status"] = "running"
try:
start = time.time()
# 获取报告数据
report_data = app_state.get_report_data(school)
# LLM 生成
if skip_llm:
llm_sections = {}
status["progress"] = status["total"]
else:
llm_engine = LLMEngine()
def progress_cb(completed, total, segment_id):
status["progress"] = completed
status["total"] = total
status["current_segment"] = segment_id
llm_engine.set_progress_callback(progress_cb)
llm_sections = llm_engine.generate_report_segments(
report_data, use_cache=use_cache
)
# 渲染 HTML
renderer = ReportRenderer()
output_path = OUTPUT_DIR / f"{school}_报告.html"
renderer.render_to_file(report_data, llm_sections, output_path)
# 保存数据 JSON
def _safe(obj):
if isinstance(obj, (np.integer,)):
return int(obj)
if isinstance(obj, (np.floating,)):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
raise TypeError(f"Type {type(obj)} not serializable")
json_path = OUTPUT_DIR / f"{school}_report_data.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(report_data, f, ensure_ascii=False, indent=2, default=_safe)
elapsed = time.time() - start
status["status"] = "completed"
status["elapsed"] = round(elapsed, 1)
status["output_path"] = str(output_path)
status["report_size"] = output_path.stat().st_size
status["score"] = report_data["overall"]["score"]
status["rank"] = report_data["overall"]["rank_in_district"]
logger.info(f"{school} 报告生成完成,耗时 {elapsed:.1f}s")
except Exception as e:
status["status"] = "failed"
status["error"] = str(e)
logger.error(f"{school} 报告生成失败: {e}", exc_info=True)
@router.get("/reports/generate/{task_id}/status")
async def get_generation_status(task_id: str):
"""查询生成任务状态"""
if task_id not in _generation_status:
raise HTTPException(404, f"任务 '{task_id}' 不存在")
return _generation_status[task_id]
@router.get("/reports/generate/{task_id}/stream")
async def stream_generation_progress(task_id: str):
"""SSE 实时进度流"""
if task_id not in _generation_status:
raise HTTPException(404, f"任务 '{task_id}' 不存在")
async def event_generator():
while True:
status = _generation_status.get(task_id, {})
data = json.dumps(status, ensure_ascii=False, default=str)
yield f"data: {data}\n\n"
if status.get("status") in ("completed", "failed"):
break
await asyncio.sleep(0.5)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
# ==================== 批量生成 ====================
_batch_status = {}
@router.post("/reports/batch")
async def batch_generate(req: BatchGenerateRequest, background_tasks: BackgroundTasks):
"""批量生成所有学校报告"""
schools = req.schools or app_state.schools
invalid = [s for s in schools if s not in app_state.schools]
if invalid:
raise HTTPException(400, f"无效学校: {invalid}")
batch_id = f"batch_{int(time.time())}"
_batch_status[batch_id] = {
"status": "pending",
"schools": schools,
"total": len(schools),
"completed": 0,
"results": [],
"started_at": time.time(),
}
background_tasks.add_task(
_do_batch_generate, batch_id, schools, req.use_cache, req.skip_llm
)
return {"batch_id": batch_id, "schools": schools, "total": len(schools)}
def _do_batch_generate(batch_id: str, schools: list, use_cache: bool, skip_llm: bool):
"""后台执行批量生成"""
status = _batch_status[batch_id]
status["status"] = "running"
renderer = ReportRenderer()
llm_engine = None if skip_llm else LLMEngine()
for idx, school in enumerate(schools):
school_start = time.time()
status["current_school"] = school
status["current_index"] = idx
try:
report_data = app_state.get_report_data(school)
if skip_llm:
llm_sections = {}
else:
llm_sections = llm_engine.generate_report_segments(
report_data, use_cache=use_cache
)
output_path = OUTPUT_DIR / f"{school}_报告.html"
renderer.render_to_file(report_data, llm_sections, output_path)
# 保存数据 JSON
def _safe(obj):
if isinstance(obj, (np.integer,)):
return int(obj)
if isinstance(obj, (np.floating,)):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
raise TypeError(f"Type {type(obj)} not serializable")
json_path = OUTPUT_DIR / f"{school}_report_data.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(report_data, f, ensure_ascii=False, indent=2, default=_safe)
elapsed = time.time() - school_start
status["results"].append({
"school": school,
"status": "success",
"score": report_data["overall"]["score"],
"rank": report_data["overall"]["rank_in_district"],
"cluster": report_data["overall"]["cluster"],
"time": round(elapsed, 1),
})
except Exception as e:
elapsed = time.time() - school_start
status["results"].append({
"school": school,
"status": "failed",
"error": str(e),
"time": round(elapsed, 1),
})
logger.error(f"❌ 批量生成 {school} 失败: {e}", exc_info=True)
status["completed"] = idx + 1
status["status"] = "completed"
status["elapsed"] = round(time.time() - status["started_at"], 1)
# 保存批量汇总 JSON
summary_path = OUTPUT_DIR / "batch_summary.json"
with open(summary_path, "w", encoding="utf-8") as f:
json.dump({
"generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"total_schools": len(schools),
"success_count": sum(1 for r in status["results"] if r["status"] == "success"),
"total_time": status["elapsed"],
"results": status["results"],
}, f, ensure_ascii=False, indent=2)
@router.get("/reports/batch/{batch_id}/status")
async def get_batch_status(batch_id: str):
"""查询批量生成状态"""
if batch_id not in _batch_status:
raise HTTPException(404, f"批次 '{batch_id}' 不存在")
return _batch_status[batch_id]
@router.get("/reports/batch/{batch_id}/stream")
async def stream_batch_progress(batch_id: str):
"""批量生成 SSE 进度流"""
if batch_id not in _batch_status:
raise HTTPException(404, f"批次 '{batch_id}' 不存在")
async def event_generator():
while True:
status = _batch_status.get(batch_id, {})
data = json.dumps(status, ensure_ascii=False, default=str)
yield f"data: {data}\n\n"
if status.get("status") in ("completed", "failed"):
break
await asyncio.sleep(1)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
# ==================== 报告预览/下载 ====================
@router.get("/reports/{school_name}/preview")
async def preview_report(school_name: str):
"""预览 HTML 报告(返回 HTML 内容)"""
report_path = OUTPUT_DIR / f"{school_name}_报告.html"
if not report_path.exists():
raise HTTPException(404, f"学校 '{school_name}' 的报告尚未生成")
return HTMLResponse(
content=report_path.read_text(encoding="utf-8"),
media_type="text/html; charset=utf-8",
)
@router.get("/reports/{school_name}/download")
async def download_report(school_name: str):
"""下载 HTML 报告"""
report_path = OUTPUT_DIR / f"{school_name}_报告.html"
if not report_path.exists():
raise HTTPException(404, f"学校 '{school_name}' 的报告尚未生成")
return FileResponse(
path=str(report_path),
filename=f"{school_name}_课程实施监测报告.html",
media_type="text/html; charset=utf-8",
)
@router.get("/reports/{school_name}/data")
async def get_report_data(school_name: str):
"""获取报告的 JSON 数据"""
json_path = OUTPUT_DIR / f"{school_name}_report_data.json"
if not json_path.exists():
raise HTTPException(404, f"学校 '{school_name}' 的报告数据不存在")
data = json.loads(json_path.read_text(encoding="utf-8"))
return data
# ==================== 系统配置 ====================
@router.get("/config/framework")
async def get_framework():
"""获取测评框架配置"""
return {
"dimensions": {
name: {
"sub_dimensions": info["sub_dimensions"],
}
for name, info in DIMENSION_FRAMEWORK.items()
},
"level_descriptions": LEVEL_DESCRIPTIONS,
}
@router.get("/reports/summary")
async def get_batch_summary():
"""获取最近一次批量生成的汇总"""
summary_path = OUTPUT_DIR / "batch_summary.json"
if not summary_path.exists():
return {"message": "尚无批量生成记录"}
return json.loads(summary_path.read_text(encoding="utf-8"))
+132
View File
@@ -0,0 +1,132 @@
"""
应用全局状态:管理数据引擎、赋分引擎、统计引擎的单例
避免每次请求重新加载 Excel 数据
"""
import logging
import time
from datetime import datetime
from typing import Dict, List, Optional
import pandas as pd
from ..engines.data_engine import DataEngine
from ..engines.scoring_engine import ScoringEngine
from ..engines.stats_engine import StatsEngine
from ..config import SCHOOL_TYPE_MAP, DIMENSION_FRAMEWORK
logger = logging.getLogger(__name__)
class AppState:
"""应用全局状态"""
def __init__(self):
self.data_engine: Optional[DataEngine] = None
self.scoring_engine: Optional[ScoringEngine] = None
self.stats_engine: Optional[StatsEngine] = None
self.raw_scores: Optional[Dict] = None
self.sub_scores: Optional[pd.DataFrame] = None
self.dim_scores: Optional[pd.DataFrame] = None
self.schools: List[str] = []
self._initialized = False
def initialize(self):
"""初始化所有引擎(只在应用启动时调用一次)"""
if self._initialized:
return
start = time.time()
# 1. 加载数据
self.data_engine = DataEngine()
self.data_engine.load_all()
self.schools = self.data_engine.schools
# 2. 赋分
self.scoring_engine = ScoringEngine(self.data_engine)
self.raw_scores = self.scoring_engine.score_all_schools()
# 3. 统计
self.stats_engine = StatsEngine()
self.sub_scores = self.stats_engine.compute_dimension_scores(self.raw_scores)
self.dim_scores = self.stats_engine.compute_dimension_aggregates(self.sub_scores)
self._initialized = True
elapsed = time.time() - start
logger.info(f"全局状态初始化完成,耗时 {elapsed:.1f}s")
def get_report_data(self, school: str) -> Dict:
"""获取某学校的报告数据包"""
if not self._initialized:
self.initialize()
return self.stats_engine.compute_school_report_data(
school, self.sub_scores, self.dim_scores
)
def get_school_info(self, school: str) -> Dict:
"""获取学校基本信息"""
info = SCHOOL_TYPE_MAP.get(school, {})
if not self.dim_scores is None and school in self.dim_scores.index:
score = round(float(self.dim_scores.loc[school, "总体得分"]), 2)
rank = int((self.dim_scores["总体得分"] >= self.dim_scores.loc[school, "总体得分"]).sum())
else:
score = 0
rank = 0
return {
"name": school,
"type": info.get("type", ""),
"code": info.get("code", ""),
"nature": info.get("nature", ""),
"feature": info.get("feature", ""),
"score": score,
"rank": rank,
"total_schools": len(self.schools),
}
def get_all_schools_summary(self) -> List[Dict]:
"""获取所有学校的摘要信息"""
if not self._initialized:
self.initialize()
summaries = []
# 排名
sorted_schools = self.dim_scores["总体得分"].sort_values(ascending=False)
for rank, (school, score) in enumerate(sorted_schools.items(), 1):
if school == "总体得分":
continue
info = SCHOOL_TYPE_MAP.get(school, {})
# 聚类
dim_cols = [c for c in self.dim_scores.columns if c in DIMENSION_FRAMEWORK]
cluster_result = self.stats_engine.cluster_analysis(self.dim_scores[dim_cols])
cluster = cluster_result["school_clusters"].get(school, "")
# 检查已生成的报告
from ..config import OUTPUT_DIR
report_path = OUTPUT_DIR / f"{school}_报告.html"
has_report = report_path.exists()
report_size = report_path.stat().st_size if has_report else 0
report_generated_at = ""
if has_report:
mtime = report_path.stat().st_mtime
report_generated_at = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
summaries.append({
"name": school,
"type": info.get("type", ""),
"code": info.get("code", ""),
"nature": info.get("nature", ""),
"score": round(float(score), 2),
"rank": rank,
"cluster": cluster,
"has_report": has_report,
"report_size": report_size,
"report_generated_at": report_generated_at,
})
return summaries
# 全局单例
app_state = AppState()
+151
View File
@@ -0,0 +1,151 @@
"""
认证模块:JWT Token + 密码哈希
部署到外网时的安全认证层
"""
import hashlib
import os
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import Depends, HTTPException, status, Request, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from pydantic import BaseModel
# ==================== 配置 ====================
# JWT 密钥:优先从环境变量读取,否则生成随机密钥(每次重启失效)
SECRET_KEY = os.environ.get("AUTH_SECRET_KEY", secrets.token_urlsafe(32))
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = int(os.environ.get("AUTH_TOKEN_EXPIRE_HOURS", "24"))
# 默认用户(可通过环境变量覆盖)
DEFAULT_USERNAME = os.environ.get("AUTH_USERNAME", "admin")
DEFAULT_PASSWORD = os.environ.get("AUTH_PASSWORD", "pswd4admin")
# Bearer token 提取器
security = HTTPBearer(auto_error=False)
# ==================== 密码哈希 (SHA-256 + salt) ====================
def _hash_password(password: str, salt: str | None = None) -> str:
"""SHA-256 加盐哈希"""
if salt is None:
salt = secrets.token_hex(16)
hashed = hashlib.sha256(f"{salt}:{password}".encode()).hexdigest()
return f"{salt}${hashed}"
def _verify_password(plain_password: str, stored_hash: str) -> bool:
"""验证密码"""
if "$" not in stored_hash:
return False
salt, _ = stored_hash.split("$", 1)
return _hash_password(plain_password, salt) == stored_hash
# ==================== 模型 ====================
class LoginRequest(BaseModel):
username: str
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
expires_in: int # 秒
class UserInfo(BaseModel):
username: str
# ==================== 用户存储(简单内存版) ====================
# 启动时对默认密码做哈希
_users_db: dict[str, str] = {
DEFAULT_USERNAME: _hash_password(DEFAULT_PASSWORD),
}
def authenticate_user(username: str, password: str) -> Optional[str]:
"""验证用户,成功返回用户名,失败返回 None"""
hashed = _users_db.get(username)
if not hashed:
return None
if not _verify_password(password, hashed):
return None
return username
# ==================== JWT 签发/验证 ====================
def create_access_token(username: str) -> tuple[str, int]:
"""创建 JWT token,返回 (token, expires_in_seconds)"""
expires_delta = timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
expire = datetime.now(timezone.utc) + expires_delta
payload = {
"sub": username,
"exp": expire,
"iat": datetime.now(timezone.utc),
}
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
return token, int(expires_delta.total_seconds())
def verify_token(token: str) -> Optional[str]:
"""验证 JWT token,成功返回用户名,失败返回 None"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
# 检查用户是否仍然存在
if username not in _users_db:
return None
return username
except JWTError:
return None
# ==================== FastAPI 依赖 ====================
async def get_current_user(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> str:
"""
从请求中提取并验证 JWT token
支持两种方式:
1. Authorization: Bearer <token> (标准方式)
2. Cookie: access_token=<token> (浏览器便捷方式)
"""
token = None
# 方式1: Authorization header
if credentials and credentials.credentials:
token = credentials.credentials
# 方式2: Cookie fallback
if not token:
token = request.cookies.get("access_token")
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="未登录,请先登录",
headers={"WWW-Authenticate": "Bearer"},
)
username = verify_token(token)
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="登录已过期,请重新登录",
headers={"WWW-Authenticate": "Bearer"},
)
return username
+248
View File
@@ -0,0 +1,248 @@
"""项目配置"""
import os
from pathlib import Path
from dotenv import load_dotenv
# 项目根目录
PROJECT_ROOT = Path(__file__).parent.parent.parent
# 加载项目根目录下的 .env(若存在),使 LLM 等配置可外部覆盖
load_dotenv(PROJECT_ROOT / ".env")
DATA_DIR = PROJECT_ROOT / "data"
TEMPLATES_DIR = PROJECT_ROOT / "templates" / "era1"
STATIC_DIR = PROJECT_ROOT / "backend" / "static"
OUTPUT_DIR = PROJECT_ROOT / "output"
# 数据文件路径
EXCEL_BASIC_INFO = DATA_DIR / "长宁区_基础信息表.xlsx"
EXCEL_COURSE_IMPL = DATA_DIR / "长宁区_课程实施情况表.xlsx"
EXCEL_SUBJECT_IMPL = DATA_DIR / "长宁区_学科课程实施情况表.xlsx"
EXCEL_SCORING_RULES = DATA_DIR / "赋分整理表.xlsx"
# =====================================================================
# LLM 配置 —— 全项目唯一真相源 (Single Source of Truth)
# 任何模块(backend / scripts / templates context)都应从此处读取,
# 不要在其他文件中重复硬编码 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL。
# 敏感信息(尤其 API Key)不写入代码,仅从环境变量 / 项目根目录 .env 读取。
# =====================================================================
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "")
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
LLM_MODEL = os.environ.get("LLM_MODEL", "")
LLM_MAX_CONCURRENCY = int(os.environ.get("LLM_MAX_CONCURRENCY", "5"))
if not (LLM_BASE_URL and LLM_API_KEY and LLM_MODEL):
raise RuntimeError(
"缺少 LLM 配置:请在项目根目录 .env 中设置 "
"LLM_BASE_URL / LLM_API_KEY / LLM_MODEL(参考 .env.example"
)
# PCA标准化参数
PCA_MEAN = 50
PCA_STD = 10
# 学校名称标准化映射(确保各表一致)
SCHOOL_NAME_MAP = {
"市三女中": "市三女中",
"复旦中学": "复旦中学",
"仙霞高中": "仙霞高中",
"建青实验": "建青实验",
"延安中学": "延安中学",
"华政附中": "华政附中",
"民办新虹桥": "民办新虹桥",
"天山学校": "天山学校",
"西郊学校": "西郊学校",
}
# 学校类型
SCHOOL_TYPE_MAP = {
"延安中学": {"type": "市实验性示范性高中", "code": "A类学校", "nature": "公办", "feature": "非特色高中"},
"复旦中学": {"type": "市实验性示范性高中", "code": "A类学校", "nature": "公办", "feature": "非特色高中"},
"市三女中": {"type": "区实验性示范性高中", "code": "B类学校", "nature": "公办", "feature": "非特色高中"},
"建青实验": {"type": "区实验性示范性高中", "code": "B类学校", "nature": "公办", "feature": "非特色高中"},
"华政附中": {"type": "特色高中", "code": "B类学校", "nature": "公办", "feature": "人文类特色高中"},
"仙霞高中": {"type": "公办普通高中", "code": "C类学校", "nature": "公办", "feature": "非特色高中"},
"天山学校": {"type": "公办普通高中", "code": "C类学校", "nature": "公办", "feature": "非特色高中"},
"西郊学校": {"type": "公办普通高中", "code": "C类学校", "nature": "公办", "feature": "非特色高中"},
"民办新虹桥": {"type": "民办高中", "code": "C类学校", "nature": "民办", "feature": "非特色高中"},
}
# 15个学科
SUBJECTS = [
"语文", "数学", "英语", "物理", "化学", "生物学",
"历史", "地理", "思想政治", "体育与健康",
"信息技术", "通用技术", "艺术", "音乐", "美术"
]
# 七大维度体系
DIMENSION_FRAMEWORK = {
"课程领导力": {
"sub_dimensions": ["国家标准遵循", "课程结构建设", "课程规范落实"],
},
"教学变革力": {
"sub_dimensions": ["教学方式变革", "作业设计与管理变革"],
},
"学生发展指导力": {
"sub_dimensions": ["学科发展的个性化辅导", "学生生涯发展指导"],
},
"教师发展支持力": {
"sub_dimensions": ["培训支持", "教研支持", "项目支持"],
},
"教育质量评估力": {
"sub_dimensions": ["科学评价观", "学业质量评估", "综合素质评估", "实践活动评估"],
},
"教育条件保障力": {
"sub_dimensions": ["区域推进", "环境支持", "资源支持"],
},
"数字化赋能力": {
"sub_dimensions": ["教学方式创新", "评价精准化与个性化", "课程迭代优化"],
},
}
# 三级维度水平划分阈值 (从赋分整理表和报告中提取)
LEVEL_THRESHOLDS = {
"国家标准遵循": {"level4": 57, "level3": 53.5, "level2": 43},
"课程结构建设": {"level4": 55.5, "level3": 49, "level2": 45},
"课程规范落实": {"level4": 58, "level3": 50, "level2": 44},
"教学方式变革": {"level4": 54, "level3": 49, "level2": 45},
"作业设计与管理变革": {"level4": 54, "level3": 49, "level2": 45},
"学科发展的个性化辅导": {"level4": 60, "level3": 50, "level2": 46},
"学生生涯发展指导": {"level4": 56, "level3": 50, "level2": 42},
"培训支持": {"level4": 51, "level3": 49, "level2": 47.5},
"教研支持": {"level4": 55, "level3": 50, "level2": 47},
"项目支持": {"level4": 56.5, "level3": 50, "level2": 44},
"科学评价观": {"level4": 57, "level3": 50, "level2": 43},
"学业质量评估": {"level4": 54, "level3": 46, "level2": 41},
"综合素质评估": {"level4": 58.5, "level3": 50, "level2": 39},
"实践活动评估": {"level4": 50, "level3": 46.5, "level2": 43},
"区域推进": {"level4": 59, "level3": 52, "level2": 40},
"环境支持": {"level4": 56, "level3": 49.5, "level2": 41},
"资源支持": {"level4": 57.5, "level3": 49, "level2": 42},
"教学方式创新": {"level4": 54, "level3": 45, "level2": 40},
"评价精准化与个性化": {"level4": 60, "level3": 50, "level2": 40},
"课程迭代优化": {"level4": 55, "level3": 50, "level2": 45},
}
# 水平的质性描述(从报告表1-2提取)
LEVEL_DESCRIPTIONS = {
"国家标准遵循": {
4: "所有科目必修课程开齐开足,选必和选修满足要求",
3: "考试类科目必修开齐,选必和选修满足要求",
2: "必修未能开齐开足,选必和选修有一个满足要求",
1: "必修未能开齐开足,选必和选修均不满足要求",
},
"课程结构建设": {
4: "学科类必修课程离差总和在30%以内,总体三类课程在150%以下,校本课程和综合实践较好",
3: "总体三类课程离差总和在250%以下,校本课程和综合实践高于平均水平",
2: "总体三类课程离差总和在300%以下,校本课程和综合实践较差",
1: "总体三类课程的离差和很高,校本课程和综合实践在末尾25%",
},
"课程规范落实": {
4: "都有建设规范文本和档案记录",
3: "都有建设规范文本,但过程性档案记录已经全部建成,部分有尚未使用",
2: "部分有建设规范文本,档案大部分已经建成但未使用",
1: "基本没有建设规范文本,档案尚未建成",
},
"教学方式变革": {
4: "所有老师有共识和研究,并能够系统设计、有效实施,至少有3种常态化落实形式",
3: "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式",
2: "个别老师有研究,并能够偶尔引导学生开展学习,至少有1种常态化落实形式",
1: "基本没有教学方法等相关研究,基本不组织相关学习,没有或仅有1种落实形式",
},
"作业设计与管理变革": {
4: "在每类创新性作业中至少掌握3种类型,作业时长有统一控制管理,定期批改评价,有多元化属性标注",
3: "在每类创新性作业中至少掌握2种类型,偶有时长控制管理,面批为主,有至少两种属性标注",
2: "至少掌握1种类型或擅长某1-2种创新作业,学生自己控制时长,很少反馈,有属性标注",
1: "擅长某种创新作业,学生自己控制时长,几乎不反馈,无属性标注",
},
"学科发展的个性化辅导": {
4: "各学科平均辅导时长每周2小时以上,教师根据学情确定内容,采取个别辅导方式",
3: "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导",
2: "各学科平均辅导时长每周1小时以内,学生提出需求后教师辅导,分组统一或分散辅导",
1: "辅导时长每周1小时以内或不辅导,学生提出需求后教师辅导,班级统一辅导",
},
"学生生涯发展指导": {
4: "专设生涯指导课程,三年覆盖90%+学生,本校+外聘教师队伍,校内外资源足够支持",
3: "外请讲座实施,三年覆盖70-90%学生,外聘教师队伍,校内外资源有一些支持",
2: "与社会考察/志愿服务结合实施,三年覆盖50-70%学生,外聘教师,几乎无资源支持",
1: "与社会考察/志愿服务结合实施,三年覆盖50%以下,未形成稳定队伍,几乎无资源支持",
},
"培训支持": {
4: "各学科教师平均外出培训人数≥2.5人",
3: "各学科教师平均外出培训人数≥1.5人",
2: "各学科教师平均外出培训人数≥1人",
1: "各学科教师几乎不进行外出培训",
},
"教研支持": {
4: "教研的数量和质量均较高",
3: "有教研活动,质量较好",
2: "有教研活动但质量一般",
1: "活动数量和质量均较低",
},
"项目支持": {
4: "各学科均有负责的校级以上项目至少一个",
3: "有部分学科负责校级以上项目至少一个",
2: "各学科没有负责的校级以上项目,部分学科有校级项目至少一个",
1: "各学科校级及校级以上的项目均没有",
},
"科学评价观": {
4: "在所有方面均能至少关注两项素养发展",
3: "至少有三个方面关注两项素养发展",
2: "能较多关注学生发展",
1: "较少关注学生发展",
},
"学业质量评估": {
4: "校本化评价工具已研制并使用,学期考试质量分析并标注属性较为全面",
3: "校本化评价工具已研制并使用,学期考试质量分析不做硬性要求",
2: "至少已研制一项校本化评价工具,学期考试质量分析不做硬性要求",
1: "未能重视学业质量评估,校本化评价工具均欠缺",
},
"综合素质评估": {
4: "校本化综合素质评价体系均有建设和使用,有平台支持且评价结果表达科学",
3: "校本化综合素质评价体系均有建设和使用,支持平台和评价结果使用有待提高",
2: "校本化综合素质评价方案建成,但具体评价工具有待开发",
1: "未能重视校本化综合素质评价,方案、工具和结果使用等均欠缺",
},
"实践活动评估": {
4: "研究性学习、社会考察和学科实践活动的校本化评价工具已研制并使用",
3: "学科实践活动的校本化评价工具已研制并使用,研究性学习/社会考察至少一项已研制但尚未使用",
2: "学科实践活动的校本化评价工具已研制",
1: "研究性学习、社会考察和学科实践活动的校本化评价工具均尚未研制和使用",
},
"区域推进": {
4: "召开会议平均一个月2次及以上,区域管理文件数量和措施均为最大值",
3: "召开会议平均一个月1次及以上,区域管理文件3个以上,措施达3项",
2: "召开会议、文件和措施至少有一项建设较好",
1: "召开会议、文件和措施三项均建设较差",
},
"环境支持": {
4: "信息化支持和硬件支持均处于较高水平",
3: "信息化支持和硬件在平均水平附近",
2: "信息化支持和硬件支持至少有一项建设较好",
1: "信息化支持和硬件支持均建设较差",
},
"资源支持": {
4: "校内外资源和师资水平均处于较高水平",
3: "校内外资源至少有一项供给较好,师资水平较好",
2: "校内外资源至少有一项仅略低于平均水平,师资水平略低于平均水平",
1: "校内外资源和师资水平三项均建设较差",
},
"教学方式创新": {
4: "信息技术与教学融合程度高,教师能常态化使用信息技术",
3: "信息技术与教学融合程度较高,教师能使用信息技术",
2: "信息技术与教学融合程度一般,学校有信息化平台建设",
1: "信息技术与教学融合程度较差,教师基本不使用信息技术",
},
"评价精准化与个性化": {
4: "有自建信息技术平台支持学科诊断与综合素质评价",
3: "借助第三方平台支持学科诊断与综合素质评价",
2: "有信息技术平台支持学业评价",
1: "没有信息技术平台支持评价",
},
"课程迭代优化": {
4: "学校对促进教学数字化转型有专项研修计划并开展相关活动,业务绝大部分使用信息化系统",
3: "学校尚未制定专项研修计划,业务绝大部分使用信息化系统",
2: "学校较少开展数字化转型活动,少数业务使用信息化系统",
1: "学校几乎不开展数字化转型活动,尚未建成信息化管理系统",
},
}
+1
View File
@@ -0,0 +1 @@
+255
View File
@@ -0,0 +1,255 @@
"""
数据引擎:Excel解析 + 数据清洗 + 赋分计算
负责将原始Excel数据转换为每所学校的结构化得分数据
"""
import pandas as pd
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import logging
from ..config import (
EXCEL_BASIC_INFO, EXCEL_COURSE_IMPL, EXCEL_SUBJECT_IMPL,
SCHOOL_NAME_MAP, SUBJECTS, SCHOOL_TYPE_MAP,
)
logger = logging.getLogger(__name__)
class DataEngine:
"""数据引擎:读取Excel、清洗、结构化"""
def __init__(self, data_dir: Optional[Path] = None):
self.data_dir = data_dir
self._basic_info: Optional[pd.DataFrame] = None
self._course_impl: Optional[pd.DataFrame] = None
self._subject_impl: Optional[pd.DataFrame] = None
def load_all(self) -> None:
"""加载所有Excel数据"""
logger.info("开始加载Excel数据...")
self._basic_info = self._load_basic_info()
self._course_impl = self._load_course_impl()
self._subject_impl = self._load_subject_impl()
logger.info(
f"数据加载完成: 基础信息={len(self._basic_info)}行, "
f"课程实施={len(self._course_impl)}行, "
f"学科课程={len(self._subject_impl)}"
)
def _standardize_school_name(self, df: pd.DataFrame, col: str) -> pd.DataFrame:
"""标准化学校名称"""
df[col] = df[col].map(lambda x: SCHOOL_NAME_MAP.get(x, x))
return df
def _load_basic_info(self) -> pd.DataFrame:
"""加载基础信息表"""
df = pd.read_excel(EXCEL_BASIC_INFO)
df = self._standardize_school_name(df, "学校名称")
return df
def _load_course_impl(self) -> pd.DataFrame:
"""加载课程实施情况表"""
df = pd.read_excel(EXCEL_COURSE_IMPL)
df = self._standardize_school_name(df, "学校简称")
# 统一列名
df = df.rename(columns={"学校简称": "学校名称"})
return df
def _load_subject_impl(self) -> pd.DataFrame:
"""加载学科课程实施情况表"""
df = pd.read_excel(EXCEL_SUBJECT_IMPL)
df = self._standardize_school_name(df, "学校简称")
df = df.rename(columns={"学校简称": "学校名称"})
return df
@property
def schools(self) -> List[str]:
"""获取所有学校名称列表"""
if self._course_impl is None:
self.load_all()
return sorted(self._course_impl["学校名称"].unique().tolist())
def get_school_basic_info(self, school: str) -> Dict:
"""获取学校基本信息"""
if self._basic_info is None:
self.load_all()
df = self._basic_info[self._basic_info["学校名称"] == school]
info = SCHOOL_TYPE_MAP.get(school, {})
# 提取关键字段
def _get_field(field_name):
rows = df[df["字段名称"] == field_name]
if len(rows) > 0:
return rows.iloc[0]["字段值"]
return None
info.update({
"school_name": school,
"建校年份": _get_field("建校年份"),
"教师总数": _get_field("学校教师总数"),
"占地面积": _get_field("占地面积"),
"建筑面积": _get_field("建筑面积"),
})
return info
def get_school_course_data(self, school: str) -> pd.DataFrame:
"""获取某学校的课程实施情况数据"""
if self._course_impl is None:
self.load_all()
return self._course_impl[self._course_impl["学校名称"] == school].copy()
def get_school_subject_data(self, school: str, subject: Optional[str] = None) -> pd.DataFrame:
"""获取某学校的学科课程实施数据"""
if self._subject_impl is None:
self.load_all()
df = self._subject_impl[self._subject_impl["学校名称"] == school].copy()
if subject:
df = df[df["学科"] == subject]
return df
# ========== 课程领导力相关数据提取 ==========
def get_weekly_hours(self, school: str) -> pd.DataFrame:
"""获取学校各学科各年级各学期的周课时数据"""
df = self.get_school_course_data(school)
# 筛选周课时相关字段
hours_fields = ["学科必修课周课时", "学科选择性必修课周课时", "学科类选修课周课时"]
result = df[df["字段名称"].isin(hours_fields)].copy()
result["字段取值"] = pd.to_numeric(result["字段取值"], errors="coerce")
return result
def get_course_norms(self, school: str) -> Dict:
"""获取学校课程规范落实相关数据(建设规范、档案等)"""
df = self.get_school_course_data(school)
norm_keywords = ["建设规范文本", "档案", "已经建成并使用", "尚未建成"]
mask = df["字段名称"].apply(
lambda x: any(k in str(x) for k in norm_keywords) if pd.notna(x) else False
)
return df[mask][["字段名称", "字段取值"]].to_dict("records")
# ========== 教学变革力相关数据提取 ==========
def get_teaching_reform_data(self, school: str) -> Dict:
"""获取教学方式变革相关数据(认识程度、落实程度、实施方式)"""
df = self.get_school_subject_data(school)
reform_keywords = [
"认识程度", "落实程度", "认识", "落实",
"理解式学习", "自主性学习", "实践性学习", "跨学科学习",
"信息技术与教学融合", "信息融入教学",
]
mask = df["字段名称"].apply(
lambda x: any(k in str(x) for k in reform_keywords) if pd.notna(x) else False
)
return df[mask]
def get_homework_data(self, school: str) -> Dict:
"""获取作业设计与管理数据"""
df = self.get_school_subject_data(school)
hw_keywords = [
"作业", "实践类", "表现类", "跨学科", "团队合作",
"批改", "评价", "属性标注", "时长控制",
]
mask = df["字段名称"].apply(
lambda x: any(k in str(x) for k in hw_keywords) if pd.notna(x) else False
)
return df[mask]
# ========== 通用数据提取方法 ==========
def get_field_value(self, school: str, source: str, field_name: str,
subject: Optional[str] = None) -> Optional[str]:
"""通用字段值获取"""
if source == "basic":
df = self._basic_info[self._basic_info["学校名称"] == school]
col = "字段名称"
val_col = "字段值"
elif source == "course":
df = self.get_school_course_data(school)
col = "字段名称"
val_col = "字段取值"
elif source == "subject":
df = self.get_school_subject_data(school, subject)
col = "字段名称"
val_col = "字段取值"
else:
return None
rows = df[df[col] == field_name]
if len(rows) > 0:
return rows.iloc[0][val_col]
return None
def get_field_by_id(self, school: str, source: str, field_id: str,
subject: Optional[str] = None) -> List[Dict]:
"""通过字段ID获取数据"""
if source == "basic":
df = self._basic_info[self._basic_info["学校名称"] == school]
elif source == "course":
df = self.get_school_course_data(school)
elif source == "subject":
df = self.get_school_subject_data(school, subject)
else:
return []
rows = df[df["字段ID"] == field_id]
return rows.to_dict("records")
def build_score_matrix(self) -> pd.DataFrame:
"""
构建所有学校×所有字段的得分矩阵
这是赋分引擎的输入
"""
if self._course_impl is None:
self.load_all()
records = []
for school in self.schools:
# 课程实施表数据
course_df = self.get_school_course_data(school)
for _, row in course_df.iterrows():
records.append({
"学校": school,
"来源": "course",
"题号": row.get("题号"),
"字段ID": row.get("字段ID"),
"字段名称": row.get("字段名称"),
"字段取值": row.get("字段取值"),
"学科": row.get("学科", "不分学科"),
"年级": row.get("年级", "不分年级"),
"学期": row.get("学期", ""),
})
# 学科课程表数据
subject_df = self.get_school_subject_data(school)
for _, row in subject_df.iterrows():
records.append({
"学校": school,
"来源": "subject",
"题号": row.get("题号"),
"字段ID": row.get("字段ID"),
"字段名称": row.get("字段名称"),
"字段取值": row.get("字段取值"),
"学科": row.get("学科", "不分学科"),
"年级": "",
"学期": "",
})
matrix = pd.DataFrame(records)
logger.info(f"得分矩阵构建完成: {len(matrix)}行, {len(self.schools)}所学校")
return matrix
def summary(self) -> Dict:
"""数据摘要"""
if self._basic_info is None:
self.load_all()
return {
"schools": self.schools,
"school_count": len(self.schools),
"basic_info_rows": len(self._basic_info),
"course_impl_rows": len(self._course_impl),
"subject_impl_rows": len(self._subject_impl),
"subjects": SUBJECTS,
"school_types": {s: SCHOOL_TYPE_MAP[s]["type"] for s in self.schools},
}
File diff suppressed because it is too large Load Diff
+777
View File
@@ -0,0 +1,777 @@
"""
报告渲染引擎:将数据+LLM文字+模板组装成最终HTML报告
"""
from datetime import datetime
from pathlib import Path
from typing import Dict, List
import numpy as np
from jinja2 import Environment, FileSystemLoader
from ..config import (
TEMPLATES_DIR, DIMENSION_FRAMEWORK, LEVEL_DESCRIPTIONS,
OUTPUT_DIR, SCHOOL_TYPE_MAP,
)
class ReportRenderer:
"""HTML报告渲染引擎"""
def __init__(self):
self.env = Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=False, # 允许HTML直接渲染
)
def render(self, report_data: Dict, llm_sections: Dict[str, str]) -> str:
"""
渲染完整HTML报告
report_data: stats_engine.compute_school_report_data() 的输出
llm_sections: llm_engine.generate_report_segments() 的输出
"""
template = self.env.get_template("base.html")
# 准备模板数据
school = report_data["school"]
context = {
"school": school,
"school_info": report_data["school_info"],
"overall": self._to_namespace(report_data["overall"]),
"dimensions": {
name: self._to_namespace(data)
for name, data in report_data["dimensions"].items()
},
"sub_dimensions": {
name: self._to_namespace(data)
for name, data in report_data["sub_dimensions"].items()
},
"framework": {
name: self._to_namespace(info)
for name, info in DIMENSION_FRAMEWORK.items()
},
"llm_sections": llm_sections,
"level_descriptions": LEVEL_DESCRIPTIONS,
"generation_date": datetime.now().strftime("%Y年%m月%d"),
# ECharts数据 — 传原始dict,由模板的tojson过滤器序列化一次
"radar_data": self._build_radar_data(report_data),
"sub_dim_chart_data": self._build_sub_dim_charts(report_data),
"score_compare_data": self._build_score_compare(report_data),
"cluster_radar_data": self._build_cluster_radar(report_data),
"correlation_data": self._build_correlation_heatmap(report_data),
"level_dist_data": self._build_level_distribution(report_data),
"school_ranking_data": self._build_school_ranking(report_data),
# 新增图表数据
"cluster_type_dist_data": self._build_cluster_type_distribution(report_data),
"cluster_line_compare_data": self._build_cluster_line_compare(report_data),
"dim_scatter_data": self._build_dim_scatter_charts(report_data),
"dim_score_bar_data": self._build_dim_score_bars(report_data),
"dim_sub_radar_data": self._build_dim_sub_radar_charts(report_data),
# 创新图表
"profile_card_data": self._build_profile_card(report_data),
"quadrant_data": self._build_quadrant_chart(report_data),
"thermometer_data": self._build_thermometer_data(report_data),
"waterfall_data": self._build_waterfall_chart(report_data),
}
return template.render(**context)
def render_to_file(self, report_data: Dict, llm_sections: Dict[str, str],
output_path: Path = None) -> Path:
"""渲染并保存到文件"""
html = self.render(report_data, llm_sections)
school = report_data["school"]
if output_path is None:
output_dir = OUTPUT_DIR
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{school}_报告.html"
output_path.write_text(html, encoding="utf-8")
return output_path
def _build_radar_data(self, report_data: Dict) -> Dict:
"""构建雷达图数据"""
school = report_data["school"]
dims = list(report_data["dimensions"].keys())
school_values = [report_data["dimensions"][d]["score"] for d in dims]
avg_values = [report_data["dimensions"][d]["district_avg"] for d in dims]
return {
"dimensions": dims,
"legend": [school, "区均值"],
"series": [
{"name": school, "values": school_values},
{"name": "区均值", "values": avg_values},
],
}
def _build_sub_dim_charts(self, report_data: Dict) -> Dict:
"""构建各维度的子维度柱状图数据"""
charts = {}
part_names = {
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
"教师发展支持力": "part6", "教育质量评估力": "part7",
"教育条件保障力": "part8", "数字化赋能力": "part9",
}
for dim_name, info in DIMENSION_FRAMEWORK.items():
part_id = part_names[dim_name]
sub_dims = info["sub_dimensions"]
categories = []
school_values = []
avg_values = []
for sd in sub_dims:
sd_data = report_data["sub_dimensions"].get(sd, {})
if sd_data:
categories.append(sd)
school_values.append(round(sd_data["score"], 2))
avg_values.append(round(sd_data["district_avg"], 2))
charts[part_id] = {
"categories": categories,
"school_values": school_values,
"avg_values": avg_values,
}
return charts
def _build_score_compare(self, report_data: Dict) -> Dict:
"""构建得分对比横向条形图数据:本校 vs 区均值 vs 同类学校均值"""
school = report_data["school"]
dims = list(report_data["dimensions"].keys())
return {
"categories": dims,
"school_values": [round(report_data["dimensions"][d]["score"], 2) for d in dims],
"district_avg": [round(report_data["dimensions"][d]["district_avg"], 2) for d in dims],
"same_type_avg": [round(report_data["dimensions"][d]["same_type_avg"], 2) for d in dims],
"school_name": school,
}
def _build_cluster_radar(self, report_data: Dict) -> Dict:
"""构建聚类类型特征对比雷达图(较好类 vs 待提升类)"""
all_dim_scores = report_data.get("all_schools_dim_scores", {})
if not all_dim_scores:
return {}
# 从 overall cluster info 中提取各学校的聚类标签
school = report_data["school"]
dims = list(report_data["dimensions"].keys())
# 计算各学校的总体得分来判断聚类
school_totals = {}
for s in list(list(all_dim_scores.values())[0].keys()):
total = 0
for d in dims:
total += all_dim_scores.get(d, {}).get(s, 50)
school_totals[s] = total / len(dims)
# 二分聚类(简单按总分中位数分)
median_score = sorted(school_totals.values())[len(school_totals) // 2]
good_schools = [s for s, v in school_totals.items() if v >= median_score]
weak_schools = [s for s, v in school_totals.items() if v < median_score]
good_avgs = []
weak_avgs = []
for d in dims:
dim_data = all_dim_scores.get(d, {})
good_avgs.append(round(sum(dim_data.get(s, 50) for s in good_schools) / max(len(good_schools), 1), 2))
weak_avgs.append(round(sum(dim_data.get(s, 50) for s in weak_schools) / max(len(weak_schools), 1), 2))
return {
"dimensions": dims,
"legend": ["课程实施较好类", "课程实施待提升类", school],
"series": [
{"name": "课程实施较好类", "values": good_avgs},
{"name": "课程实施待提升类", "values": weak_avgs},
{"name": school, "values": [round(report_data["dimensions"][d]["score"], 2) for d in dims]},
],
"good_count": len(good_schools),
"weak_count": len(weak_schools),
}
def _build_correlation_heatmap(self, report_data: Dict) -> Dict:
"""构建维度间相关性热力图"""
correlation = report_data.get("correlation", {})
if not correlation:
return {}
dims = list(correlation.keys())
# 构建二维数组 [x_index, y_index, value]
data = []
for i, d1 in enumerate(dims):
for j, d2 in enumerate(dims):
val = correlation.get(d1, {}).get(d2, 0)
data.append([i, j, round(val, 3) if val is not None else 0])
# 短名
short_names = [d.replace("", "").replace("教育", "") for d in dims]
return {
"dimensions": dims,
"short_names": short_names,
"data": data,
}
def _build_level_distribution(self, report_data: Dict) -> Dict:
"""构建各三级维度水平分布堆叠条形图"""
charts = {}
part_names = {
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
"教师发展支持力": "part6", "教育质量评估力": "part7",
"教育条件保障力": "part8", "数字化赋能力": "part9",
}
for dim_name, info in DIMENSION_FRAMEWORK.items():
part_id = part_names[dim_name]
sub_dims = info["sub_dimensions"]
categories = []
level1_pcts = []
level2_pcts = []
level3_pcts = []
level4_pcts = []
school_levels = []
for sd in sub_dims:
sd_data = report_data["sub_dimensions"].get(sd, {})
if not sd_data:
continue
dist = sd_data.get("level_distribution", {})
total = sum(dist.values())
if total == 0:
continue
categories.append(sd)
level1_pcts.append(round(dist.get("水平1", 0) / total * 100, 1))
level2_pcts.append(round(dist.get("水平2", 0) / total * 100, 1))
level3_pcts.append(round(dist.get("水平3", 0) / total * 100, 1))
level4_pcts.append(round(dist.get("水平4", 0) / total * 100, 1))
school_levels.append(sd_data.get("level", 0))
charts[part_id] = {
"categories": categories,
"level1": level1_pcts,
"level2": level2_pcts,
"level3": level3_pcts,
"level4": level4_pcts,
"school_levels": school_levels,
}
return charts
def _build_school_ranking(self, report_data: Dict) -> Dict:
"""构建区内各校维度排名对比图"""
school = report_data["school"]
all_dim_scores = report_data.get("all_schools_dim_scores", {})
dims = list(report_data["dimensions"].keys())
if not all_dim_scores:
return {}
# 获取所有学校名
first_dim = list(all_dim_scores.values())[0] if all_dim_scores else {}
all_schools = list(first_dim.keys())
# 计算每校总体得分并排序
school_totals = {}
for s in all_schools:
total = sum(all_dim_scores.get(d, {}).get(s, 50) for d in dims)
school_totals[s] = round(total / len(dims), 2)
sorted_schools = sorted(school_totals.items(), key=lambda x: x[1], reverse=True)
return {
"schools": [s[0] for s in sorted_schools],
"total_scores": [s[1] for s in sorted_schools],
"current_school": school,
"dimensions": dims,
"dim_scores": {
d: [round(all_dim_scores.get(d, {}).get(s[0], 50), 2) for s in sorted_schools]
for d in dims
},
}
def _build_cluster_type_distribution(self, report_data: Dict) -> Dict:
"""
构建课程实施类型分布饼/条形图数据(如参考报告图2-2)
展示 较好类 vs 待提升类 在区内各校的分布
"""
all_dim_scores = report_data.get("all_schools_dim_scores", {})
if not all_dim_scores:
return {}
school = report_data["school"]
dims = list(report_data["dimensions"].keys())
# 计算各学校总体得分并二分聚类
school_totals = {}
first_dim_data = list(all_dim_scores.values())[0] if all_dim_scores else {}
all_schools = list(first_dim_data.keys())
for s in all_schools:
total = sum(all_dim_scores.get(d, {}).get(s, 50) for d in dims)
school_totals[s] = total / len(dims)
median_score = sorted(school_totals.values())[len(school_totals) // 2]
good_schools = [s for s, v in school_totals.items() if v >= median_score]
weak_schools = [s for s, v in school_totals.items() if v < median_score]
school_cluster = "较好" if school in good_schools else "待提升"
return {
"good_count": len(good_schools),
"weak_count": len(weak_schools),
"total": len(all_schools),
"school_cluster": school_cluster,
"school_name": school,
"good_schools": good_schools,
"weak_schools": weak_schools,
}
def _build_cluster_line_compare(self, report_data: Dict) -> Dict:
"""
构建两类学校特征折线对比图(如参考报告图2-3)
两条折线:较好类 vs 待提升类在7个维度上的得分
"""
all_dim_scores = report_data.get("all_schools_dim_scores", {})
if not all_dim_scores:
return {}
school = report_data["school"]
dims = list(report_data["dimensions"].keys())
# 计算聚类
first_dim_data = list(all_dim_scores.values())[0] if all_dim_scores else {}
all_schools = list(first_dim_data.keys())
school_totals = {}
for s in all_schools:
total = sum(all_dim_scores.get(d, {}).get(s, 50) for d in dims)
school_totals[s] = total / len(dims)
median_score = sorted(school_totals.values())[len(school_totals) // 2]
good_schools = [s for s, v in school_totals.items() if v >= median_score]
weak_schools = [s for s, v in school_totals.items() if v < median_score]
good_avgs = []
weak_avgs = []
for d in dims:
dim_data = all_dim_scores.get(d, {})
good_avgs.append(round(sum(dim_data.get(s, 50) for s in good_schools) / max(len(good_schools), 1), 2))
weak_avgs.append(round(sum(dim_data.get(s, 50) for s in weak_schools) / max(len(weak_schools), 1), 2))
return {
"dimensions": dims,
"good_values": good_avgs,
"weak_values": weak_avgs,
"good_count": len(good_schools),
"weak_count": len(weak_schools),
}
def _build_dim_scatter_charts(self, report_data: Dict) -> Dict:
"""
构建各二级维度的聚类散点图数据(2D / 3D)
参考报告中每个维度都有一个散点图显示所有学校的聚类分布
对于有2个子维度的 → 2D散点图
对于有3个子维度的 → 3D散点图
对于有4个子维度的 → 取前2个主成分的2D散点图
"""
all_sub_scores = report_data.get("all_schools_sub_scores", {})
if not all_sub_scores:
return {}
school = report_data["school"]
charts = {}
part_names = {
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
"教师发展支持力": "part6", "教育质量评估力": "part7",
"教育条件保障力": "part8", "数字化赋能力": "part9",
}
first_sub = list(all_sub_scores.values())[0] if all_sub_scores else {}
all_schools = list(first_sub.keys())
for dim_name, info in DIMENSION_FRAMEWORK.items():
part_id = part_names[dim_name]
sub_dims = info["sub_dimensions"]
n_subs = len(sub_dims)
# 获取各学校在这些子维度上的得分
school_scores = {}
for s in all_schools:
scores = []
for sd in sub_dims:
val = all_sub_scores.get(sd, {}).get(s, 50)
scores.append(round(float(val), 2))
school_scores[s] = scores
# 简单二分聚类
totals = {s: sum(v) / len(v) for s, v in school_scores.items()}
med = sorted(totals.values())[len(totals) // 2]
clusters = {s: 0 if totals[s] >= med else 1 for s in all_schools}
if n_subs == 2:
# 2D散点图
data_good = []
data_weak = []
school_point = None
for s in all_schools:
point = school_scores[s]
if s == school:
school_point = point
elif clusters[s] == 0:
data_good.append(point)
else:
data_weak.append(point)
charts[part_id] = {
"type": "2d",
"axes": sub_dims,
"good_data": data_good,
"weak_data": data_weak,
"school_point": school_point,
"school_name": school,
"dim_name": dim_name,
}
elif n_subs == 3:
# 3D散点图
data_good = []
data_weak = []
school_point = None
for s in all_schools:
point = school_scores[s]
if s == school:
school_point = point
elif clusters[s] == 0:
data_good.append(point)
else:
data_weak.append(point)
charts[part_id] = {
"type": "3d",
"axes": sub_dims,
"good_data": data_good,
"weak_data": data_weak,
"school_point": school_point,
"school_name": school,
"dim_name": dim_name,
}
elif n_subs >= 4:
# 取前两个子维度做2D散点
axes = sub_dims[:2]
data_good = []
data_weak = []
school_point = None
for s in all_schools:
point = school_scores[s][:2]
if s == school:
school_point = point
elif clusters[s] == 0:
data_good.append(point)
else:
data_weak.append(point)
charts[part_id] = {
"type": "2d",
"axes": axes,
"good_data": data_good,
"weak_data": data_weak,
"school_point": school_point,
"school_name": school,
"dim_name": dim_name,
}
return charts
def _build_dim_score_bars(self, report_data: Dict) -> Dict:
"""
构建各维度独立得分柱状图数据(如参考报告图3-1、图4-1等)
展示本校 vs 区均值 vs 同类学校均值 的对比
"""
school = report_data["school"]
charts = {}
part_names = {
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
"教师发展支持力": "part6", "教育质量评估力": "part7",
"教育条件保障力": "part8", "数字化赋能力": "part9",
}
for dim_name, dim_data in report_data["dimensions"].items():
part_id = part_names.get(dim_name, "")
if not part_id:
continue
charts[part_id] = {
"dim_name": dim_name,
"school_name": school,
"school_score": round(dim_data["score"], 2),
"district_avg": round(dim_data["district_avg"], 2),
"same_type_avg": round(dim_data["same_type_avg"], 2),
}
return charts
def _build_dim_sub_radar_charts(self, report_data: Dict) -> Dict:
"""
构建各二级维度的子维度雷达图(如参考报告图3-2)
多条线对比: 本校 vs 区均值 vs 同类学校均值
"""
school = report_data["school"]
all_sub_scores = report_data.get("all_schools_sub_scores", {})
charts = {}
part_names = {
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
"教师发展支持力": "part6", "教育质量评估力": "part7",
"教育条件保障力": "part8", "数字化赋能力": "part9",
}
# Get same-type schools
school_info = report_data.get("school_info", {})
school_type = school_info.get("type", "")
all_schools = list(list(all_sub_scores.values())[0].keys()) if all_sub_scores else []
same_type_schools = [s for s in all_schools if SCHOOL_TYPE_MAP.get(s, {}).get("type") == school_type]
for dim_name, info in DIMENSION_FRAMEWORK.items():
part_id = part_names.get(dim_name, "")
if not part_id:
continue
sub_dims = info["sub_dimensions"]
school_values = []
district_avg = []
same_type_avg = []
for sd in sub_dims:
sd_data = report_data["sub_dimensions"].get(sd, {})
if sd_data:
school_values.append(round(sd_data["score"], 2))
district_avg.append(round(sd_data["district_avg"], 2))
# 计算同类学校均值
if same_type_schools and all_sub_scores:
st_vals = [all_sub_scores.get(sd, {}).get(s, 50) for s in same_type_schools]
same_type_avg.append(round(sum(float(v) for v in st_vals) / len(st_vals), 2))
else:
same_type_avg.append(district_avg[-1])
if len(sub_dims) >= 3:
charts[part_id] = {
"type": "radar",
"sub_dims": sub_dims,
"school_name": school,
"school_values": school_values,
"district_avg": district_avg,
"same_type_avg": same_type_avg,
}
else:
charts[part_id] = {
"type": "bar",
"sub_dims": sub_dims,
"school_name": school,
"school_values": school_values,
"district_avg": district_avg,
"same_type_avg": same_type_avg,
}
return charts
# ========== 创新图表数据构建 ==========
def _build_profile_card(self, report_data: Dict) -> Dict:
"""
A. 学校画像卡(综合仪表盘)
- 总体得分环形仪表盘
- 7个维度的红绿灯状态(基于各维度下子维度的最低水平)
- 20个三级维度的水平分布概览
"""
school = report_data["school"]
overall = report_data["overall"]
# 维度红绿灯:每个二级维度取其子维度的最低水平作为"短板"指示
dim_signals = []
for dim_name, dim_data in report_data["dimensions"].items():
sub_dims = DIMENSION_FRAMEWORK[dim_name]["sub_dimensions"]
levels = []
for sd in sub_dims:
sd_data = report_data["sub_dimensions"].get(sd, {})
if sd_data:
levels.append(sd_data.get("level", 0))
min_level = min(levels) if levels else 0
avg_level = round(sum(levels) / len(levels), 1) if levels else 0
dim_signals.append({
"name": dim_name,
"score": round(dim_data["score"], 2),
"min_level": min_level,
"avg_level": avg_level,
"rank": dim_data.get("rank_in_district", 0),
})
# 20个三级维度的水平分布统计
level_counts = {1: 0, 2: 0, 3: 0, 4: 0}
for sd_name, sd_data in report_data["sub_dimensions"].items():
lv = sd_data.get("level", 0)
if lv in level_counts:
level_counts[lv] += 1
return {
"school_name": school,
"school_type": report_data["school_info"].get("type", ""),
"total_score": overall["score"],
"district_avg": overall["district_avg"],
"rank": overall["rank_in_district"],
"total_schools": overall["total_schools"],
"cluster": overall.get("cluster", ""),
"dim_signals": dim_signals,
"level_counts": level_counts,
"total_sub_dims": sum(level_counts.values()),
}
def _build_quadrant_chart(self, report_data: Dict) -> Dict:
"""
B. 优势-短板象限图(Gap Analysis
X轴 = 得分, Y轴 = 与区均值的差值
四象限:右上=核心优势, 左下=急需改进, 右下=隐性风险, 左上=潜力项
"""
school = report_data["school"]
items = []
for sd_name, sd_data in report_data["sub_dimensions"].items():
parent_dim = sd_data.get("parent_dimension", "")
items.append({
"name": sd_name,
"parent": parent_dim,
"score": round(sd_data["score"], 2),
"diff": round(sd_data["diff_district"], 2),
"level": sd_data.get("level", 0),
})
return {
"school_name": school,
"items": items,
"x_center": 50, # 区均值(标准化后均值=50
"y_center": 0, # 差值=0 的参照线
}
def _build_thermometer_data(self, report_data: Dict) -> Dict:
"""
C. 维度温度计条形图数据
为每个三级维度构建横向温度计数据:
- 得分范围20-80
- 水平分界线
- 本校位置、区均值位置、同类学校均值位置
"""
school = report_data["school"]
school_type = report_data["school_info"].get("type", "")
all_sub_scores = report_data.get("all_schools_sub_scores", {})
same_type_schools = [s for s in SCHOOL_TYPE_MAP
if SCHOOL_TYPE_MAP[s].get("type") == school_type]
thermometers = {}
for sd_name, sd_data in report_data["sub_dimensions"].items():
# 同类学校均值
if same_type_schools and sd_name in all_sub_scores:
st_vals = [float(all_sub_scores[sd_name].get(s, 50)) for s in same_type_schools]
same_type_avg = round(sum(st_vals) / len(st_vals), 2)
else:
same_type_avg = round(sd_data["district_avg"], 2)
# 水平阈值
from ..config import LEVEL_THRESHOLDS
thresholds = LEVEL_THRESHOLDS.get(sd_name, {})
thermometers[sd_name] = {
"score": round(sd_data["score"], 2),
"district_avg": round(sd_data["district_avg"], 2),
"same_type_avg": same_type_avg,
"level": sd_data.get("level", 0),
"thresholds": {
"level4": thresholds.get("level4", 57),
"level3": thresholds.get("level3", 50),
"level2": thresholds.get("level2", 43),
},
}
return {
"school_name": school,
"data": thermometers,
}
def _build_waterfall_chart(self, report_data: Dict) -> Dict:
"""
D. 进步空间瀑布图
展示:如果每个低于水平3的子维度提升到水平3的阈值,总分增加多少
让校长看到"改哪几个点收益最大"
"""
from ..config import LEVEL_THRESHOLDS
school = report_data["school"]
current_total = report_data["overall"]["score"]
# 找出所有低于水平3的子维度
improvement_items = []
for sd_name, sd_data in report_data["sub_dimensions"].items():
level = sd_data.get("level", 0)
if level < 3:
current_score = sd_data["score"]
# 提升到水平3的阈值
target_score = LEVEL_THRESHOLDS.get(sd_name, {}).get("level3", 50)
gap = round(target_score - current_score, 2)
if gap > 0:
improvement_items.append({
"name": sd_name,
"parent": sd_data.get("parent_dimension", ""),
"current_score": round(current_score, 2),
"target_score": round(target_score, 2),
"gap": gap,
"current_level": level,
})
# 按收益从大到小排序
improvement_items.sort(key=lambda x: x["gap"], reverse=True)
# 估算总分提升(简化:假设20个子维度等权重影响总分)
total_sub_dims = len(report_data["sub_dimensions"])
cumulative = current_total
waterfall_steps = [{"name": "当前总分", "value": round(current_total, 2), "type": "current"}]
for item in improvement_items:
# 粗略估算:子维度提升gap分 → 总分提升 gap / total_sub_dims * 权重
# 实际PCA权重不同,此处用等权近似
estimated_gain = round(item["gap"] / total_sub_dims, 2)
cumulative += estimated_gain
waterfall_steps.append({
"name": item["name"],
"value": round(estimated_gain, 2),
"type": "gain",
"detail": f"从水平{item['current_level']}→水平3 (+{item['gap']}分)",
})
waterfall_steps.append({"name": "潜在总分", "value": round(cumulative, 2), "type": "potential"})
return {
"school_name": school,
"current_total": round(current_total, 2),
"potential_total": round(cumulative, 2),
"total_gain": round(cumulative - current_total, 2),
"steps": waterfall_steps,
"improvements": improvement_items,
}
@staticmethod
def _to_namespace(d: Dict) -> Dict:
"""将dict转为可用点号访问的对象(Jinja2兼容)"""
class Namespace(dict):
def __getattr__(self, key):
try:
return self[key]
except KeyError:
return None
return Namespace(d) if isinstance(d, dict) else d
+850
View File
@@ -0,0 +1,850 @@
"""
赋分引擎:将原始题目回答按赋分规则转换为分数
基于赋分整理表的规则,实现各类赋分函数
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple
import logging
import re
from ..config import DIMENSION_FRAMEWORK, SUBJECTS
from .data_engine import DataEngine
logger = logging.getLogger(__name__)
class ScoringEngine:
"""
赋分引擎
赋分逻辑基于"高中课程实施监测指标、题目、赋分整理0127.xlsx"
核心策略:按维度分组,从原始数据中提取相关字段,按规则赋分,
然后将赋分结果交给统计引擎做PCA合成
"""
def __init__(self, data_engine: DataEngine):
self.data_engine = data_engine
def score_all_schools(self) -> Dict[str, Dict[str, List[float]]]:
"""
对所有学校的所有维度进行赋分
返回: {
school_name: {
sub_dimension_name: [score1, score2, ...]
}
}
"""
result = {}
for school in self.data_engine.schools:
logger.info(f"正在对 {school} 进行赋分...")
result[school] = self._score_school(school)
return result
def _score_school(self, school: str) -> Dict[str, List[float]]:
"""对单个学校进行全维度赋分"""
scores = {}
# 课程领导力
scores["国家标准遵循"] = self._score_national_standard(school)
scores["课程结构建设"] = self._score_course_structure(school)
scores["课程规范落实"] = self._score_course_norms(school)
# 教学变革力
scores["教学方式变革"] = self._score_teaching_reform(school)
scores["作业设计与管理变革"] = self._score_homework_reform(school)
# 学生发展指导力
scores["学科发展的个性化辅导"] = self._score_personalized_tutoring(school)
scores["学生生涯发展指导"] = self._score_career_guidance(school)
# 教师发展支持力
scores["培训支持"] = self._score_training_support(school)
scores["教研支持"] = self._score_research_support(school)
scores["项目支持"] = self._score_project_support(school)
# 教育质量评估力
scores["科学评价观"] = self._score_scientific_evaluation(school)
scores["学业质量评估"] = self._score_academic_evaluation(school)
scores["综合素质评估"] = self._score_comprehensive_evaluation(school)
scores["实践活动评估"] = self._score_practice_evaluation(school)
# 教育条件保障力
scores["区域推进"] = self._score_regional_promotion(school)
scores["环境支持"] = self._score_environment_support(school)
scores["资源支持"] = self._score_resource_support(school)
# 数字化赋能力
scores["教学方式创新"] = self._score_digital_teaching(school)
scores["评价精准化与个性化"] = self._score_digital_evaluation(school)
scores["课程迭代优化"] = self._score_digital_curriculum(school)
return scores
# ========== 课程领导力 ==========
def _score_national_standard(self, school: str) -> List[float]:
"""
国家标准遵循:开足开齐国家课程
赋分:必修课程学分低于标准=0,高于标准=1,与标准一致=2
选必/选修课程:低于标准=0,达到标准=1
"""
scores = []
hours_df = self.data_engine.get_weekly_hours(school)
if len(hours_df) == 0:
return [1.0] # 默认中等
# 按课程类型汇总
for course_type, field_name in [
("必修", "学科必修课周课时"),
("选必", "学科选择性必修课周课时"),
("选修", "学科类选修课周课时"),
]:
type_df = hours_df[hours_df["字段名称"] == field_name]
if len(type_df) == 0:
scores.append(0.5)
continue
# 按学科汇总总课时
total = type_df.groupby("学科")["字段取值"].sum()
has_courses = (total > 0).sum()
total_hours = total.sum()
if course_type == "必修":
# 必修课:与标准一致=2,高于=1,低于=0
# 简化处理:有多少学科开了课
exam_subjects = ["语文", "数学", "英语", "物理", "化学", "生物学",
"历史", "地理", "思想政治"]
opened = sum(1 for s in exam_subjects if s in total.index and total.get(s, 0) > 0)
if opened >= len(exam_subjects):
scores.append(2.0)
elif opened >= 6:
scores.append(1.0)
else:
scores.append(0.0)
else:
# 选必/选修:达到标准=1,低于=0
if total_hours > 0:
scores.append(1.0)
else:
scores.append(0.0)
return scores if scores else [1.0]
def _score_course_structure(self, school: str) -> List[float]:
"""
课程结构建设:学科类课程结构、校本特色课程结构、综合实践活动与劳动
赋分:按照离差百分比和填报数值
"""
scores = []
hours_df = self.data_engine.get_weekly_hours(school)
if len(hours_df) > 0:
# 1. 学科类课程结构:各学科三类课程的离差
by_subject = hours_df.groupby(["学科", "字段名称"])["字段取值"].sum().unstack(fill_value=0)
if len(by_subject) > 0:
total_per_subject = by_subject.sum(axis=1)
overall_total = total_per_subject.sum()
if overall_total > 0:
proportions = total_per_subject / overall_total
mean_prop = proportions.mean()
deviation = np.abs(proportions - mean_prop).sum()
# 离差越小越好,标准化到0-3分
structure_score = max(0, 3 - deviation * 10)
scores.append(structure_score)
else:
scores.append(1.0)
else:
scores.append(1.0)
# 2. 校本特色课程:跨学科选修课门数、综合主题选修课门数
course_df = self.data_engine.get_school_course_data(school)
for field in ["跨学科选修课门数", "综合主题选修课门数", "综合实践选修课门数"]:
vals = course_df[course_df["字段名称"] == field]["字段取值"]
if len(vals) > 0:
try:
v = float(vals.iloc[0])
scores.append(min(v / 5, 3.0)) # 归一化
except (ValueError, TypeError):
scores.append(0.5)
# 3. 综合实践活动与劳动
for field in ["三年应完成的研究性学习数量", "三年社会考察个数", "三年志愿服务时长"]:
vals = course_df[course_df["字段名称"] == field]["字段取值"]
if len(vals) > 0:
try:
v = float(vals.iloc[0])
scores.append(min(v / 10, 3.0))
except (ValueError, TypeError):
scores.append(0.5)
return scores if scores else [1.0]
def _score_course_norms(self, school: str) -> List[float]:
"""
课程规范落实:建设规范文本 + 档案规范
赋分:有=2,无=0
"""
scores = []
norms = self.data_engine.get_course_norms(school)
norm_score = 0
archive_score = 0
norm_count = 0
archive_count = 0
for n in norms:
name = str(n.get("字段名称", ""))
val = str(n.get("字段取值", ""))
if "建设规范文本" in name:
norm_count += 1
if val and val not in ["0", "nan", "None", ""]:
norm_score += 2
elif "档案" in name:
archive_count += 1
if "已经建成并使用" in name and val == "1":
archive_score += 2
elif "已经建成" in name and val == "1":
archive_score += 1
if norm_count > 0:
scores.append(norm_score / norm_count * 2)
if archive_count > 0:
scores.append(archive_score / archive_count * 2)
return scores if scores else [1.0]
# ========== 教学变革力 ==========
def _score_teaching_reform(self, school: str) -> List[float]:
"""
教学方式变革:认识程度 + 落实程度 + 实施方式
赋分:量表题4/3/2/1分;多选每项1分加总
"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
sub_scores = []
# 认识程度类题目
for keyword in ["认识程度", "认识"]:
rows = sub_df[sub_df["字段名称"].str.contains(keyword, na=False)]
for _, row in rows.iterrows():
s = self._score_likert(row["字段取值"], reverse=False)
if s is not None:
sub_scores.append(s)
# 落实程度类题目
for keyword in ["落实程度", "落实"]:
rows = sub_df[sub_df["字段名称"].str.contains(keyword, na=False)]
for _, row in rows.iterrows():
s = self._score_likert(row["字段取值"], reverse=False)
if s is not None:
sub_scores.append(s)
# 实施方式(多选,体现形式类)
form_rows = sub_df[sub_df["字段名称"].str.contains("体现形式", na=False)]
if len(form_rows) > 0:
form_count = (form_rows["字段取值"].astype(str) == "1").sum()
sub_scores.append(min(form_count, 6))
if sub_scores:
scores.append(np.mean(sub_scores))
return scores if scores else [2.0]
def _score_homework_reform(self, school: str) -> List[float]:
"""
作业设计与管理变革:作业设计 + 作业管理
"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
sub_scores = []
# 作业设计:实践类/表现类/跨学科/团队合作作业是否布置
for hw_type in ["实践类作业_有布置", "表现类作业_有布置", "跨学科作业_有布置", "团队合作类作业_有布置"]:
rows = sub_df[sub_df["字段名称"] == hw_type]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
sub_scores.append(1.0)
else:
sub_scores.append(0.0)
# 作业属性标注(多选)
attr_rows = sub_df[sub_df["字段名称"].str.contains("作业属性标注", na=False)]
if len(attr_rows) > 0:
attr_count = (attr_rows["字段取值"].astype(str) == "1").sum()
sub_scores.append(min(attr_count, 5))
# 作业管理:时长控制、批改、评价
for field, score_map in [
("回家作业时长控制_学校控制", 3),
("回家作业时长控制_教研组负责", 2),
("作业批改范围_全部批改", 3),
("作业批改范围_部分练习", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
sub_scores.append(score_map)
if sub_scores:
scores.append(np.mean(sub_scores))
return scores if scores else [1.5]
# ========== 学生发展指导力 ==========
def _score_personalized_tutoring(self, school: str) -> List[float]:
"""个性化辅导"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 辅导时长
for field, val in [
("个别辅导时长_每周>2h", 4),
("个别辅导时长_每周1~2h", 3),
("个别辅导时长_每周<1h", 2),
("个别辅导时长_几乎无", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 辅导方式
for field, val in [
("个别辅导实施方式_分散辅导", 3),
("个别辅导实施方式_分组统一辅导", 2),
("个别辅导实施方式_班级统一辅导", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [2.0]
def _score_career_guidance(self, school: str) -> List[float]:
"""生涯发展指导"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 生涯指导实施方式
for field, val in [
("生涯指导实施方式_专设课程", 3),
("生涯指导实施方式_社会考察和志愿服务", 2),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
# 覆盖率
rows = course_df[course_df["字段名称"] == "完成生涯指导的学生占比_90%+"]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(4)
else:
scores.append(2)
# 教师构成
for field, val in [
("生涯指导教师构成_本校和外聘结合", 4),
("生涯指导教师构成_本校为主", 3),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 资源支持
for resource in ["生涯指导的校外资源支持程度", "生涯指导的校内资源支持程度"]:
rows = course_df[course_df["字段名称"] == resource]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
return scores if scores else [2.0]
# ========== 教师发展支持力 ==========
def _score_training_support(self, school: str) -> List[float]:
"""培训支持"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 提供外校培训的教师人数
rows = sub_df[sub_df["字段名称"] == "提供外校培训的教师人数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
# 区域培训指导人数
rows = sub_df[sub_df["字段名称"] == "区域培训指导人数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
return scores if scores else [1.0]
def _score_research_support(self, school: str) -> List[float]:
"""教研支持"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 教研活动次数
rows = sub_df[sub_df["字段名称"] == "学科教研组每学期活动次数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v / 3, 4)) # 归一化
except (ValueError, TypeError):
pass
# 教研活动时长
rows = sub_df[sub_df["字段名称"] == "学科教研组平均每次活动时长"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v / 30, 4)) # 30分钟为基准
except (ValueError, TypeError):
pass
# 教研计划
rows = sub_df[sub_df["字段名称"] == "学科教研工作计划_有"]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(2)
# 校级展示
for field in ["学科教研组校级展示_有", "学科教研组区域展示_有", "学科教研组成果发表_有"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(2)
return scores if scores else [1.0]
def _score_project_support(self, school: str) -> List[float]:
"""项目支持"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
course_df = self.data_engine.get_school_course_data(school)
# 学科层面的项目
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
for field in ["学科承担的市级教改项目个数", "学科承担的区级教改项目个数", "学科承担的校级教改项目个数"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
# 学校层面的项目
for field in ["学校负责的市级教改项目个数", "学校参与的市级教改项目个数",
"学校负责的区级教改项目个数", "学校参与的区级教改项目个数",
"校级教改项目个数"]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
return scores if scores else [0.5]
# ========== 教育质量评估力 ==========
def _score_scientific_evaluation(self, school: str) -> List[float]:
"""科学评价观"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 作业评价关注点(多选,关注素养发展的项目越多越好)
eval_items = sub_df[sub_df["字段名称"].str.contains("作业评价关注点|课堂表现评价关注点|学科实践活动评价关注点", na=False)]
if len(eval_items) > 0:
focus_count = (eval_items["字段取值"].astype(str) == "1").sum()
scores.append(min(focus_count / 3, 4))
return scores if scores else [2.0]
def _score_academic_evaluation(self, school: str) -> List[float]:
"""学业质量评估"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 评价工具建成
for field, val in [
("作业评价工具_已经建成并使用", 2),
("课堂表现评价工具_已经建成并使用", 2),
("作业评价工具_已经建成尚未使用", 1),
("作业评价工具_尚未建成和使用", 0),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 考试分析
for field, val in [
("学期考试分析_执行分析并存档", 3),
("学期考试分析_执行分析,不要求存档", 2),
("学期考试分析_教师自己决定", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 表现性评价应用
for field, val in [
("表现性评价应用程度_经常", 4),
("表现性评价应用程度_有时", 3),
("表现性评价应用程度_总是", 4),
("表现性评价应用程度_从不", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [2.0]
def _score_comprehensive_evaluation(self, school: str) -> List[float]:
"""综合素质评估"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 综评评价体系
for field, val in [
("综评评价体系_已经建成并使用", 3),
("综评评价体系_已经建成尚未使用", 2),
("综评评价体系_未建成", 1),
("综评评价体系_不准备建设", 0),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 综评信息化
for field, val in [
("综评信息化实现_自建平台支持", 2),
("综评信息化实现_借助第三方平台支持", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 综评结果使用(多选)
result_uses = course_df[course_df["字段名称"].str.contains("综评结果使用|综评应用", na=False)]
if len(result_uses) > 0:
use_count = (result_uses["字段取值"].astype(str) == "1").sum()
scores.append(min(use_count, 6))
return scores if scores else [1.0]
def _score_practice_evaluation(self, school: str) -> List[float]:
"""实践活动评估"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 研究性学习评价
for field, val in [
("研究性学习评价工具_已经建成并使用", 2),
("研究性学习评价工具_尚未建成和使用", 0),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 社会考察评价
for field, val in [
("社会考察评价工具_已经建成并使用", 2),
("社会考察评价工具_尚未建成和使用", 0),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 学科实践活动评价
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS[:5]: # 抽样几个学科
sub_df = subject_df[subject_df["学科"] == subject]
for field, val in [
("学科实践活动工具_已经建成并使用", 2),
("学科实践活动工具_已经建成尚未使用", 1),
("学科实践活动工具_尚未建成和使用", 0),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [1.0]
# ========== 教育条件保障力 ==========
def _score_regional_promotion(self, school: str) -> List[float]:
"""区域推进"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 工作会频率
for field, val in [
("区域工作会参与_一月4次以上", 7),
("区域工作会参与_一月1次", 5),
("区域工作会参与_二月1次", 3),
("区域工作会参与_三月1次", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 区域推进措施(多选)
measures = course_df[course_df["字段名称"].str.contains("区域推进措施", na=False)]
if len(measures) > 0:
measure_count = (measures["字段取值"].astype(str) == "1").sum()
scores.append(min(measure_count, 5))
return scores if scores else [2.0]
def _score_environment_support(self, school: str) -> List[float]:
"""环境支持:硬件+信息化"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 场馆供给
for field, val in [
("场馆供给_能满足需要", 3),
("场馆供给_基本满足需要", 2),
("场馆供给_难以满足需要", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 专用教室供给
for field, val in [
("专用教室供给_能满足需要", 3),
("专用教室供给_基本满足需要", 2),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 信息化平台功能(多选)
info_funcs = course_df[course_df["字段名称"].str.contains("信息化平台功能", na=False)]
if len(info_funcs) > 0:
func_count = (info_funcs["字段取值"].astype(str) == "1").sum()
scores.append(min(func_count / 3, 5))
return scores if scores else [2.0]
def _score_resource_support(self, school: str) -> List[float]:
"""资源支持:校内资源 + 校外资源 + 师资配置"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 校内资源支持程度
for field in ["必修课校内资源支持程度", "选择性必修课校内资源支持程度"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
# 校外资源支持程度
for field in ["必修课校外资源支持程度", "选择性必修课校外资源支持程度"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
# 师资:教研组总人数、高级教师比例等
rows = sub_df[sub_df["字段名称"] == "学科教研组总人数"]
if len(rows) > 0:
try:
total = float(rows.iloc[0]["字段取值"])
scores.append(min(total / 3, 4))
except (ValueError, TypeError):
pass
return scores if scores else [2.0]
# ========== 数字化赋能力 ==========
def _score_digital_teaching(self, school: str) -> List[float]:
"""教学方式创新"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 信息技术融合认识
for field in ["信息技术与教学融合的认识_所有人可做到"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(4)
break
else:
rows = sub_df[sub_df["字段名称"] == "信息技术与教学融合的认识_个别人可做到"]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(1)
# 信息化终端使用比例
for field, val in [
("信息化终端使用比例_80%+", 4),
("信息化终端使用比例_60~79%", 3),
("信息化终端使用比例_30~59%", 2),
("信息化终端使用比例_30%-", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [2.0]
def _score_digital_evaluation(self, school: str) -> List[float]:
"""评价精准化与个性化"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
course_df = self.data_engine.get_school_course_data(school)
# 学业评价信息化
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
for field, val in [
("学业评价信息化实现_自建平台支持", 3),
("学业评价信息化实现_借助第三方平台支持", 2),
("学业评价信息化实现_没有平台支持", 0),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 学校层面的信息化支持
for field, val in [
("学校信息系统对选课支持程度", None),
("学校信息系统对排课支持程度", None),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
return scores if scores else [1.5]
def _score_digital_curriculum(self, school: str) -> List[float]:
"""课程迭代优化"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 数据连通
for field, val in [
("数据连通_有数据能互通", 3),
("数据连通_有数据不互通", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 管理业务信息化
for field, val in [
("管理业务的信息化应用_绝大部分", 4),
("教学业务的信息化应用_绝大部分", 4),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
# 已完成网络课程数
rows = course_df[course_df["字段名称"] == "已完成的网络课程门数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v / 3, 4))
except (ValueError, TypeError):
pass
return scores if scores else [1.5]
# ========== 辅助赋分函数 ==========
@staticmethod
def _score_likert(value, scale: int = 4, reverse: bool = False) -> Optional[float]:
"""
量表题赋分
value格式可能是 "1""2(有一些支持)"
"""
try:
val_str = str(value).strip()
# 提取数字部分
match = re.match(r'^(\d+)', val_str)
if match:
v = int(match.group(1))
if reverse:
return float(scale + 1 - v)
return float(v)
except (ValueError, TypeError):
pass
return None
@staticmethod
def _score_resource_level(value) -> Optional[float]:
"""资源支持程度赋分:几乎没有=1, 有一些=2, 有足够=3"""
val_str = str(value).strip()
match = re.match(r'^(\d+)', val_str)
if match:
v = int(match.group(1))
return float(v)
if "足够" in val_str or "3" in val_str:
return 3.0
elif "一些" in val_str or "2" in val_str:
return 2.0
elif "没有" in val_str or "1" in val_str:
return 1.0
return None
+363
View File
@@ -0,0 +1,363 @@
"""
统计引擎:PCA合成 + 标准化 + 水平判定 + 聚类 + T检验 + 相关性
将赋分后的原始数据合成为维度得分,并进行统计分析
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from scipy import stats
import logging
from ..config import (
PCA_MEAN, PCA_STD, LEVEL_THRESHOLDS, LEVEL_DESCRIPTIONS,
DIMENSION_FRAMEWORK, SUBJECTS, SCHOOL_TYPE_MAP,
)
logger = logging.getLogger(__name__)
class StatsEngine:
"""统计引擎"""
def __init__(self):
self._dimension_scores: Optional[pd.DataFrame] = None
self._sub_dimension_scores: Optional[pd.DataFrame] = None
self._subject_dimension_scores: Optional[pd.DataFrame] = None
def standardize_scores(self, raw_scores: np.ndarray) -> np.ndarray:
"""标准化到均值50标准差10"""
if len(raw_scores) < 2:
return np.full_like(raw_scores, PCA_MEAN, dtype=float)
mean = np.nanmean(raw_scores)
std = np.nanstd(raw_scores, ddof=1)
if std == 0 or np.isnan(std):
return np.full_like(raw_scores, PCA_MEAN, dtype=float)
return (raw_scores - mean) / std * PCA_STD + PCA_MEAN
def pca_compose(self, data_matrix: pd.DataFrame) -> np.ndarray:
"""
PCA合成:将多个变量合成为一个主成分分数
data_matrix: 行=学校, 列=变量
返回:合成后的分数(已标准化到50/10)
"""
# 处理缺失值:均值填充
filled = data_matrix.fillna(data_matrix.mean())
if filled.shape[1] == 0:
return np.full(filled.shape[0], PCA_MEAN)
if filled.shape[1] == 1:
# 只有一个变量,直接标准化
return self.standardize_scores(filled.iloc[:, 0].values)
# 标准化
scaler = StandardScaler()
scaled = scaler.fit_transform(filled)
# PCA取第一主成分
n_components = min(1, filled.shape[1], filled.shape[0])
pca = PCA(n_components=n_components)
scores = pca.fit_transform(scaled)[:, 0]
# 如果载荷为负(方向反转),翻转
loadings = pca.components_[0]
if np.sum(loadings) < 0:
scores = -scores
# 标准化到50/10
return self.standardize_scores(scores)
def determine_level(self, score: float, dimension: str) -> int:
"""根据分数和维度确定水平(1-4"""
thresholds = LEVEL_THRESHOLDS.get(dimension, {})
if not thresholds:
# 默认阈值
if score > 55:
return 4
elif score > 50:
return 3
elif score > 45:
return 2
else:
return 1
if score > thresholds["level4"]:
return 4
elif score > thresholds["level3"]:
return 3
elif score > thresholds["level2"]:
return 2
else:
return 1
def get_level_description(self, dimension: str, level: int) -> str:
"""获取水平的质性描述"""
descriptions = LEVEL_DESCRIPTIONS.get(dimension, {})
return descriptions.get(level, f"水平{level}")
def compute_dimension_scores(self, school_raw_scores: Dict[str, Dict[str, List[float]]]) -> pd.DataFrame:
"""
计算所有学校在各三级维度上的得分
school_raw_scores: {
school_name: {
sub_dimension_name: [score1, score2, ...] # 该维度下各题目的赋分
}
}
返回 DataFrame: 行=学校, 列=三级维度, 值=标准化得分
"""
schools = list(school_raw_scores.keys())
all_sub_dims = []
for dim, info in DIMENSION_FRAMEWORK.items():
all_sub_dims.extend(info["sub_dimensions"])
# 构建原始矩阵
raw_matrix = {}
for sub_dim in all_sub_dims:
values = []
for school in schools:
scores = school_raw_scores.get(school, {}).get(sub_dim, [])
values.append(np.nanmean(scores) if scores else np.nan)
raw_matrix[sub_dim] = values
raw_df = pd.DataFrame(raw_matrix, index=schools)
# PCA合成并标准化各维度
result = pd.DataFrame(index=schools)
for sub_dim in all_sub_dims:
if sub_dim in raw_df.columns:
result[sub_dim] = self.standardize_scores(raw_df[sub_dim].values)
else:
result[sub_dim] = PCA_MEAN
self._sub_dimension_scores = result
return result
def compute_dimension_aggregates(self, sub_scores: pd.DataFrame) -> pd.DataFrame:
"""
从三级维度分数聚合到二级维度(均值)
sub_scores: 行=学校, 列=三级维度
返回: 行=学校, 列=二级维度
"""
result = pd.DataFrame(index=sub_scores.index)
for dim, info in DIMENSION_FRAMEWORK.items():
sub_dims = [s for s in info["sub_dimensions"] if s in sub_scores.columns]
if sub_dims:
result[dim] = sub_scores[sub_dims].mean(axis=1)
else:
result[dim] = PCA_MEAN
# 总体得分(七维度均值)
result["总体得分"] = result[list(DIMENSION_FRAMEWORK.keys())].mean(axis=1)
self._dimension_scores = result
return result
def compute_levels(self, sub_scores: pd.DataFrame) -> pd.DataFrame:
"""计算各学校各维度的水平等级"""
levels = pd.DataFrame(index=sub_scores.index)
for col in sub_scores.columns:
levels[col] = sub_scores[col].apply(
lambda x: self.determine_level(x, col)
)
return levels
def cluster_analysis(self, scores: pd.DataFrame, n_clusters: int = 2) -> Dict:
"""
聚类分析
scores: 行=学校, 列=维度
返回: 聚类标签和各类特征
"""
# 标准化
scaler = StandardScaler()
scaled = scaler.fit_transform(scores.fillna(PCA_MEAN))
# KMeans聚类
n_clusters = min(n_clusters, len(scores))
if n_clusters < 2:
return {"labels": [0] * len(scores), "centers": scores.values.tolist()}
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
labels = kmeans.fit_predict(scaled)
# 计算各类均值
cluster_means = {}
for c in range(n_clusters):
mask = labels == c
cluster_means[c] = scores[mask].mean().to_dict()
# 确定哪个是"较好类"(总均值更高的)
avg_per_cluster = {c: np.mean(list(v.values())) for c, v in cluster_means.items()}
sorted_clusters = sorted(avg_per_cluster.items(), key=lambda x: x[1], reverse=True)
cluster_names = {}
for rank, (c, _) in enumerate(sorted_clusters):
if rank == 0:
cluster_names[c] = "较好"
else:
cluster_names[c] = "待提升"
return {
"labels": labels.tolist(),
"school_clusters": {
school: cluster_names[labels[i]]
for i, school in enumerate(scores.index)
},
"cluster_means": cluster_means,
"cluster_names": cluster_names,
}
def t_test_vs_mean(self, school_scores: np.ndarray, ref_mean: float) -> Dict:
"""
单样本T检验:学校各学科得分 vs 参考均值
school_scores: 该学校在某维度各学科的得分
ref_mean: 参考均值(如区均值、全市均值)
"""
scores = school_scores[~np.isnan(school_scores)]
if len(scores) < 2:
return {"t": np.nan, "p": np.nan, "significant": False, "n": len(scores)}
t_stat, p_value = stats.ttest_1samp(scores, ref_mean)
return {
"t": round(float(t_stat), 3),
"p": round(float(p_value), 4),
"significant": float(p_value) < 0.05,
"n": len(scores),
}
def correlation_analysis(self, dim_scores: pd.DataFrame) -> pd.DataFrame:
"""维度间相关性分析"""
dim_cols = [c for c in dim_scores.columns if c in DIMENSION_FRAMEWORK]
return dim_scores[dim_cols].corr()
def compute_school_report_data(self, school: str,
sub_scores: pd.DataFrame,
dim_scores: pd.DataFrame) -> Dict:
"""
为某一所学校生成完整的报告数据包
返回包含所有统计分析结果的结构化数据
"""
schools = list(sub_scores.index)
if school not in schools:
raise ValueError(f"学校 '{school}' 不在数据中")
school_info = SCHOOL_TYPE_MAP.get(school, {})
school_type = school_info.get("type", "")
# 区均值
district_avg_sub = sub_scores.mean()
district_avg_dim = dim_scores.mean()
# 同类学校均值
same_type_schools = [s for s in schools if SCHOOL_TYPE_MAP.get(s, {}).get("type") == school_type]
if same_type_schools:
same_type_avg_sub = sub_scores.loc[same_type_schools].mean()
same_type_avg_dim = dim_scores.loc[same_type_schools].mean()
else:
same_type_avg_sub = district_avg_sub
same_type_avg_dim = district_avg_dim
# 水平判定
levels = self.compute_levels(sub_scores)
# 聚类(二级维度)
dim_cols = [c for c in dim_scores.columns if c in DIMENSION_FRAMEWORK]
overall_cluster = self.cluster_analysis(dim_scores[dim_cols])
# 各二级维度聚类
dim_clusters = {}
for dim, info in DIMENSION_FRAMEWORK.items():
sub_dims = [s for s in info["sub_dimensions"] if s in sub_scores.columns]
if sub_dims:
dim_clusters[dim] = self.cluster_analysis(sub_scores[sub_dims])
# 相关性
correlation = self.correlation_analysis(dim_scores)
# 构建报告数据
report = {
"school": school,
"school_info": school_info,
# 总体得分
"overall": {
"score": round(float(dim_scores.loc[school, "总体得分"]), 2),
"district_avg": round(float(district_avg_dim["总体得分"]), 2),
"same_type_avg": round(float(same_type_avg_dim.get("总体得分", PCA_MEAN)), 2),
"rank_in_district": int((dim_scores["总体得分"] >= dim_scores.loc[school, "总体得分"]).sum()),
"total_schools": len(schools),
"cluster": overall_cluster["school_clusters"].get(school, ""),
},
# 二级维度
"dimensions": {},
# 三级维度
"sub_dimensions": {},
# 相关性矩阵
"correlation": correlation.to_dict(),
# 所有学校得分(用于对比)
"all_schools_dim_scores": dim_scores.to_dict(),
"all_schools_sub_scores": sub_scores.to_dict(),
}
# 填充二级维度数据
for dim in DIMENSION_FRAMEWORK:
score = float(dim_scores.loc[school, dim])
d_avg = float(district_avg_dim[dim])
st_avg = float(same_type_avg_dim.get(dim, PCA_MEAN))
# T检验:该学校在该维度下各三级维度得分 vs 区均值
sub_dims = DIMENSION_FRAMEWORK[dim]["sub_dimensions"]
sub_vals = np.array([float(sub_scores.loc[school, s]) for s in sub_dims if s in sub_scores.columns])
t_test = self.t_test_vs_mean(sub_vals, d_avg)
report["dimensions"][dim] = {
"score": round(score, 2),
"district_avg": round(d_avg, 2),
"same_type_avg": round(st_avg, 2),
"diff_district": round(score - d_avg, 2),
"rank_in_district": int((dim_scores[dim] >= score).sum()),
"t_test_vs_district": t_test,
"cluster": dim_clusters.get(dim, {}).get("school_clusters", {}).get(school, ""),
}
# 填充三级维度数据
for dim, info in DIMENSION_FRAMEWORK.items():
for sub_dim in info["sub_dimensions"]:
if sub_dim not in sub_scores.columns:
continue
score = float(sub_scores.loc[school, sub_dim])
d_avg = float(district_avg_sub[sub_dim])
level = int(levels.loc[school, sub_dim])
# 各学校在该维度的排名
rank = int((sub_scores[sub_dim] >= score).sum())
# 水平分布统计
dim_levels = levels[sub_dim]
level_dist = {
f"水平{i}": int((dim_levels == i).sum())
for i in range(1, 5)
}
report["sub_dimensions"][sub_dim] = {
"parent_dimension": dim,
"score": round(score, 2),
"district_avg": round(d_avg, 2),
"diff_district": round(score - d_avg, 2),
"rank_in_district": rank,
"level": level,
"level_description": self.get_level_description(sub_dim, level),
"level_distribution": level_dist,
}
return report
+253
View File
@@ -0,0 +1,253 @@
"""
FastAPI 主应用入口
报告管理系统:API + 前端静态文件,单端口服务
增加 JWT 认证保护
"""
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request, Depends, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from .config import OUTPUT_DIR, STATIC_DIR, PROJECT_ROOT
from .api.era2_routes import router as era2_router
from .api.era2_state import era2_state
from .auth import (
LoginRequest, TokenResponse,
authenticate_user, create_access_token, get_current_user, verify_token,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)
# 前端构建产物目录
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用启动/关闭生命周期"""
logger.info("🚀 正在初始化全市数据引擎...")
era2_state.initialize()
logger.info(f"✅ 数据引擎就绪: {len(era2_state.schools)}所学校, {len(era2_state.districts)}个区")
if FRONTEND_DIST.exists():
logger.info(f"📦 前端静态文件: {FRONTEND_DIST}")
else:
logger.warning(f"⚠️ 前端静态文件不存在: {FRONTEND_DIST},请先 cd frontend && npm run build")
logger.info("🔐 认证已启用,所有 API 需要登录访问")
yield
logger.info("👋 应用关闭")
import os
# 生产环境关闭 API 文档(设 DOCS_ENABLED=1 可临时打开)
_docs_enabled = os.environ.get("DOCS_ENABLED", "0") == "1"
app = FastAPI(
title="课程实施监测报告管理系统",
description="上海市高中课程实施监测数据分析与报告生成 API",
version="2.0.0",
lifespan=lifespan,
docs_url="/docs" if _docs_enabled else None,
redoc_url="/redoc" if _docs_enabled else None,
openapi_url="/openapi.json" if _docs_enabled else None,
)
# CORS — 收紧配置(部署时按需修改 allow_origins
ALLOWED_ORIGINS = [
"http://localhost:5173", # 前端开发服务器
"http://localhost:7777", # 后端自身
"http://127.0.0.1:5173",
"http://127.0.0.1:7777",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
# ==================== 无需认证的路由 ====================
# 登录接口
@app.post("/api/auth/login", response_model=TokenResponse)
async def login(req: LoginRequest, response: Response):
"""用户登录,返回 JWT token"""
username = authenticate_user(req.username, req.password)
if not username:
return JSONResponse(
status_code=401,
content={"detail": "用户名或密码错误"},
)
token, expires_in = create_access_token(username)
# 同时设置 Cookie(方便浏览器直接访问静态资源)
response.set_cookie(
key="access_token",
value=token,
max_age=expires_in,
httponly=True,
samesite="lax",
# secure=True, # 生产环境使用 HTTPS 时取消注释
)
logger.info(f"🔑 用户 '{username}' 登录成功")
return TokenResponse(access_token=token, expires_in=expires_in)
# 验证 token 是否有效
@app.get("/api/auth/verify")
async def verify_auth(request: Request):
"""验证当前 token 是否有效(前端刷新页面时调用)"""
# 从 header 或 cookie 中提取 token
token = None
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
if not token:
token = request.cookies.get("access_token")
if not token:
return JSONResponse(status_code=401, content={"valid": False})
username = verify_token(token)
if not username:
return JSONResponse(status_code=401, content={"valid": False})
return {"valid": True, "username": username}
# 登出
@app.post("/api/auth/logout")
async def logout(response: Response):
"""清除认证 Cookie"""
response.delete_cookie("access_token")
return {"message": "已退出登录"}
# ==================== 认证中间件 ====================
# 不需要认证的路径前缀(API 文档不对外暴露)
PUBLIC_PATHS = {
"/api/auth/login",
"/api/auth/logout",
"/api/auth/verify",
}
# 前端静态资源路径前缀(不需要 API 级别认证,前端自己处理路由守卫)
STATIC_PREFIXES = ("/assets/", "/favicon", "/vite.svg")
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
"""
全局认证中间件:
- 公开路径(登录、静态资源)直接放行
- API 路径需要有效的 JWT token
- 前端 SPA 页面路径放行(由前端路由守卫处理)
"""
path = request.url.path
# 1. 公开 API 路径 — 放行
if path in PUBLIC_PATHS:
return await call_next(request)
# 2. 前端静态资源 — 放行
if any(path.startswith(p) for p in STATIC_PREFIXES):
return await call_next(request)
# 3. API 路径 — 需要认证
if path.startswith("/api/") or path.startswith("/output/"):
token = None
# 来源1: Authorization header
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
# 来源2: Cookie
if not token:
token = request.cookies.get("access_token")
# 来源3: URL query param(供 <a href> / <iframe src> 等无法设 header 的场景)
if not token:
token = request.query_params.get("token")
if not token or not verify_token(token):
return JSONResponse(
status_code=401,
content={"detail": "未授权访问,请先登录"},
)
# 4. 其他路径(前端 SPA 页面)— 放行,由前端路由守卫处理
response = await call_next(request)
# 安全响应头
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# 防止浏览器缓存敏感 API 响应
if path.startswith("/api/"):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
response.headers["Pragma"] = "no-cache"
return response
# ==================== 业务路由(全部需要认证) ====================
# 注册 API 路由
app.include_router(era2_router, prefix="/api")
# 挂载 output 静态文件(报告 HTML/JSON)— 已由中间件保护
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/output", StaticFiles(directory=str(OUTPUT_DIR)), name="output")
# 挂载 backend/static(如有)
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# 挂载前端静态资源(JS/CSS 等)
if FRONTEND_DIST.exists():
assets_dir = FRONTEND_DIST / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="frontend-assets")
# SPA Fallback:所有未匹配的路由返回前端 index.html
@app.get("/{full_path:path}")
async def serve_frontend(request: Request, full_path: str):
"""
SPA 路由兜底:
- 如果请求的是 dist 目录下的真实文件(如 favicon.ico),直接返回
- 否则返回 index.html,让前端路由处理
"""
if FRONTEND_DIST.exists():
# 尝试匹配真实文件(防路径穿越: resolve 后必须在 FRONTEND_DIST 内)
file_path = (FRONTEND_DIST / full_path).resolve()
if file_path.is_file() and str(file_path).startswith(str(FRONTEND_DIST.resolve())):
return FileResponse(str(file_path))
# SPA fallback → index.html
index_path = FRONTEND_DIST / "index.html"
if index_path.exists():
return FileResponse(str(index_path))
# 前端未构建时返回 API 信息
return {
"name": "课程实施监测报告管理系统",
"version": "2.0.0",
"status": "running",
"message": "前端未构建,请访问 /api/auth/login 登录后使用 API",
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+233
View File
@@ -0,0 +1,233 @@
{
"基础信息表行数": 5557,
"课程实施情况表行数": 145484,
"学科课程实施情况表行数": 1348513,
"学校列表": [
"上海七宝德怀特高级中学",
"上海中医药大学附属浦江高级中学",
"上海交通大学附属中学",
"上海交通大学附属中学闵行分校",
"上海交通大学附属闵行实验学校",
"上海体育大学附属金山亭林中学",
"上海创艺高级中学",
"上海协和双语高级中学",
"上海外国语大学附属外国语学校",
"上海外国语大学附属外国语学校东校",
"上海外国语大学附属外国语学校松江云间中学",
"上海外国语大学附属大境中学",
"上海大学市北附属中学",
"上海存志高级中学",
"上海宋庆龄学校",
"上海宝山区世外学校",
"上海宝山区民办维尚高级中学",
"上海市七宝中学",
"上海市七宝中学浦江分校",
"上海市七宝中学附属鑫都实验中学",
"上海市上海中学",
"上海市中原中学",
"上海市中国中学",
"上海市久隆模范中学",
"上海市五爱高级中学",
"上海市仙霞高级中学",
"上海市位育中学",
"上海市体育学院附属中学",
"上海市光明中学",
"上海市华东模范中学",
"上海市南洋中学",
"上海市南洋模范中学",
"上海市卢湾高级中学",
"上海市古美高级中学",
"上海市同洲模范学校",
"上海市同济中学",
"上海市同济黄浦设计创意中学",
"上海市向东中学",
"上海市向明中学",
"上海市吴淞中学",
"上海市回民中学",
"上海市复兴高级中学",
"上海市复旦中学",
"上海市复旦实验中学",
"上海市大同中学",
"上海市奉贤中学",
"上海市奉贤区奉城高级中学",
"上海市奉贤区景秀高级中学",
"上海市宝山中学",
"上海市宝山区海滨中学",
"上海市宝山华曜高级中学",
"上海市实验学校东滩高级中学",
"上海市崇明中学",
"上海市崇明区城桥中学",
"上海市崇明区堡镇中学",
"上海市崇明区横沙中学",
"上海市崇明区民本中学",
"上海市市北中学",
"上海市市南中学",
"上海市市西中学",
"上海市延安中学",
"上海市建青实验学校",
"上海市彭浦中学",
"上海市徐汇区董恒甫高级中学",
"上海市控江中学",
"上海市敬业中学",
"上海市文来中学",
"上海市新中高级中学",
"上海市朱家角中学",
"上海市杨浦高级中学",
"上海市松江一中",
"上海市松江九峰实验学校",
"上海市松江二中",
"上海市松江区民办茸一中学",
"上海市松江区科德高级中学",
"上海市松江区立达中学",
"上海市松江区第四中学",
"上海市民办上实剑桥外国语中学",
"上海市民办交大南洋中学",
"上海市民办扬波中学",
"上海市民办文绮中学",
"上海市民办新和中学",
"上海市民办新虹桥中学",
"上海市民办永昌中学",
"上海市民办燎原双语高级中学",
"上海市民办西南高级中学",
"上海市民办风范中学",
"上海市民星中学",
"上海市民立中学",
"上海市淞浦中学",
"上海市澄衷高级中学",
"上海市第一中学",
"上海市第二中学",
"上海市第二体育运动学校(上海市体育中学)",
"上海市第五十二中学",
"上海市第五十四中学",
"上海市第八中学",
"上海市第十中学",
"上海市第四中学",
"上海市紫竹园中学",
"上海市继光高级中学",
"上海市育才中学",
"上海市莘庄中学",
"上海市虹口高级中学",
"上海市行知中学",
"上海市行知实验中学",
"上海市西南位育中学",
"上海市西南模范中学",
"上海市西外外国语学校",
"上海市西郊学校",
"上海市通河中学",
"上海市金山中学",
"上海市金汇高级中学",
"上海市金陵中学",
"上海市闵行中学",
"上海市闵行中学东校",
"上海市闵行区实验高级中学",
"上海市闵行区教育学院附属中学",
"上海市闵行区教育学院附属友爱实验中学",
"上海市闵行第三中学",
"上海市闸北第八中学",
"上海市零陵中学",
"上海市青浦区东湖中学",
"上海市青浦区第一中学",
"上海市青浦区第二中学",
"上海市顾村中学",
"上海市风华中学",
"上海市高境第一中学",
"上海市鲁迅中学",
"上海师范大学第二附属中学",
"上海师范大学第四附属中学",
"上海师范大学附属中学宝山分校",
"上海师范大学附属中学闵行分校",
"上海师范大学附属外国语中学",
"上海师范大学附属宝山罗店中学",
"上海戏剧学院附属高级中学",
"上海民办包玉刚实验高中",
"上海民办民一中学",
"上海民办行中中学",
"上海理工大学附属中学",
"上海理工大学附属储能中学",
"上海理工大学附属杨浦少云中学",
"上海田家炳中学",
"上海美达菲双语高级中学",
"上海财经大学附属中学",
"上海赫贤学校",
"上海金山区世外学校",
"上海金山区枫叶学校",
"上海闵行区万科双语学校",
"上海闵行区协和双语教科学校",
"上海闵行区民办德闳学校",
"上海闵行区诺达双语学校",
"上海青浦区世外学校",
"上海青浦区协和双语学校",
"上海音乐学院虹口区北虹高级中学",
"上海音乐学院附属黄浦比乐中学",
"上海领科双语学校",
"北京外国语大学附属上海闵行田园高级中学",
"华东师范大学第一附属中学",
"华东师范大学第三附属中学",
"华东师范大学第二附属中学临港奉贤分校",
"华东师范大学第二附属中学宝山校区",
"华东师范大学第二附属中学松江分校",
"华东师范大学第二附属中学闵行紫竹分校",
"华东师范大学附属天山学校",
"华东师范大学附属枫泾中学",
"华东政法大学附属中学",
"华东政法大学附属松江高级中学",
"华东理工大学附属中学",
"华东理工大学附属奉贤曙光中学",
"华东理工大学附属闵行科技高级中学",
"同济大学第一附属中学",
"同济大学附属七一中学",
"复旦大学附属中学",
"复旦大学附属中学徐汇分校",
"复旦大学附属中学青浦分校"
],
"区域列表": [
"奉贤区",
"宝山区",
"崇明区",
"徐汇区",
"杨浦区",
"松江区",
"虹口区",
"金山区",
"长宁区",
"闵行区",
"青浦区",
"静安区",
"黄浦区"
],
"学科列表": [
"体育与健康",
"信息技术",
"化学",
"历史",
"地理",
"思想政治",
"数学",
"物理",
"生物学",
"美术",
"艺术",
"英语",
"语文",
"通用技术",
"音乐"
],
"课程实施题数": "103",
"各学科题数": {
"体育与健康": "87",
"信息技术": "101",
"化学": "130",
"历史": "95",
"地理": "131",
"思想政治": "95",
"数学": "131",
"物理": "130",
"生物学": "130",
"美术": "87",
"艺术": "87",
"英语": "95",
"语文": "95",
"通用技术": "93",
"音乐": "87"
}
}
+5
View File
@@ -0,0 +1,5 @@
XSym
0156
286333cd198354204f9962d2b25d1bea
/Users/lofyer/Documents/my_work/Digiman.live/99-销售商机/20260301-邱老师-校长画像/高中课程实施监测指标、题目、赋分整理0127.xlsx
+5
View File
@@ -0,0 +1,5 @@
XSym
0123
0bf9516440f2d93f5c343acdcf1604b8
/Users/lofyer/Documents/my_work/Digiman.live/99-销售商机/20260301-邱老师-校长画像/长宁区_基础信息表.xlsx
@@ -0,0 +1,5 @@
XSym
0135
df0620c5ffdc1efed5f8bd6b5fcb407d
/Users/lofyer/Documents/my_work/Digiman.live/99-销售商机/20260301-邱老师-校长画像/长宁区_学科课程实施情况表.xlsx
@@ -0,0 +1,5 @@
XSym
0129
17433ed6be897b86f3c0615156eb7973
/Users/lofyer/Documents/my_work/Digiman.live/99-销售商机/20260301-邱老师-校长画像/长宁区_课程实施情况表.xlsx
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+73
View File
@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+23
View File
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+6472
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@types/three": "^0.183.1",
"antd": "^6.3.1",
"axios": "^1.13.6",
"gsap": "^3.14.2",
"i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^17.0.6",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.1",
"three": "^0.183.2"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
}
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+209
View File
@@ -0,0 +1,209 @@
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { ConfigProvider, Spin } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import { useState, useEffect, createContext, useContext, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import AppLayout from './components/Layout';
import DistrictList from './pages/DistrictList';
import DistrictSchools from './pages/DistrictSchools';
import Era2ReportPreview from './pages/Era2ReportPreview';
import History from './pages/History';
import DataTraceView from './components/DataTrace/DataTraceView';
import Login from './pages/Login';
import { verifyToken, getToken, clearAuth, logout as doLogout } from './services/auth';
// ==================== Auth Context ====================
interface AuthContextType {
isAuthenticated: boolean;
username: string;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>({
isAuthenticated: false,
username: '',
logout: async () => {},
});
export const useAuth = () => useContext(AuthContext);
// ==================== 路由守卫 ====================
function RequireAuth({ children }: { children: React.ReactNode }) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// ==================== App ====================
function AppContent() {
const [authState, setAuthState] = useState<{
checking: boolean;
isAuthenticated: boolean;
username: string;
}>({ checking: true, isAuthenticated: false, username: '' });
// 检查认证状态
useEffect(() => {
const checkAuth = async () => {
const token = getToken();
if (!token) {
setAuthState({ checking: false, isAuthenticated: false, username: '' });
return;
}
try {
const valid = await verifyToken();
if (valid) {
// 从 verify 接口获取用户名
const res = await fetch('/api/auth/verify', {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
setAuthState({
checking: false,
isAuthenticated: true,
username: data.username || 'admin',
});
} else {
clearAuth();
setAuthState({ checking: false, isAuthenticated: false, username: '' });
}
} catch {
clearAuth();
setAuthState({ checking: false, isAuthenticated: false, username: '' });
}
};
checkAuth();
}, []);
// 监听 localStorage 变化(登录成功后更新状态)
useEffect(() => {
const handleStorage = () => {
const token = getToken();
if (token && !authState.isAuthenticated) {
setAuthState(prev => ({ ...prev, checking: true }));
verifyToken().then(valid => {
setAuthState({
checking: false,
isAuthenticated: valid,
username: valid ? 'admin' : '',
});
});
}
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, [authState.isAuthenticated]);
// 登录成功后由 Login 页面 navigate 到 /,此时需要重新检测
const location = useLocation();
useEffect(() => {
if (location.pathname !== '/login') {
const token = getToken();
if (token && !authState.isAuthenticated && !authState.checking) {
setAuthState(prev => ({ ...prev, checking: true }));
verifyToken().then(valid => {
setAuthState({
checking: false,
isAuthenticated: valid,
username: valid ? 'admin' : '',
});
});
}
}
}, [location.pathname]);
const handleLogout = useCallback(async () => {
await doLogout();
setAuthState({ checking: false, isAuthenticated: false, username: '' });
}, []);
if (authState.checking) {
return <AuthCheckingSpinner />;
}
return (
<AuthContext.Provider value={{
isAuthenticated: authState.isAuthenticated,
username: authState.username,
logout: handleLogout,
}}>
<Routes>
{/* 登录页 — 公开 */}
<Route path="/login" element={
authState.isAuthenticated
? <Navigate to="/" replace />
: <Login />
} />
{/* 受保护的路由 */}
<Route element={
<RequireAuth>
<AppLayout />
</RequireAuth>
}>
<Route path="/" element={<DistrictList />} />
<Route path="/district/:district" element={<DistrictSchools />} />
<Route path="/district/:district/report/:school" element={<Era2ReportPreview />} />
<Route path="/history" element={<History />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
{/* 数据溯源 — 也需要认证 */}
<Route path="/district/:district/trace/:school" element={
<RequireAuth>
<DataTraceView />
</RequireAuth>
} />
</Routes>
</AuthContext.Provider>
);
}
function AuthCheckingSpinner() {
const { t } = useTranslation();
return (
<div style={{
height: '100vh', display: 'flex',
alignItems: 'center', justifyContent: 'center',
background: '#f0f2f5',
}}>
<Spin size="large" tip={t('common.verifying_login')} />
</div>
);
}
function AppWithLocale() {
const { i18n } = useTranslation();
const antdLocale = i18n.language?.startsWith('en') ? enUS : zhCN;
return (
<ConfigProvider
locale={antdLocale}
theme={{
token: {
colorPrimary: '#1677ff',
borderRadius: 8,
},
}}
>
<BrowserRouter>
<AppContent />
</BrowserRouter>
</ConfigProvider>
);
}
function App() {
return <AppWithLocale />;
}
export default App;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+459
View File
@@ -0,0 +1,459 @@
/**
* ChatFab — 右下角浮动AI助理组件
* 自动绑定当前报告的区+学校上下文,无需手动选择
* - SSE 流式输出(带 buffer 处理跨 chunk 断行)
* - Markdown 渲染(react-markdown
*/
import { useState, useRef, useEffect, useCallback } from 'react';
import { Input, Button, Spin, Badge } from 'antd';
import {
RobotOutlined, SendOutlined, UserOutlined,
CloseOutlined, DeleteOutlined,
} from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import { sendChatMessage } from '../services/era2Api';
const { TextArea } = Input;
interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}
interface ChatFabProps {
school: string;
district: string;
/** 报告语言;用于切换 system prompt 和 UI 文案 */
lang?: 'zh' | 'en';
}
const QUICK_PROMPTS_ZH = [
'总体表现如何?',
'最大的短板是什么?',
'最紧迫应该改进什么?',
'与同类学校对比如何?',
];
const QUICK_PROMPTS_EN = [
'How does the school perform overall?',
'What are the most significant gaps?',
'Which improvements are most urgent?',
'How does it compare with peer-type schools?',
];
/* ====== Markdown 气泡样式 ====== */
const markdownBubbleStyle: React.CSSProperties = {
padding: '12px 16px',
borderRadius: '4px 12px 12px 12px',
background: '#fff',
border: '1px solid #e5e7eb',
fontSize: 15,
lineHeight: 1.75,
wordBreak: 'break-word',
overflow: 'hidden',
};
/** 流式请求超时 (ms):2 分钟 — 留足 LLM 生成时间 */
const CHAT_TIMEOUT_MS = 120_000;
export default function ChatFab({ school, district, lang = 'zh' }: ChatFabProps) {
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
const messagesRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<any>(null);
/** 当前请求的 AbortController,用于取消/超时 */
const abortRef = useRef<AbortController | null>(null);
const QUICK_PROMPTS = lang === 'en' ? QUICK_PROMPTS_EN : QUICK_PROMPTS_ZH;
// 学校 / 语言变化时清空对话 & 取消进行中的请求
useEffect(() => {
abortRef.current?.abort();
setMessages([]);
setInput('');
}, [school, district, lang]);
// 组件卸载时取消请求
useEffect(() => {
return () => { abortRef.current?.abort(); };
}, []);
// 自动滚动到底部
useEffect(() => {
if (messagesRef.current) {
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
}
}, [messages]);
// 打开时聚焦输入框
useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 300);
}
}, [open]);
const handleSend = useCallback(async (text?: string) => {
const msg = text || input.trim();
if (!msg || loading) return;
// 取消上一次未完成的请求
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
// 超时自动中断
const timeoutId = setTimeout(() => controller.abort(), CHAT_TIMEOUT_MS);
const userMsg: ChatMessage = { role: 'user', content: msg };
setMessages(prev => [...prev, userMsg]);
setInput('');
setLoading(true);
const history = messages.map(m => ({ role: m.role, content: m.content }));
try {
const response = await sendChatMessage(
msg, school, district, history, controller.signal, lang,
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const reader = response.body?.getReader();
if (!reader) throw new Error('No readable stream');
const decoder = new TextDecoder();
let assistantContent = '';
let sseBuffer = ''; // 缓冲跨 chunk 的不完整 SSE 行
// 先插入一条空的 assistant 消息占位
setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
sseBuffer += decoder.decode(value, { stream: true });
// 按换行分割,最后一段可能不完整,留在 buffer 中
const parts = sseBuffer.split('\n');
sseBuffer = parts.pop() || '';
for (const line of parts) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
const data = trimmed.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
if (parsed.error) {
assistantContent += `\n⚠️ ${parsed.error}`;
} else if (parsed.content) {
assistantContent += parsed.content;
}
// 实时更新最后一条 assistant 消息
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = { role: 'assistant', content: assistantContent };
return updated;
});
} catch {
/* 跳过非JSON行 */
}
}
}
// 处理 buffer 中剩余的最后一行
if (sseBuffer.trim().startsWith('data: ')) {
const data = sseBuffer.trim().slice(6);
if (data !== '[DONE]') {
try {
const parsed = JSON.parse(data);
if (parsed.content) {
assistantContent += parsed.content;
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = { role: 'assistant', content: assistantContent };
return updated;
});
}
} catch { /* ignore */ }
}
}
// 如果最终没有任何内容
if (!assistantContent) {
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = { role: 'assistant', content: '⚠️ 未收到有效回复,请重试。' };
return updated;
});
}
} catch (err: unknown) {
// 用户主动取消或组件卸载 — 静默处理
if (err instanceof DOMException && err.name === 'AbortError') {
setMessages(prev => {
const last = prev[prev.length - 1];
if (last?.role === 'assistant' && !last.content) {
// 清除空占位
return prev.slice(0, -1);
}
return prev;
});
return;
}
const errorMessage = err instanceof Error ? err.message : String(err);
const failPrefix = lang === 'en' ? '⚠️ Request failed: ' : '⚠️ 请求失败: ';
setMessages(prev => {
const last = prev[prev.length - 1];
// 如果已有占位 assistant 消息,更新它;否则追加
if (last?.role === 'assistant' && !last.content) {
const updated = [...prev];
updated[updated.length - 1] = {
role: 'assistant',
content: `${failPrefix}${errorMessage}`,
};
return updated;
}
return [...prev, { role: 'assistant', content: `${failPrefix}${errorMessage}` }];
});
} finally {
clearTimeout(timeoutId);
setLoading(false);
}
}, [input, loading, messages, school, district, lang]);
/* ====== 渲染单条消息内容 ====== */
const renderContent = (msg: ChatMessage, idx: number) => {
if (!msg.content && loading && idx === messages.length - 1) {
return <Spin size="small" />;
}
// 用户消息:纯文本
if (msg.role === 'user') {
return msg.content;
}
// AI消息:Markdown 渲染
return <ReactMarkdown>{msg.content}</ReactMarkdown>;
};
return (
<>
{/* FAB 按钮 */}
<div
onClick={() => setOpen(v => !v)}
style={{
position: 'fixed', bottom: 32, right: 32, zIndex: 1000,
width: 56, height: 56, borderRadius: '50%',
background: 'linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)',
color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', boxShadow: '0 4px 20px rgba(37,99,235,0.4)',
transition: 'transform 0.2s, box-shadow 0.2s',
fontSize: 24,
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.transform = 'scale(1.08)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 6px 28px rgba(37,99,235,0.55)';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.transform = 'scale(1)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 20px rgba(37,99,235,0.4)';
}}
title={lang === 'en' ? 'AI Report Assistant' : 'AI 报告助手'}
>
<Badge count={messages.length > 0 && !open ? messages.length : 0} size="small" offset={[-2, 2]}>
<RobotOutlined style={{ fontSize: 26, color: '#fff' }} />
</Badge>
</div>
{/* 对话面板 */}
{open && (
<div style={{
position: 'fixed', bottom: 100, right: 32, zIndex: 1001,
width: 'min(560px, calc(100vw - 64px))',
height: 'min(780px, calc(100vh - 140px))',
background: '#fff', borderRadius: 16,
boxShadow: '0 12px 48px rgba(0,0,0,0.18)',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
animation: 'chatSlideUp 0.3s ease',
}}>
{/* 头部 */}
<div style={{
background: 'linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)',
color: '#fff', padding: '14px 20px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
flexShrink: 0,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 32, height: 32, borderRadius: '50%',
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18,
}}>🤖</div>
<div>
<div style={{ fontWeight: 600, fontSize: 15 }}>{lang === 'en' ? 'AI Report Assistant' : 'AI 报告助手'}</div>
<div style={{ fontSize: 12, opacity: 0.8 }}>{district} · {school}</div>
</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<Button type="text" size="small" icon={<DeleteOutlined />}
style={{ color: 'rgba(255,255,255,0.8)' }}
onClick={() => setMessages([])} title={lang === 'en' ? 'Clear conversation' : '清空对话'} />
<Button type="text" size="small" icon={<CloseOutlined />}
style={{ color: 'rgba(255,255,255,0.8)' }}
onClick={() => setOpen(false)} title={lang === 'en' ? 'Close' : '关闭'} />
</div>
</div>
{/* 消息区 */}
<div ref={messagesRef} style={{
flex: 1, overflowY: 'auto', padding: 16,
display: 'flex', flexDirection: 'column', gap: 12,
minHeight: 200, background: '#f8fafc',
}}>
{/* 欢迎消息 */}
{messages.length === 0 && (
<div style={{ display: 'flex', gap: 8 }}>
<div style={{
width: 28, height: 28, borderRadius: '50%', flexShrink: 0,
background: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
color: '#1e40af', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14,
}}>🤖</div>
<div style={markdownBubbleStyle}>
{lang === 'en' ? (
<>
<p style={{ margin: '2px 0' }}>Hello! I have full access to the curriculum-implementation monitoring data for <strong>{school}</strong>.</p>
<p style={{ margin: '2px 0' }}>Ask any question directly, or pick a quick prompt below to begin.</p>
</>
) : (
<>
<p style={{ margin: '2px 0' }}> <strong>{school}</strong> </p>
<p style={{ margin: '2px 0' }}></p>
</>
)}
</div>
</div>
)}
{messages.map((msg, idx) => (
<div key={idx} style={{
display: 'flex', gap: 8, maxWidth: '88%',
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
flexDirection: msg.role === 'user' ? 'row-reverse' : 'row',
}}>
<div style={{
width: 28, height: 28, borderRadius: '50%', flexShrink: 0,
background: msg.role === 'user'
? 'linear-gradient(135deg, #d1fae5, #a7f3d0)'
: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
color: msg.role === 'user' ? '#065f46' : '#1e40af',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14,
}}>
{msg.role === 'user' ? <UserOutlined /> : <RobotOutlined />}
</div>
<div
className={msg.role === 'assistant' ? 'chat-md-bubble' : undefined}
style={{
padding: '12px 16px',
borderRadius: msg.role === 'user' ? '12px 4px 12px 12px' : '4px 12px 12px 12px',
background: msg.role === 'user'
? 'linear-gradient(135deg, #2563eb, #3b82f6)'
: '#fff',
color: msg.role === 'user' ? '#fff' : '#1f2937',
border: msg.role === 'assistant' ? '1px solid #e5e7eb' : 'none',
fontSize: 15, lineHeight: 1.75, wordBreak: 'break-word',
overflow: 'hidden',
}}
>
{renderContent(msg, idx)}
</div>
</div>
))}
<div ref={chatEndRef} />
</div>
{/* 快捷问题 */}
{messages.length === 0 && (
<div style={{
padding: '8px 16px', display: 'flex', flexWrap: 'wrap', gap: 6,
background: '#f8fafc', borderTop: '1px solid #f1f5f9', flexShrink: 0,
}}>
{QUICK_PROMPTS.map(p => (
<Button key={p} size="small" onClick={() => handleSend(p)}
style={{ borderRadius: 16, fontSize: 13, height: 28 }}>{p}</Button>
))}
</div>
)}
{/* 输入区 */}
<div style={{
padding: '10px 14px', borderTop: '1px solid #e5e7eb',
display: 'flex', gap: 8, alignItems: 'flex-end', background: '#fff',
flexShrink: 0,
}}>
<TextArea
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
placeholder={lang === 'en' ? 'Type your question...' : '输入问题...'}
autoSize={{ minRows: 1, maxRows: 3 }}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
disabled={loading}
style={{ flex: 1, borderRadius: 10, fontSize: 14 }}
/>
<Button type="primary" icon={<SendOutlined />}
onClick={() => handleSend()} loading={loading}
style={{ borderRadius: 10, height: 36, width: 36, padding: 0 }} />
</div>
</div>
)}
<style>{`
@keyframes chatSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@media print { [class*="ChatFab"] { display: none !important; } }
/* Markdown 气泡内的排版 */
.chat-md-bubble p { margin: 5px 0; text-indent: 0; }
.chat-md-bubble ul, .chat-md-bubble ol { margin: 5px 0 5px 1.4em; padding: 0; }
.chat-md-bubble li { margin: 3px 0; }
.chat-md-bubble li > p { margin: 0; }
.chat-md-bubble strong { color: #1a56db; }
.chat-md-bubble h1, .chat-md-bubble h2, .chat-md-bubble h3 {
font-size: 16px; font-weight: 700; margin: 10px 0 5px 0;
color: #1e3a5f;
}
.chat-md-bubble table { border-collapse: collapse; margin: 8px 0; font-size: 13.5px; width: 100%; }
.chat-md-bubble th, .chat-md-bubble td {
border: 1px solid #e5e7eb; padding: 4px 8px; text-align: left;
}
.chat-md-bubble th { background: #f1f5f9; font-weight: 600; }
.chat-md-bubble code {
background: #f1f5f9; padding: 1px 5px; border-radius: 3px;
font-size: 13.5px; color: #c2410c;
}
.chat-md-bubble blockquote {
border-left: 3px solid #3b82f6; margin: 6px 0; padding: 4px 10px;
background: #eff6ff; color: #1e40af;
}
.chat-md-bubble hr { border: none; border-top: 1px solid #e5e7eb; margin: 8px 0; }
@media (max-width: 768px) {
/* 移动端自适应 — 对话面板全宽 */
}
`}</style>
</>
);
}
@@ -0,0 +1,126 @@
/**
* CameraController — gsap驱动的丝滑相机飞行
*
* 镜头策略:
* 初始 → 对准第一个阶段(x=-35),近景
* 切换阶段 → 相机飞到该阶段上方,拉远到能看清标题(y=22)
* 最后一个阶段(step=5) → 1.5s后自动斜视回全局俯瞰
* 点击节点 → 聚焦飞过去
*/
import { useEffect, useRef, useCallback } from 'react';
import { useThree, useFrame } from '@react-three/fiber';
import gsap from 'gsap';
import type { StageIndex } from './types';
import { STAGE_POSITIONS } from './types';
interface Props {
currentStep: StageIndex;
focusTarget?: { x: number; y: number; z: number } | null;
onFocusDone?: () => void;
}
export default function CameraController({ currentStep, focusTarget, onFocusDone }: Props) {
const { camera } = useThree();
const controlsRef = useRef<any>(null);
const tweenRef = useRef<gsap.core.Timeline | null>(null);
const overviewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isFirstMount = useRef(true);
useFrame((state) => {
if (!controlsRef.current && (state as any).controls) {
controlsRef.current = (state as any).controls;
}
});
const flyTo = useCallback((
pos: { x: number; y: number; z: number },
lookAt: { x: number; y: number; z: number },
duration = 1.5,
) => {
if (tweenRef.current) {
tweenRef.current.kill();
}
const tl = gsap.timeline();
tweenRef.current = tl;
tl.to(camera.position, {
x: pos.x, y: pos.y, z: pos.z,
duration, ease: 'power2.inOut',
}, 0);
if (controlsRef.current?.target) {
tl.to(controlsRef.current.target, {
x: lookAt.x, y: lookAt.y, z: lookAt.z,
duration, ease: 'power2.inOut',
}, 0);
}
return tl;
}, [camera]);
// 阶段切换 → 相机飞行
useEffect(() => {
// 清除之前的全局俯瞰定时器
if (overviewTimerRef.current) {
clearTimeout(overviewTimerRef.current);
overviewTimerRef.current = null;
}
const centerX = STAGE_POSITIONS[currentStep];
if (isFirstMount.current) {
// ====== 初始:偏左看第一个阶段全貌 ======
isFirstMount.current = false;
camera.position.set(centerX - 18, 22, 55); // 左移18+后移55,看到节点1完整圆盘和文件夹
if (controlsRef.current?.target) {
controlsRef.current.target.set(centerX, 8, 0);
}
return;
}
if (currentStep === 5) {
// ====== 最后阶段:先飞到Stage5,然后2s后斜视全局 ======
flyTo(
{ x: centerX, y: 25, z: 45 },
{ x: centerX, y: 8, z: 0 },
);
overviewTimerRef.current = setTimeout(() => {
// 从偏右上方俯瞰全局(场景x:-35~35,需要足够远才能全部看到)
flyTo(
{ x: 25, y: 35, z: 75 }, // 右移+拉高+拉远,确保6个阶段全部入镜
{ x: 0, y: 6, z: 0 }, // 看向全局中心
2.5,
);
}, 2500);
} else {
// ====== 普通阶段:飞到该阶段,拉远够高以看到标题(y=22) ======
flyTo(
{ x: centerX, y: 25, z: 45 }, // y=25看得到y=22标题, z=45够远
{ x: centerX, y: 8, z: 0 }, // 看向阶段主体中心
);
}
return () => {
if (overviewTimerRef.current) {
clearTimeout(overviewTimerRef.current);
overviewTimerRef.current = null;
}
};
}, [currentStep, flyTo, camera]);
// 点击节点聚焦
useEffect(() => {
if (!focusTarget) return;
flyTo(
{ x: focusTarget.x, y: focusTarget.y + 10, z: focusTarget.z + 22 },
{ x: focusTarget.x, y: focusTarget.y, z: focusTarget.z },
1.0,
).then(() => {
onFocusDone?.();
});
}, [focusTarget, flyTo, onFocusDone]);
return null;
}
@@ -0,0 +1,259 @@
/**
* DataTraceView — 数据溯源可视化主容器
*
* 路由: /district/:district/trace/:school
* 职责: Canvas + 6个阶段 + 粒子 + 管道 + 相机飞行 + 交互 + UI叠加层
*/
import { useState, useCallback, Suspense } from 'react';
import { Canvas } from '@react-three/fiber';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Scene from './Scene';
import CameraController from './CameraController';
import Interaction, { type TooltipData } from './Interaction';
import { useTraceData } from './hooks/useTraceData';
import { useStepControl } from './hooks/useStepControl';
import { STAGE_COLORS } from './types';
// Stages
import RawDataStage from './stages/RawDataStage';
import ScoringStage from './stages/ScoringStage';
import PCAStage from './stages/PCAStage';
import StandardStage from './stages/StandardStage';
import LevelStage from './stages/LevelStage';
import DimensionStage from './stages/DimensionStage';
// Effects
import Particles from './effects/Particles';
import Connections from './effects/Connections';
// UI
import StepBar from './ui/StepBar';
import InfoPanel from './ui/InfoPanel';
import Tooltip from './ui/Tooltip';
function LoadingFallback() {
return (
<mesh>
<sphereGeometry args={[1, 16, 16]} />
<meshStandardMaterial color="#4fc3f7" wireframe />
</mesh>
);
}
export default function DataTraceView() {
const { district, school } = useParams<{ district: string; school: string }>();
const navigate = useNavigate();
const { t, i18n } = useTranslation();
const isEn = i18n.language?.startsWith('en');
const { data, loading, error } = useTraceData(district ?? '', school ?? '');
const {
currentStep, autoPlaying,
goToStep, nextStep, prevStep, reset, toggleAutoPlay,
} = useStepControl();
// 交互状态
const [tooltip, setTooltip] = useState<TooltipData>({ text: '', x: 0, y: 0, visible: false });
const [focusTarget, setFocusTarget] = useState<{ x: number; y: number; z: number } | null>(null);
const handleNodeClick = useCallback((worldPos: { x: number; y: number; z: number }, _userData: any) => {
setFocusTarget(worldPos);
}, []);
const handleFocusDone = useCallback(() => {
setFocusTarget(null);
}, []);
// 加载状态
if (loading || !data) {
return (
<div style={{
width: '100vw', height: '100vh',
background: STAGE_COLORS.bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#4fc3f7', fontSize: 18,
}}>
{loading ? (
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 40, marginBottom: 16 }}></div>
<div>
{isEn
? `Loading computation pipeline for ${school}...`
: `正在加载 ${school} 的计算链路...`}
</div>
</div>
) : error ? (
<div style={{ textAlign: 'center', color: '#ef5350' }}>
<div style={{ fontSize: 40, marginBottom: 16 }}></div>
<div>{isEn ? `Failed to load: ${error}` : `加载失败: ${error}`}</div>
<button
onClick={() => navigate(-1)}
style={{
marginTop: 16, padding: '8px 20px',
background: 'transparent', border: '1px solid #ef5350',
color: '#ef5350', borderRadius: 8, cursor: 'pointer',
}}
>
{t('common.back')}
</button>
</div>
) : null}
</div>
);
}
return (
<div style={{ width: '100vw', height: '100vh', background: STAGE_COLORS.bg, position: 'relative' }}>
{/* 顶部标题栏 */}
<div style={{
position: 'absolute', top: 0, left: 0, right: 0,
height: 64, padding: '0 24px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
background: 'rgba(10,14,23,0.85)',
borderBottom: '1px solid rgba(79,195,247,0.15)',
zIndex: 100,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<button
onClick={() => navigate(-1)}
style={{
background: 'transparent', border: '1px solid rgba(79,195,247,0.3)',
color: '#4fc3f7', borderRadius: 8, padding: '6px 14px',
cursor: 'pointer', fontSize: 14,
}}
>
{t('trace.back')}
</button>
<span style={{ color: '#ffffff', fontSize: 18, fontWeight: 600 }}>
{t('trace.title')}
</span>
<span style={{ color: '#4fc3f7', fontSize: 16 }}>
{data.school}
</span>
</div>
<div style={{ color: '#667788', fontSize: 13 }}>
{isEn ? 'Score: ' : '总分: '}
<span style={{ color: '#e53935', fontSize: 18, fontWeight: 700 }}>
{data.stages.overall.score?.toFixed(1)}
</span>
<span style={{ marginLeft: 12 }}>
{isEn
? `${data.city_school_count} citywide · ${data.district_school_count} in district`
: `全市${data.city_school_count}校 · 区内${data.district_school_count}`}
</span>
</div>
</div>
{/* Three.js Canvas */}
<Canvas
camera={{
position: [-53, 22, 55], // 初始偏左+后移,看到第一个阶段完整全貌
fov: 50,
near: 0.1,
far: 500,
}}
shadows
gl={{
antialias: true,
toneMapping: 6, // ACESFilmic
toneMappingExposure: 1.2,
}}
style={{ width: '100%', height: '100%' }}
>
<Suspense fallback={<LoadingFallback />}>
<Scene />
{/* 相机飞行控制器 */}
<CameraController
currentStep={currentStep}
focusTarget={focusTarget}
onFocusDone={handleFocusDone}
/>
{/* Raycaster交互 */}
<Interaction
onTooltipChange={setTooltip}
onNodeClick={handleNodeClick}
/>
{/* 6个阶段 */}
<RawDataStage data={data.stages.raw_data} visible={currentStep >= 0} />
<ScoringStage scoring={data.stages.scoring} visible={currentStep >= 1} />
<PCAStage pcaDetails={data.stages.pca_detail} visible={currentStep >= 2} />
<StandardStage standardized={data.stages.standardized} visible={currentStep >= 3} />
<LevelStage levels={data.stages.levels} visible={currentStep >= 4} />
<DimensionStage
dimensions={data.stages.dimensions}
overall={data.stages.overall}
school={data.school}
visible={currentStep >= 5}
/>
{/* 粒子系统(管道流动+汇聚+shader辉光) */}
<Particles currentStep={currentStep} />
{/* CatmullRom管道连接 */}
<Connections currentStep={currentStep} />
</Suspense>
</Canvas>
{/* UI叠加层 */}
<InfoPanel currentStep={currentStep} data={data} />
<Tooltip data={tooltip} />
<StepBar
currentStep={currentStep}
onStepChange={goToStep}
autoPlaying={autoPlaying}
onToggleAutoPlay={toggleAutoPlay}
onReset={reset}
onNext={nextStep}
onPrev={prevStep}
/>
{/* 右上角操作说明 */}
<div style={{
position: 'absolute', top: 76, right: 16,
background: 'rgba(10,14,23,0.75)',
border: '1px solid rgba(79,195,247,0.15)',
borderRadius: 8, padding: '10px 14px',
color: '#667788', fontSize: 12, lineHeight: 1.8,
zIndex: 100, pointerEvents: 'none',
}}>
<div style={{ color: '#8899aa', fontSize: 11, marginBottom: 4, fontWeight: 600 }}>
🖱 {isEn ? 'Mouse' : '鼠标操作'}
</div>
{isEn ? (
<>
<div>Drag (left) Rotate view</div>
<div>Drag (right) Pan</div>
<div>Scroll Zoom</div>
<div>Click node Focus</div>
</>
) : (
<>
<div> </div>
<div> </div>
<div> </div>
<div> </div>
</>
)}
<div style={{ color: '#8899aa', fontSize: 11, marginTop: 6, marginBottom: 4, fontWeight: 600 }}>
{isEn ? 'Shortcuts' : '快捷键'}
</div>
{isEn ? (
<>
<div> Switch stage</div>
<div>Space Next step</div>
<div>R Reset</div>
</>
) : (
<>
<div> </div>
<div> </div>
<div>R </div>
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,138 @@
/**
* Interaction.tsx — Raycaster 交互系统
*
* 功能:
* 1. 悬浮高亮(emissiveIntensity 增强 + scale 放大)
* 2. 点击节点 → 通知父组件飞行聚焦
* 3. 暴露tooltip数据给UI层
*
* 策略:不在R3F内部做tooltip DOM,而是通过回调把hover数据传给外层HTML
*/
import { useRef, useCallback } from 'react';
import { useThree, useFrame } from '@react-three/fiber';
import gsap from 'gsap';
import * as THREE from 'three';
export interface TooltipData {
text: string;
x: number; // screen px
y: number;
visible: boolean;
}
interface Props {
onTooltipChange: (data: TooltipData) => void;
onNodeClick: (worldPos: { x: number; y: number; z: number }, userData: any) => void;
}
export default function Interaction({ onTooltipChange, onNodeClick }: Props) {
const { camera, scene, gl } = useThree();
const raycaster = useRef(new THREE.Raycaster());
const mouse = useRef(new THREE.Vector2(-999, -999));
const hoveredRef = useRef<THREE.Mesh | null>(null);
const origEmissiveRef = useRef<number>(0.3);
// 鼠标移动
const onPointerMove = useCallback((e: PointerEvent) => {
const rect = gl.domElement.getBoundingClientRect();
mouse.current.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.current.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
}, [gl]);
// 鼠标点击
const onPointerDown = useCallback(() => {
if (hoveredRef.current) {
const pos = new THREE.Vector3();
hoveredRef.current.getWorldPosition(pos);
onNodeClick(
{ x: pos.x, y: pos.y, z: pos.z },
hoveredRef.current.userData,
);
}
}, [onNodeClick]);
// 绑定DOM事件
useFrame(() => {
const dom = gl.domElement;
// 这里用原始addEventListener来确保每帧检测
// 但只绑定一次(通过userData标记)
if (!(dom as any).__interactionBound) {
dom.addEventListener('pointermove', onPointerMove);
dom.addEventListener('pointerdown', onPointerDown);
(dom as any).__interactionBound = true;
}
});
// 每帧 raycast
useFrame(() => {
raycaster.current.setFromCamera(mouse.current, camera);
// 收集所有可交互的mesh
const interactables: THREE.Mesh[] = [];
scene.traverse((obj) => {
if ((obj as THREE.Mesh).isMesh && obj.userData?.tooltip) {
interactables.push(obj as THREE.Mesh);
}
});
const intersects = raycaster.current.intersectObjects(interactables, false);
if (intersects.length > 0) {
const mesh = intersects[0].object as THREE.Mesh;
// 新悬浮对象
if (hoveredRef.current !== mesh) {
// 取消上一个高亮
if (hoveredRef.current) {
_unhighlight(hoveredRef.current, origEmissiveRef.current);
}
hoveredRef.current = mesh;
origEmissiveRef.current = (mesh.material as THREE.MeshStandardMaterial)?.emissiveIntensity ?? 0.3;
_highlight(mesh);
}
// tooltip位置(屏幕坐标)
const worldPos = new THREE.Vector3();
mesh.getWorldPosition(worldPos);
const screenPos = worldPos.clone().project(camera);
const rect = gl.domElement.getBoundingClientRect();
const sx = (screenPos.x * 0.5 + 0.5) * rect.width + rect.left + 15;
const sy = (-screenPos.y * 0.5 + 0.5) * rect.height + rect.top - 10;
onTooltipChange({
text: mesh.userData.tooltip,
x: sx,
y: sy,
visible: true,
});
gl.domElement.style.cursor = 'pointer';
} else {
if (hoveredRef.current) {
_unhighlight(hoveredRef.current, origEmissiveRef.current);
hoveredRef.current = null;
}
onTooltipChange({ text: '', x: 0, y: 0, visible: false });
gl.domElement.style.cursor = 'grab';
}
});
return null;
}
// gsap高亮动画
function _highlight(mesh: THREE.Mesh) {
const mat = mesh.material as THREE.MeshStandardMaterial;
if (mat?.emissiveIntensity !== undefined) {
gsap.to(mat, { emissiveIntensity: 0.8, duration: 0.3 });
}
gsap.to(mesh.scale, { x: 1.1, y: 1.1, z: 1.1, duration: 0.3 });
}
function _unhighlight(mesh: THREE.Mesh, origEmissive: number) {
const mat = mesh.material as THREE.MeshStandardMaterial;
if (mat?.emissiveIntensity !== undefined) {
gsap.to(mat, { emissiveIntensity: origEmissive, duration: 0.3 });
}
gsap.to(mesh.scale, { x: 1, y: 1, z: 1, duration: 0.3 });
}
+116
View File
@@ -0,0 +1,116 @@
/**
* Scene.tsx — R3F场景基础设施:灯光、雾、网格地面、环境粒子
* OrbitControls 通过 makeDefault 暴露给 useThree().controls
*/
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import * as THREE from 'three';
/** 发光网格地面 */
function Grid() {
const geo = useMemo(() => {
const size = 100;
const divisions = 40;
const half = size / 2;
const step = size / divisions;
const positions: number[] = [];
for (let i = 0; i <= divisions; i++) {
const p = -half + i * step;
positions.push(-half, 0, p, half, 0, p);
positions.push(p, 0, -half, p, 0, half);
}
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
return g;
}, []);
return (
<lineSegments geometry={geo} position={[0, -0.5, 0]}>
<lineBasicMaterial color="#1a2040" transparent opacity={0.4} />
</lineSegments>
);
}
/** 环境漂浮粒子 */
function AmbientParticles() {
const ref = useRef<THREE.Points>(null!);
const count = 600;
const [positions, colors] = useMemo(() => {
const pos = new Float32Array(count * 3);
const col = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * 120;
pos[i * 3 + 1] = Math.random() * 50;
pos[i * 3 + 2] = (Math.random() - 0.5) * 120;
const c = new THREE.Color().setHSL(0.55 + Math.random() * 0.15, 0.6, 0.3 + Math.random() * 0.2);
col[i * 3] = c.r;
col[i * 3 + 1] = c.g;
col[i * 3 + 2] = c.b;
}
return [pos, col];
}, []);
useFrame(() => {
if (ref.current) {
ref.current.rotation.y += 0.0002;
}
});
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
<bufferAttribute attach="attributes-color" args={[colors, 3]} />
</bufferGeometry>
<pointsMaterial
size={0.15}
vertexColors
transparent
opacity={0.5}
blending={THREE.AdditiveBlending}
depthWrite={false}
/>
</points>
);
}
export default function Scene() {
return (
<>
{/* 雾效 */}
<fogExp2 attach="fog" args={['#0a0e17', 0.008]} />
{/* 灯光 */}
<ambientLight intensity={0.6} color="#334466" />
<directionalLight
position={[20, 40, 30]}
intensity={0.8}
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
/>
<directionalLight position={[-15, 20, -10]} intensity={0.3} color="#4488cc" />
<pointLight position={[0, 30, 0]} intensity={0.5} color="#4fc3f7" distance={80} />
{/*
OrbitControls — makeDefault 使其可通过 useThree().controls 访问
这是CameraController能操作target的关键
*/}
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.05}
minDistance={15}
maxDistance={120}
maxPolarAngle={Math.PI * 0.75}
target={[0, 5, 0]}
/>
{/* 网格地面 + 环境粒子 */}
<Grid />
<AmbientParticles />
</>
);
}
@@ -0,0 +1,79 @@
/**
* Connections.tsx — 阶段间CatmullRom管道连接 + 数据流转标注
* 每段管道旁标注数据变化摘要,帮助理解每一步的输入输出
*/
import { useMemo } from 'react';
import { Text } from '@react-three/drei';
import * as THREE from 'three';
import type { StageIndex } from '../types';
import { STAGE_POSITIONS, CN_FONT } from '../types';
// 管道上方的数据流转标注
const PIPE_LABELS = [
'1048字段', // 原始数据 → 赋分
'20个原始分', // 赋分 → PCA
'20个标准化分', // PCA → 分布
'20个水平等级', // 分布 → 水平
'7维度 → 1总分', // 水平 → 维度
];
interface Props {
currentStep: StageIndex;
}
export default function Connections({ currentStep }: Props) {
const tubes = useMemo(() => {
const result: Array<{
geometry: THREE.TubeGeometry;
from: number;
to: number;
midX: number;
}> = [];
for (let i = 0; i < STAGE_POSITIONS.length - 1; i++) {
const x1 = STAGE_POSITIONS[i] + 5;
const x2 = STAGE_POSITIONS[i + 1] - 5;
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(x1, 6, 0),
new THREE.Vector3((x1 + x2) / 2, 8, 0),
new THREE.Vector3(x2, 6, 0),
]);
const geo = new THREE.TubeGeometry(curve, 20, 0.08, 6, false);
result.push({ geometry: geo, from: i, to: i + 1, midX: (x1 + x2) / 2 });
}
return result;
}, []);
return (
<group>
{tubes.map((tube, i) => {
const active = tube.to <= currentStep;
return (
<group key={i}>
{/* 管道 */}
<mesh geometry={tube.geometry}>
<meshBasicMaterial
color={active ? '#4fc3f7' : '#334466'}
transparent
opacity={active ? 0.6 : 0.25}
/>
</mesh>
{/* 数据流转标注 — 管道上方 */}
{active && PIPE_LABELS[i] && (
<Text
position={[tube.midX, 9.5, 0]}
fontSize={0.3}
color="#667788"
anchorX="center"
anchorY="middle"
font={CN_FONT}
>
{PIPE_LABELS[i]}
</Text>
)}
</group>
);
})}
</group>
);
}
@@ -0,0 +1,294 @@
/**
* Particles.tsx — 数据流粒子动画系统
*
* 对齐demo-visual的粒子效果:
* 1. 管道粒子:沿CatmullRom曲线从前一阶段流向当前阶段,带颜色渐变和wobble
* 2. 汇聚粒子:从阶段周围散射位置 ease-out 汇聚到中心
* 3. 自定义 shader:辉光点精灵(径向渐变 + additive blending
*/
import { useRef, useMemo, useEffect, useCallback } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import type { StageIndex } from '../types';
import { STAGE_POSITIONS } from '../types';
const MAX_PARTICLES = 200;
const STAGE_COLORS_VEC = [
new THREE.Color(0x4fc3f7), // 原始数据 - 蓝
new THREE.Color(0xffd54f), // 赋分 - 金
new THREE.Color(0x81c784), // PCA - 绿
new THREE.Color(0xba68c8), // 标准化 - 紫
new THREE.Color(0xff8a65), // 水平 - 橙
new THREE.Color(0xe53935), // 总分 - 红
];
// 阶段间的CatmullRom管道曲线
function buildCurves(): THREE.CatmullRomCurve3[] {
const curves: THREE.CatmullRomCurve3[] = [];
for (let i = 0; i < STAGE_POSITIONS.length - 1; i++) {
const x1 = STAGE_POSITIONS[i] + 5;
const x2 = STAGE_POSITIONS[i + 1] - 5;
curves.push(new THREE.CatmullRomCurve3([
new THREE.Vector3(x1, 6, 0),
new THREE.Vector3((x1 + x2) / 2, 8, 0),
new THREE.Vector3(x2, 6, 0),
]));
}
return curves;
}
interface Particle {
idx: number;
curve: THREE.CatmullRomCurve3 | null;
t: number;
speed: number;
fromColor: THREE.Color;
toColor: THREE.Color;
size: number;
wobble: number;
wobbleSpeed: number;
wobbleAmp: number;
// 汇聚模式
converge: boolean;
startPos?: THREE.Vector3;
targetPos?: THREE.Vector3;
}
interface Props {
currentStep: StageIndex;
}
export default function Particles({ currentStep }: Props) {
const pointsRef = useRef<THREE.Points>(null!);
const curvesRef = useRef(buildCurves());
const poolRef = useRef<number[]>([...Array(MAX_PARTICLES).keys()]);
const activeRef = useRef<Particle[]>([]);
const prevStepRef = useRef<StageIndex>(-1 as StageIndex);
const positions = useMemo(() => {
const arr = new Float32Array(MAX_PARTICLES * 3);
for (let i = 0; i < MAX_PARTICLES; i++) {
arr[i * 3 + 1] = -100; // 隐藏在下方
}
return arr;
}, []);
const colors = useMemo(() => {
const arr = new Float32Array(MAX_PARTICLES * 3);
for (let i = 0; i < MAX_PARTICLES; i++) {
arr[i * 3] = 0.3;
arr[i * 3 + 1] = 0.7;
arr[i * 3 + 2] = 0.95;
}
return arr;
}, []);
const sizes = useMemo(() => new Float32Array(MAX_PARTICLES).fill(0), []);
// Shader材质
const shaderMaterial = useMemo(() => new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 } },
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
varying float vAlpha;
void main() {
vColor = color;
vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (200.0 / -mvPos.z);
gl_Position = projectionMatrix * mvPos;
vAlpha = size > 0.0 ? 1.0 : 0.0;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vAlpha;
void main() {
float dist = length(gl_PointCoord - vec2(0.5));
if (dist > 0.5) discard;
float glow = 1.0 - smoothstep(0.0, 0.5, dist);
glow = pow(glow, 1.5);
gl_FragColor = vec4(vColor * 1.5, glow * vAlpha);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
}), []);
const getParticle = useCallback(() => {
if (poolRef.current.length === 0) return null;
return poolRef.current.pop()!;
}, []);
const returnParticle = useCallback((idx: number) => {
positions[idx * 3 + 1] = -100;
sizes[idx] = 0;
poolRef.current.push(idx);
}, [positions, sizes]);
// 管道粒子发射
const emitBetween = useCallback((fromStage: number, toStage: number) => {
const curveIdx = fromStage;
if (curveIdx < 0 || curveIdx >= curvesRef.current.length) return;
const curve = curvesRef.current[curveIdx];
const batchSize = 15;
for (let i = 0; i < batchSize; i++) {
const idx = getParticle();
if (idx === null) break;
const t = Math.random() * 0.3;
const point = curve.getPoint(t);
activeRef.current.push({
idx,
curve,
t,
speed: 0.002 + Math.random() * 0.004,
fromColor: STAGE_COLORS_VEC[fromStage] ?? STAGE_COLORS_VEC[0],
toColor: STAGE_COLORS_VEC[toStage] ?? STAGE_COLORS_VEC[5],
size: 1.5 + Math.random() * 2,
wobble: Math.random() * Math.PI * 2,
wobbleSpeed: 0.5 + Math.random() * 1.5,
wobbleAmp: 0.3 + Math.random() * 0.5,
converge: false,
});
positions[idx * 3] = point.x;
positions[idx * 3 + 1] = point.y;
positions[idx * 3 + 2] = point.z;
sizes[idx] = 0.1;
}
}, [getParticle, positions, sizes]);
// 汇聚粒子发射
const emitConverge = useCallback((stageIndex: number) => {
const center = new THREE.Vector3(STAGE_POSITIONS[stageIndex], 8, 0);
const color = STAGE_COLORS_VEC[stageIndex] ?? STAGE_COLORS_VEC[0];
const batchSize = 20;
for (let i = 0; i < batchSize; i++) {
const idx = getParticle();
if (idx === null) break;
const startPos = new THREE.Vector3(
center.x + (Math.random() - 0.5) * 12,
center.y + (Math.random() - 0.5) * 12,
center.z + (Math.random() - 0.5) * 8,
);
activeRef.current.push({
idx,
curve: null,
t: 0,
speed: 0.008 + Math.random() * 0.008,
fromColor: color,
toColor: color,
size: 1.0 + Math.random() * 1.5,
wobble: Math.random() * Math.PI * 2,
wobbleSpeed: 1 + Math.random() * 2,
wobbleAmp: 0.2 + Math.random() * 0.3,
converge: true,
startPos: startPos.clone(),
targetPos: center.clone(),
});
positions[idx * 3] = startPos.x;
positions[idx * 3 + 1] = startPos.y;
positions[idx * 3 + 2] = startPos.z;
sizes[idx] = 0.1;
}
}, [getParticle, positions, sizes]);
// 阶段切换时触发粒子
useEffect(() => {
const prev = prevStepRef.current;
prevStepRef.current = currentStep;
if (prev >= 0 && currentStep > prev) {
// 从前一个阶段到当前阶段逐段发射管道粒子
for (let s = prev; s < currentStep; s++) {
setTimeout(() => emitBetween(s, s + 1), (s - prev) * 400);
}
}
// 汇聚粒子
setTimeout(() => emitConverge(currentStep), currentStep > prev ? 600 : 100);
}, [currentStep, emitBetween, emitConverge]);
// 每帧更新粒子
useFrame((_, delta) => {
shaderMaterial.uniforms.uTime.value += delta;
const toRemove: number[] = [];
activeRef.current.forEach((p, pi) => {
p.t += p.speed;
p.wobble += p.wobbleSpeed * delta;
if (p.converge && p.startPos && p.targetPos) {
// 汇聚
const lerpT = Math.min(p.t / 0.5, 1);
const eased = 1 - Math.pow(1 - lerpT, 3); // ease-out cubic
const cx = THREE.MathUtils.lerp(p.startPos.x, p.targetPos.x, eased);
const cy = THREE.MathUtils.lerp(p.startPos.y, p.targetPos.y, eased);
const cz = THREE.MathUtils.lerp(p.startPos.z, p.targetPos.z, eased);
positions[p.idx * 3] = cx + Math.sin(p.wobble) * p.wobbleAmp * (1 - eased);
positions[p.idx * 3 + 1] = cy + Math.cos(p.wobble * 0.7) * p.wobbleAmp * (1 - eased);
positions[p.idx * 3 + 2] = cz;
sizes[p.idx] = p.size * (1 - eased);
if (lerpT >= 1) toRemove.push(pi);
} else if (p.curve) {
// 管道流动
if (p.t >= 1) {
toRemove.push(pi);
} else {
const point = p.curve.getPoint(p.t);
positions[p.idx * 3] = point.x + Math.sin(p.wobble) * p.wobbleAmp;
positions[p.idx * 3 + 1] = point.y + Math.cos(p.wobble * 0.7) * p.wobbleAmp;
positions[p.idx * 3 + 2] = point.z + Math.sin(p.wobble * 1.3) * p.wobbleAmp * 0.5;
// 颜色渐变
const c = p.fromColor.clone().lerp(p.toColor, p.t);
colors[p.idx * 3] = c.r;
colors[p.idx * 3 + 1] = c.g;
colors[p.idx * 3 + 2] = c.b;
// 尺寸:中间大两头小
const sizeT = Math.sin(p.t * Math.PI);
sizes[p.idx] = p.size * sizeT;
}
}
});
// 回收
toRemove.sort((a, b) => b - a);
toRemove.forEach(pi => {
const p = activeRef.current[pi];
returnParticle(p.idx);
activeRef.current.splice(pi, 1);
});
// 更新GPU buffer
if (pointsRef.current) {
const geo = pointsRef.current.geometry;
geo.attributes.position.needsUpdate = true;
geo.attributes.color.needsUpdate = true;
(geo.attributes as any).size.needsUpdate = true;
}
});
return (
<points ref={pointsRef} material={shaderMaterial}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
<bufferAttribute attach="attributes-color" args={[colors, 3]} />
<bufferAttribute attach="attributes-size" args={[sizes, 1]} />
</bufferGeometry>
</points>
);
}
@@ -0,0 +1,83 @@
/**
* useStepControl — 步骤状态管理 + 自动播放
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import type { StageIndex } from '../types';
export function useStepControl() {
const [currentStep, setCurrentStep] = useState<StageIndex>(0);
const [autoPlaying, setAutoPlaying] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const goToStep = useCallback((step: StageIndex) => {
setCurrentStep(step);
}, []);
const nextStep = useCallback(() => {
setCurrentStep((prev) => (prev < 5 ? (prev + 1) as StageIndex : prev));
}, []);
const prevStep = useCallback(() => {
setCurrentStep((prev) => (prev > 0 ? (prev - 1) as StageIndex : prev));
}, []);
const reset = useCallback(() => {
setAutoPlaying(false);
setCurrentStep(0);
}, []);
const toggleAutoPlay = useCallback(() => {
setAutoPlaying((prev) => !prev);
}, []);
// 自动播放逻辑
useEffect(() => {
if (!autoPlaying) {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
return;
}
if (currentStep >= 5) {
setAutoPlaying(false);
return;
}
timerRef.current = setTimeout(() => {
setCurrentStep((prev) => (prev + 1) as StageIndex);
}, 3000);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [autoPlaying, currentStep]);
// 键盘快捷键
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault();
nextStep();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
prevStep();
} else if (e.key === 'r' || e.key === 'R') {
reset();
}
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [nextStep, prevStep, reset]);
return {
currentStep,
autoPlaying,
goToStep,
nextStep,
prevStep,
reset,
toggleAutoPlay,
};
}
@@ -0,0 +1,46 @@
/**
* useTraceData — 获取单校的计算链路溯源数据
*/
import { useState, useEffect } from 'react';
import type { TraceData } from '../types';
import { getToken, clearAuth } from '../../../services/auth';
export function useTraceData(district: string, school: string) {
const [data, setData] = useState<TraceData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!district || !school) return;
setLoading(true);
setError(null);
const url = `/api/era2/trace/${encodeURIComponent(district)}/${encodeURIComponent(school)}`;
const token = getToken();
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
fetch(url, { headers })
.then(async (res) => {
if (res.status === 401) {
clearAuth();
window.location.href = '/login';
throw new Error('未登录');
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json as TraceData);
})
.catch((err) => {
setError(err.message);
})
.finally(() => {
setLoading(false);
});
}, [district, school]);
return { data, loading, error };
}
@@ -0,0 +1,144 @@
/**
* Stage 5: 维度聚合 → 总分
* 7个维度柱体围绕中心红球,柱高=得分,连线到中心
*/
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { Text, Line } from '@react-three/drei';
import * as THREE from 'three';
import type { DimensionInfo, OverallInfo } from '../types';
import { DIM_COLORS, STAGE_POSITIONS, CN_FONT } from '../types';
interface Props {
dimensions: Record<string, DimensionInfo>;
overall: OverallInfo;
school: string;
visible: boolean;
}
export default function DimensionStage({ dimensions, overall, visible }: Props) {
const totalRef = useRef<THREE.Mesh>(null!);
const x = STAGE_POSITIONS[5];
const dimEntries = useMemo(() => Object.entries(dimensions), [dimensions]);
// 总分球脉冲
useFrame(({ clock }) => {
if (totalRef.current && visible) {
const s = 1 + Math.sin(clock.elapsedTime * 2) * 0.05;
totalRef.current.scale.set(s, s, s);
}
});
const angleStep = (Math.PI * 2) / Math.max(dimEntries.length, 1);
const radius = 5;
return (
<group visible={visible}>
{/* 阶段标签 */}
<Text position={[x, 22, 0]} fontSize={1.6} color="#4fc3f7" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{/* 7个维度柱体 */}
{dimEntries.map(([name, data], i) => {
const angle = i * angleStep - Math.PI / 2;
const px = x + Math.cos(angle) * radius;
const pz = Math.sin(angle) * radius;
const height = (data.score ?? 50) / 10;
const color = DIM_COLORS[name] || '#ff8a65';
return (
<group key={name}>
{/* 柱体 */}
<mesh
position={[px, height / 2 + 0.5, pz]}
castShadow
userData={{
tooltip: `${name}\n维度分: ${data.score?.toFixed(2) ?? '-'}\n子维度: ${Object.keys(data.sub_scores).join('、')}`,
type: 'dimension-pillar',
}}
>
<cylinderGeometry args={[0.8, 0.8, height, 16]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={0.3}
metalness={0.3}
roughness={0.5}
transparent
opacity={0.92}
/>
</mesh>
{/* 维度名标签 */}
<Text
position={[px, height + 2, pz]}
fontSize={0.7}
color="#ffffff"
anchorX="center"
anchorY="middle"
maxWidth={4}
font={CN_FONT}
>
{name}
</Text>
{/* 得分标签 */}
<Text
position={[px, height + 1.2, pz]}
fontSize={0.5}
color={color}
anchorX="center"
anchorY="middle"
>
{data.score?.toFixed(1) ?? '-'}
</Text>
{/* 连接线到中心 */}
<Line
points={[[px, height + 0.5, pz], [x, 12, 0]]}
color={color}
transparent
opacity={0.3}
lineWidth={1}
/>
</group>
);
})}
{/* 中心总分球 */}
<mesh
ref={totalRef}
position={[x, 12, 0]}
castShadow
userData={{
tooltip: `总体得分: ${overall.score?.toFixed(1) ?? '-'}\n排名: ${overall.rank ?? '-'}/${overall.total_schools}`,
type: 'total-score',
}}
>
<sphereGeometry args={[2, 32, 32]} />
<meshStandardMaterial
color="#e53935"
emissive="#e53935"
emissiveIntensity={0.5}
metalness={0.3}
roughness={0.5}
transparent
opacity={0.92}
/>
</mesh>
{/* 总分数字 */}
<Text position={[x, 16, 0]} fontSize={2} color="#e53935" anchorX="center" anchorY="middle" fontWeight="bold">
{overall.score?.toFixed(1) ?? '-'}
</Text>
{/* 底部平台 */}
<mesh position={[x, 0.15, 0]}>
<cylinderGeometry args={[8, 8, 0.3, 32]} />
<meshStandardMaterial color="#e53935" transparent opacity={0.15} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
@@ -0,0 +1,129 @@
/**
* Stage 4: 水平判定
* 4层色带(水平1-4)+ 子维度球体落入对应层
*/
import { useMemo } from 'react';
import { Text } from '@react-three/drei';
import * as THREE from 'three';
import type { LevelInfo } from '../types';
import { STAGE_POSITIONS, DIM_COLORS, CN_FONT } from '../types';
interface Props {
levels: Record<string, LevelInfo>;
visible: boolean;
}
const LEVEL_COLORS = ['#ef5350', '#ff9800', '#ffd54f', '#81c784']; // 1,2,3,4
const LEVEL_LABELS = ['水平1:尚需努力', '水平2:合格', '水平3:良好', '水平4:优秀'];
export default function LevelStage({ levels, visible }: Props) {
const x = STAGE_POSITIONS[4];
const levelEntries = useMemo(() => Object.entries(levels), [levels]);
// 按水平分组,并预计算每个球的固定位置
const grouped = useMemo(() => {
const g: Record<number, Array<{ name: string; data: LevelInfo; xOff: number; zOff: number }>> = { 1: [], 2: [], 3: [], 4: [] };
// 先分组
const temp: Record<number, Array<{ name: string; data: LevelInfo }>> = { 1: [], 2: [], 3: [], 4: [] };
for (const [name, data] of levelEntries) {
const lv = Math.max(1, Math.min(4, data.level));
temp[lv].push({ name, data });
}
// 计算固定位置(用确定性散布,不用 Math.random
for (const lv of [1, 2, 3, 4]) {
const items = temp[lv];
g[lv] = items.map((item, i) => ({
...item,
xOff: x - 3 + (i % 5) * 1.5, // 网格排列 x
zOff: (i - items.length / 2) * 1.2, // 均匀分布 z
}));
}
return g;
}, [levelEntries, x]);
return (
<group visible={visible}>
{/* 阶段标签 */}
<Text position={[x, 22, 0]} fontSize={1.6} color="#ff8a65" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{/* 4层色带 */}
{[1, 2, 3, 4].map((lv) => {
const y = lv * 3.5 + 1;
const color = LEVEL_COLORS[lv - 1];
const items = grouped[lv] || [];
return (
<group key={lv}>
{/* 色带 */}
<mesh position={[x, y, 0]}>
<boxGeometry args={[12, 3, 10]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={0.1}
transparent
opacity={0.15}
/>
</mesh>
{/* 层边框 */}
<lineSegments position={[x, y, 0]}>
<edgesGeometry args={[new THREE.BoxGeometry(12, 3, 10)]} />
<lineBasicMaterial color={color} transparent opacity={0.3} />
</lineSegments>
{/* 水平标签 */}
<Text position={[x - 6.5, y, 5.5]} fontSize={0.4} color={color} anchorX="right" anchorY="middle" font={CN_FONT}>
{LEVEL_LABELS[lv - 1]}
</Text>
{/* 子维度球 — 位置已在useMemo中预计算,不会每帧抖动 */}
{items.map((item) => {
const dimColor = DIM_COLORS[item.data.parent_dimension] || color;
return (
<group key={item.name}>
<mesh
position={[item.xOff, y, item.zOff]}
castShadow
userData={{
tooltip: `${item.name}\n标准化分: ${item.data.score.toFixed(1)}\n${item.data.description}\n→ 水平${item.data.level}`,
type: 'level-dot',
}}
>
<sphereGeometry args={[0.4, 16, 16]} />
<meshStandardMaterial
color={dimColor}
emissive={dimColor}
emissiveIntensity={0.3}
metalness={0.3}
roughness={0.5}
/>
</mesh>
<Text
position={[item.xOff, y + 0.7, item.zOff]}
fontSize={0.25}
color="#ffffff"
anchorX="center"
anchorY="middle"
maxWidth={3}
font={CN_FONT}
>
{item.name}
</Text>
</group>
);
})}
</group>
);
})}
{/* 底部平台 */}
<mesh position={[x, 0.15, 0]}>
<cylinderGeometry args={[7, 7, 0.3, 32]} />
<meshStandardMaterial color="#ff8a65" transparent opacity={0.15} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
@@ -0,0 +1,226 @@
/**
* Stage 2: PCA标准化(完整的 Z→PCA→×10+50 流程)
*
* 视觉设计:
* 左侧:多个输入变量柱(代表题目赋分值)
* 中间:收缩漏斗(代表PCA降维)
* 右侧:单一得分柱(×10+50后的标准化分)
* 底部:解释方差环 + Loadings前3名
*/
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { Text, Line } from '@react-three/drei';
import * as THREE from 'three';
import type { PCADetail } from '../types';
import { STAGE_POSITIONS, CN_FONT } from '../types';
interface Props {
pcaDetails: Record<string, PCADetail>;
visible: boolean;
}
/** 收缩漏斗 — 代表PCA降维过程 */
function Funnel({ position, color }: { position: [number, number, number]; color: string }) {
const ref = useRef<THREE.Mesh>(null!);
useFrame(({ clock }) => {
if (ref.current) {
// 微微呼吸效果
const s = 1 + Math.sin(clock.elapsedTime * 1.5) * 0.03;
ref.current.scale.set(s, 1, s);
}
});
return (
<mesh ref={ref} position={position} rotation={[Math.PI, 0, 0]}>
<coneGeometry args={[3, 5, 6, 1, true]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={0.15}
transparent
opacity={0.3}
side={THREE.DoubleSide}
/>
</mesh>
);
}
export default function PCAStage({ pcaDetails, visible }: Props) {
const x = STAGE_POSITIONS[2];
// 找一个有详细数据的PCA示例
const examplePCA = useMemo(() => {
for (const [name, detail] of Object.entries(pcaDetails)) {
if (detail.type === 'pca' && detail.loadings && detail.loadings.length > 0) {
return { name, detail };
}
}
const first = Object.entries(pcaDetails)[0];
return first ? { name: first[0], detail: first[1] } : null;
}, [pcaDetails]);
const nVars = examplePCA?.detail.n_variables ?? 8;
const nSchools = examplePCA?.detail.n_schools ?? 266;
const evr = examplePCA?.detail.explained_variance_ratio ?? 0;
const schoolScore = examplePCA?.detail.school_standardized ?? 50;
const topLoadings = useMemo(() => {
return (examplePCA?.detail.loadings ?? []).slice(0, 5);
}, [examplePCA]);
return (
<group visible={visible}>
{/* 阶段标题 */}
<Text position={[x, 22, 0]} fontSize={1.6} color="#81c784" anchorX="center" anchorY="middle" font={CN_FONT}>
PCA标准化
</Text>
{/* ===== 左侧:输入变量柱群(代表多个题目赋分值) ===== */}
<group position={[x - 5, 5, 0]}>
{Array.from({ length: Math.min(nVars, 10) }).map((_, i) => {
const h = 2 + (i % 3) * 1.5; // 不同高度代表不同值
const z = (i - Math.min(nVars, 10) / 2) * 0.8;
return (
<mesh key={i} position={[0, h / 2, z]}>
<boxGeometry args={[0.5, h, 0.5]} />
<meshStandardMaterial
color="#81c784"
emissive="#81c784"
emissiveIntensity={0.2}
transparent
opacity={0.7}
/>
</mesh>
);
})}
<Text position={[0, -0.8, 0]} fontSize={0.35} color="#8899aa" anchorX="center" anchorY="middle" font={CN_FONT}>
{nVars}
</Text>
</group>
{/* ===== 中间:收缩漏斗(PCA降维) ===== */}
<Funnel position={[x, 8, 0]} color="#81c784" />
{/* 漏斗上方标注 */}
<Text position={[x, 12, 0]} fontSize={0.5} color="#81c784" anchorX="center" anchorY="middle" font={CN_FONT}>
PCA第一主成分
</Text>
{/* 漏斗左右箭头线:输入→漏斗→输出 */}
<Line
points={[[x - 3.5, 8, 0], [x - 1.5, 8, 0]]}
color="#81c784" lineWidth={2} transparent opacity={0.5}
/>
<Line
points={[[x + 1.5, 6, 0], [x + 3.5, 6, 0]]}
color="#81c784" lineWidth={2} transparent opacity={0.5}
/>
{/* ===== 右侧:输出得分柱(×10+50后) ===== */}
{/* 基底y=1(圆盘上表面y=0.3,留余量),柱高按比例缩放 */}
<group position={[x + 5, 0, 0]}>
{(() => {
const barH = Math.max(1, schoolScore / 8); // 50→6.25, 映射更紧凑
const baseY = 1; // 柱底离地
return (
<>
<mesh position={[0, baseY + barH / 2, 0]}
userData={{
tooltip: `该校标准化得分: ${schoolScore.toFixed(1)}\n(均值50, 标准差10)`,
type: 'pca-score',
}}
>
<boxGeometry args={[1.2, barH, 1.2]} />
<meshStandardMaterial
color="#e53935"
emissive="#e53935"
emissiveIntensity={0.3}
metalness={0.3}
roughness={0.5}
transparent
opacity={0.9}
/>
</mesh>
<Text position={[0, baseY + barH + 0.5, 0]} fontSize={0.5} color="#e53935" anchorX="center" anchorY="middle">
{schoolScore.toFixed(1)}
</Text>
<Text position={[0, baseY - 0.5, 0]} fontSize={0.35} color="#8899aa" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
</>
);
})()}
</group>
{/* ===== 底部信息区 ===== */}
{/* 解释方差环 */}
{evr > 0 && (
<group position={[x - 2, 2, 4]}>
<mesh>
<ringGeometry args={[1.2, 1.6, 32, 1, 0, Math.PI * 2 * evr]} />
<meshStandardMaterial color="#81c784" emissive="#81c784" emissiveIntensity={0.4} side={THREE.DoubleSide} />
</mesh>
<mesh>
<ringGeometry args={[1.2, 1.6, 32]} />
<meshStandardMaterial color="#1a2040" transparent opacity={0.3} side={THREE.DoubleSide} />
</mesh>
<Text position={[0, 0, 0.1]} fontSize={0.4} color="#81c784" anchorX="center" anchorY="middle">
{(evr * 100).toFixed(0)}%
</Text>
<Text position={[0, -0.6, 0.1]} fontSize={0.25} color="#8899aa" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
</group>
)}
{/* Loadings前几名 */}
{topLoadings.length > 0 && (
<group position={[x + 2, 2, 4]}>
<Text position={[0, 1.5, 0]} fontSize={0.3} color="#81c784" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{topLoadings.map((l, i) => {
const barH = Math.abs(l.loading) * 3;
const barColor = l.loading > 0 ? '#81c784' : '#ef5350';
return (
<group key={l.variable}>
<mesh position={[(i - topLoadings.length / 2) * 0.8, barH / 2, 0]}>
<boxGeometry args={[0.5, barH, 0.3]} />
<meshStandardMaterial color={barColor} emissive={barColor} emissiveIntensity={0.3} transparent opacity={0.8} />
</mesh>
<Text
position={[(i - topLoadings.length / 2) * 0.8, -0.3, 0]}
fontSize={0.18} color="#8899aa" anchorX="center" anchorY="top"
font={CN_FONT} maxWidth={1}
>
{l.variable.length > 4 ? l.variable.slice(0, 3) + '..' : l.variable}
</Text>
</group>
);
})}
</group>
)}
{/* 公式卡片 */}
<group position={[x, 18, 0]}>
<mesh>
<boxGeometry args={[10, 1.8, 0.15]} />
<meshStandardMaterial color="#81c784" emissive="#81c784" emissiveIntensity={0.08} transparent opacity={0.7} />
</mesh>
<Text position={[0, 0.3, 0.1]} fontSize={0.35} color="#81c784" anchorX="center" anchorY="middle">
Z = (x-mean)/std PCA(PC1) ×10+50
</Text>
<Text position={[0, -0.3, 0.1]} fontSize={0.28} color="#8899aa" anchorX="center" anchorY="middle" font={CN_FONT}>
{nSchools}
</Text>
</group>
{/* 底部平台 */}
<mesh position={[x, 0.15, 0]}>
<cylinderGeometry args={[7, 7, 0.3, 32]} />
<meshStandardMaterial color="#81c784" transparent opacity={0.15} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
@@ -0,0 +1,109 @@
/**
* Stage 0: 原始数据
* B表和C表作为两个立体文件夹,小数据块散落其中
*/
import { useMemo } from 'react';
import { Text } from '@react-three/drei';
import * as THREE from 'three';
import type { RawDataStage as RawDataStageType } from '../types';
import { STAGE_POSITIONS, CN_FONT } from '../types';
interface Props {
data: RawDataStageType;
visible: boolean;
}
function DataBlock({ position, color }: { position: [number, number, number]; color: string }) {
return (
<mesh position={position}>
<boxGeometry args={[0.3, 0.3, 0.3]} />
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.5} />
</mesh>
);
}
export default function RawDataStage({ data, visible }: Props) {
const x = STAGE_POSITIONS[0];
const blocks = useMemo(() => {
const items: Array<{ pos: [number, number, number]; color: string }> = [];
// B表数据块
const bCount = Math.min(Math.floor(data.b_table.field_count / 15), 10);
for (let i = 0; i < bCount; i++) {
items.push({
pos: [x + (Math.random() - 0.5) * 3, 1 + i * 0.6, -5 + (Math.random() - 0.5) * 2],
color: '#4fc3f7',
});
}
// C表数据块
const cCount = Math.min(Math.floor(data.c_table.field_count / 80), 12);
for (let i = 0; i < cCount; i++) {
items.push({
pos: [x + (Math.random() - 0.5) * 3, 1 + i * 0.6, 5 + (Math.random() - 0.5) * 2],
color: '#29b6f6',
});
}
return items;
}, [data, x]);
const tables = useMemo(() => [
{ name: 'B表', subtitle: '课程实施', fields: data.b_table.field_count, z: -5, color: '#4fc3f7' },
{ name: 'C表', subtitle: '学科课程', fields: data.c_table.field_count, z: 5, color: '#29b6f6' },
], [data]);
return (
<group visible={visible}>
{/* 阶段标签 */}
<Text position={[x, 22, 0]} fontSize={1.6} color="#4fc3f7" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{tables.map((t) => (
<group key={t.name}>
{/* 文件夹主体 */}
<mesh
position={[x, 5, t.z]}
castShadow
userData={{ tooltip: `${t.name} ${t.subtitle}\n共 ${t.fields} 个字段`, type: 'raw-table' }}
>
<boxGeometry args={[4, 8, 3]} />
<meshStandardMaterial
color={t.color}
emissive={t.color}
emissiveIntensity={0.2}
metalness={0.3}
roughness={0.5}
transparent
opacity={0.92}
/>
</mesh>
{/* 边框 */}
<lineSegments position={[x, 5, t.z]}>
<edgesGeometry args={[new THREE.BoxGeometry(4, 8, 3)]} />
<lineBasicMaterial color={t.color} transparent opacity={0.6} />
</lineSegments>
{/* 标签 */}
<Text position={[x, 10, t.z]} fontSize={0.7} color={t.color} anchorX="center" anchorY="middle" font={CN_FONT}>
{t.name}
</Text>
<Text position={[x, 9.2, t.z]} fontSize={0.5} color="#aabbcc" anchorX="center" anchorY="middle" font={CN_FONT}>
{t.subtitle} · {t.fields}
</Text>
</group>
))}
{/* 散落的数据块 */}
{blocks.map((b, i) => (
<DataBlock key={i} position={b.pos} color={b.color} />
))}
{/* 底部平台 */}
<mesh position={[x, 0.15, 0]}>
<cylinderGeometry args={[7, 7, 0.3, 32]} />
<meshStandardMaterial color="#4fc3f7" transparent opacity={0.15} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
@@ -0,0 +1,167 @@
/**
* Stage 1: 赋分规则
*
* 视觉设计:
* 中间:齿轮形赋分引擎(ExtrudeGeometry,带旋转动画)
* 左侧:B表(学校级)子维度卡片组 — 蓝色调
* 右侧:C表(学科级)子维度卡片组 — 绿色调
* 这样一眼能看出两种数据来源的区别
*/
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { Text } from '@react-three/drei';
import * as THREE from 'three';
import type { ScoringInfo } from '../types';
import { STAGE_POSITIONS, CN_FONT } from '../types';
interface Props {
scoring: Record<string, ScoringInfo>;
visible: boolean;
}
/** 真实齿轮形状 */
function GearMesh({ position }: { position: [number, number, number] }) {
const ref = useRef<THREE.Mesh>(null!);
const geometry = useMemo(() => {
const shape = new THREE.Shape();
const teeth = 12;
const outerR = 3.5;
const innerR = 2.5;
for (let i = 0; i < teeth; i++) {
const a1 = (i / teeth) * Math.PI * 2;
const a2 = ((i + 0.3) / teeth) * Math.PI * 2;
const a3 = ((i + 0.5) / teeth) * Math.PI * 2;
const a4 = ((i + 0.8) / teeth) * Math.PI * 2;
if (i === 0) shape.moveTo(Math.cos(a1) * innerR, Math.sin(a1) * innerR);
shape.lineTo(Math.cos(a2) * outerR, Math.sin(a2) * outerR);
shape.lineTo(Math.cos(a3) * outerR, Math.sin(a3) * outerR);
shape.lineTo(Math.cos(a4) * innerR, Math.sin(a4) * innerR);
}
shape.closePath();
return new THREE.ExtrudeGeometry(shape, {
depth: 1.2, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.08,
});
}, []);
useFrame(({ clock }) => {
if (ref.current) ref.current.rotation.z = clock.elapsedTime * 0.3;
});
return (
<mesh ref={ref} geometry={geometry} position={position} rotation={[-Math.PI / 2, 0, 0]}
userData={{ tooltip: '赋分引擎\n20个赋分函数\n量表题·多选题·填空题·二值题', type: 'scoring-engine' }}
>
<meshStandardMaterial color="#ffd54f" emissive="#ffd54f" emissiveIntensity={0.25}
metalness={0.3} roughness={0.5} transparent opacity={0.92} />
</mesh>
);
}
/** 规则卡片 */
function RuleCard({ position, name, method, score, color }:
{ position: [number, number, number]; name: string; method: string; score: number | null; color: string }) {
return (
<group position={position}>
<mesh castShadow
userData={{ tooltip: `${name}\n${method}\n得分: ${score?.toFixed(1) ?? '-'}`, type: 'scoring-rule' }}
>
<boxGeometry args={[4.5, 1.6, 0.12]} />
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.1}
metalness={0.1} roughness={0.8} transparent opacity={0.85} />
</mesh>
<Text position={[0, 0.3, 0.08]} fontSize={0.35} color={color}
anchorX="center" anchorY="middle" maxWidth={4} font={CN_FONT}>
{name}
</Text>
<Text position={[1.5, -0.35, 0.08]} fontSize={0.3} color="#ffffff"
anchorX="center" anchorY="middle">
{score?.toFixed(1) ?? '-'}
</Text>
</group>
);
}
export default function ScoringStage({ scoring, visible }: Props) {
const x = STAGE_POSITIONS[1];
// 按数据来源分组
const { bTable, cTable } = useMemo(() => {
const b: Array<[string, ScoringInfo]> = [];
const c: Array<[string, ScoringInfo]> = [];
for (const [name, info] of Object.entries(scoring)) {
if (info.data_source?.includes('B表') || info.method?.includes('学校级')) {
b.push([name, info]);
} else {
c.push([name, info]);
}
}
// 如果分组失败(全在一边),平分
if (b.length === 0 && c.length > 0) {
const half = Math.ceil(c.length / 2);
return { bTable: c.slice(0, half), cTable: c.slice(half) };
}
return { bTable: b, cTable: c };
}, [scoring]);
return (
<group visible={visible}>
{/* 阶段标题 */}
<Text position={[x, 22, 0]} fontSize={1.6} color="#ffd54f" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{/* 中间齿轮 — 下移到底盘上方,距底盘约5个单位 */}
<GearMesh position={[x, 5, 0]} />
<Text position={[x, 7, 1]} fontSize={0.5} color="#ffd54f" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{/* 左侧 B表(学校级)卡片 */}
<Text position={[x - 5, 19, 0]} fontSize={0.5} color="#4fc3f7" anchorX="center" anchorY="middle" font={CN_FONT}>
B表 ·
</Text>
{bTable.slice(0, 5).map(([name, info], i) => (
<RuleCard
key={name}
position={[x - 5, 17 - i * 2, 0]}
name={name}
method={info.method?.slice(0, 40) ?? ''}
score={info.final_score}
color="#4fc3f7"
/>
))}
{/* 右侧 C表(学科级)卡片 */}
<Text position={[x + 5, 19, 0]} fontSize={0.5} color="#81c784" anchorX="center" anchorY="middle" font={CN_FONT}>
C表 ·
</Text>
{cTable.slice(0, 5).map(([name, info], i) => (
<RuleCard
key={name}
position={[x + 5, 17 - i * 2, 0]}
name={name}
method={info.method?.slice(0, 40) ?? ''}
score={info.final_score}
color="#81c784"
/>
))}
{/* 赋分类型图例 */}
<group position={[x, 3, 4]}>
{['量表 1-4分', '分档 0/1/2', '二值 有/无', '连续值'].map((label, i) => (
<Text key={label} position={[(i - 1.5) * 3, 0, 0]} fontSize={0.3} color="#8899aa"
anchorX="center" anchorY="middle" font={CN_FONT}>
{label}
</Text>
))}
</group>
{/* 底部平台 */}
<mesh position={[x, 0.15, 0]}>
<cylinderGeometry args={[8, 8, 0.3, 32]} />
<meshStandardMaterial color="#ffd54f" transparent opacity={0.15} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
@@ -0,0 +1,179 @@
/**
* Stage 3: 得分分布对比
*
* 视觉设计(替代原来21条小曲线):
* 一条大的正态分布管(TubeGeometry,和demo一样)
* 该校标记球(红色,带分数标注)
* 区均值线(虚线)
* 底部:按维度分组的得分柱状图(7组,每组2-4个子维度)
*/
import { useMemo } from 'react';
import { Text, Line } from '@react-three/drei';
import * as THREE from 'three';
import type { StandardizedInfo } from '../types';
import { STAGE_POSITIONS, DIM_COLORS, CN_FONT } from '../types';
interface Props {
standardized: Record<string, StandardizedInfo>;
visible: boolean;
}
export default function StandardStage({ standardized, visible }: Props) {
const x = STAGE_POSITIONS[3];
const entries = useMemo(() => Object.entries(standardized), [standardized]);
// 按维度分组
const dimGrouped = useMemo(() => {
const groups: Record<string, Array<{ name: string; data: StandardizedInfo }>> = {};
for (const [name, data] of entries) {
const dim = data.parent_dimension;
if (!groups[dim]) groups[dim] = [];
groups[dim].push({ name, data });
}
return Object.entries(groups);
}, [entries]);
// 计算总体均值和该校总体
const stats = useMemo(() => {
const scores = entries.map(([, d]) => d.score);
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
const distAvgs = entries.map(([, d]) => d.district_avg);
const distAvg = distAvgs.reduce((a, b) => a + b, 0) / distAvgs.length;
return { schoolAvg: avg, districtAvg: distAvg };
}, [entries]);
// 大正态分布曲线点
const curvePoints = useMemo(() => {
const pts: [number, number, number][] = [];
for (let i = -3; i <= 3; i += 0.1) {
const yv = Math.exp(-i * i / 2) / Math.sqrt(2 * Math.PI);
pts.push([x + i * 2.5, 10 + yv * 18, 0]);
}
return pts;
}, [x]);
// 该校在分布上的X位置
const schoolOffset = useMemo(() => {
const zScore = (stats.schoolAvg - 50) / 10;
return Math.max(-3, Math.min(3, zScore)) * 2.5;
}, [stats]);
// 区均值在分布上的X位置
const distOffset = useMemo(() => {
const zScore = (stats.districtAvg - 50) / 10;
return Math.max(-3, Math.min(3, zScore)) * 2.5;
}, [stats]);
return (
<group visible={visible}>
{/* 阶段标题 */}
<Text position={[x, 22, 0]} fontSize={1.6} color="#ba68c8" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{/* ===== 大正态分布曲线 ===== */}
<Line points={curvePoints} color="#ba68c8" lineWidth={3} transparent opacity={0.8} />
{/* 均值=50 虚线 */}
<Line
points={[[x, 9.5, 0], [x, 18, 0]]}
color="#ba68c8" lineWidth={1} transparent opacity={0.3}
dashed dashSize={0.3} gapSize={0.2}
/>
<Text position={[x, 18.5, 0]} fontSize={0.3} color="#ba68c8" anchorX="center" anchorY="middle">
μ=50
</Text>
{/* 该校标记球 */}
<mesh position={[x + schoolOffset, 10, 0]}
userData={{
tooltip: `该校平均标准化分: ${stats.schoolAvg.toFixed(1)}\n偏离均值: ${(stats.schoolAvg - 50).toFixed(1)}`,
type: 'school-marker',
}}
>
<sphereGeometry args={[0.6, 16, 16]} />
<meshStandardMaterial color="#e53935" emissive="#e53935" emissiveIntensity={0.6} />
</mesh>
<Text position={[x + schoolOffset, 9, 0]} fontSize={0.4} color="#e53935" anchorX="center" anchorY="middle">
{stats.schoolAvg.toFixed(1)}
</Text>
<Text position={[x + schoolOffset, 8.3, 0]} fontSize={0.25} color="#ff8a80" anchorX="center" anchorY="middle" font={CN_FONT}>
</Text>
{/* 区均值标记 */}
<Line
points={[[x + distOffset, 9.5, 0], [x + distOffset, 15, 0]]}
color="#ffd54f" lineWidth={1.5} transparent opacity={0.6}
dashed dashSize={0.4} gapSize={0.2}
/>
<Text position={[x + distOffset, 15.5, 0]} fontSize={0.25} color="#ffd54f" anchorX="center" anchorY="middle" font={CN_FONT}>
{stats.districtAvg.toFixed(1)}
</Text>
{/* ===== 底部:按维度分组的得分对比柱(圆盘中心,正态曲线后方) ===== */}
{dimGrouped.map(([dim, items], di) => {
const dimColor = DIM_COLORS[dim] || '#ba68c8';
const groupX = x - 6 + di * 2; // 左右展开对齐7个维度
const baseY = 1.5; // 圆盘上方
return (
<group key={dim}>
{/* 维度名 — 在柱子下方 */}
<Text
position={[groupX, baseY - 0.3, 0]}
fontSize={0.22} color={dimColor}
anchorX="center" anchorY="middle"
font={CN_FONT} maxWidth={2}
>
{dim.slice(0, 4)}
</Text>
{/* 子维度得分柱 — 沿z负方向排列(曲线后方),不遮挡正面文字 */}
{items.map((item, si) => {
const barH = Math.max(0.3, (item.data.score - 30) / 6);
const z = -(1 + si * 0.8); // z从-1开始往后排
const diffColor = item.data.diff_district > 0 ? '#81c784' : '#ef5350';
return (
<group key={item.name}>
<mesh
position={[groupX, baseY + barH / 2, z]}
userData={{
tooltip: `${item.name}\n得分: ${item.data.score.toFixed(1)}\n区均: ${item.data.district_avg.toFixed(1)}\n差值: ${item.data.diff_district > 0 ? '+' : ''}${item.data.diff_district.toFixed(1)}`,
type: 'sub-dim-bar',
}}
>
<boxGeometry args={[0.5, barH, 0.5]} />
<meshStandardMaterial
color={diffColor}
emissive={diffColor}
emissiveIntensity={0.2}
transparent opacity={0.8}
/>
</mesh>
</group>
);
})}
</group>
);
})}
{/* 公式说明 */}
<group position={[x, 19.5, 0]}>
<mesh>
<boxGeometry args={[8, 1.2, 0.15]} />
<meshStandardMaterial color="#ba68c8" emissive="#ba68c8" emissiveIntensity={0.08} transparent opacity={0.7} />
</mesh>
<Text position={[0, 0, 0.1]} fontSize={0.35} color="#ba68c8" anchorX="center" anchorY="middle" font={CN_FONT}>
{entries.length > 0 ? entries[0][1].city_avg ? '266' : '' : ''}
</Text>
</group>
{/* 底部平台 */}
<mesh position={[x, 0.15, 0]}>
<cylinderGeometry args={[7, 7, 0.3, 32]} />
<meshStandardMaterial color="#ba68c8" transparent opacity={0.15} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
+182
View File
@@ -0,0 +1,182 @@
/**
* 数据溯源可视化的类型定义
* 对应后端 TraceEngine.compute_trace() 的输出结构
*/
// ====== 阶段0:原始数据 ======
export interface RawField {
name: string;
value: string | number | null;
type: 'number' | 'binary' | 'ordinal' | 'text' | 'null';
subject?: string;
}
export interface RawDataStage {
b_table: {
field_count: number;
sample_fields: RawField[];
};
c_table: {
field_count: number;
subject_count: number;
subjects: Array<{ subject: string; field_count: number }>;
sample_fields: RawField[];
};
total_fields: number;
}
// ====== 阶段1:赋分 ======
export interface ScoringInput {
name: string;
raw: number;
standard?: number;
score: number;
rule: string;
subject?: string;
}
export interface SubFactor {
name: string;
type: 'PCA' | 'Z-score' | string;
input_count?: number;
raw?: number;
threshold?: number;
met?: boolean;
score?: number;
description?: string;
}
export interface ScoringInfo {
method: string;
data_source?: string;
inputs?: ScoringInput[];
sub_factors?: SubFactor[];
sample_inputs?: Array<{ name: string; value: string | number | null; subject?: string }>;
subject_count?: number;
final_score: number | null;
parent_dimension: string;
error?: string;
}
// ====== 阶段2PCA细节 ======
export interface LoadingDetail {
variable: string;
loading: number;
}
export interface PCADetail {
label?: string;
type: 'pca' | 'single_variable_z' | 'subject_level' | 'school_level' | 'multi_factor' | string;
n_schools?: number;
n_variables?: number;
explained_variance_ratio?: number;
loadings?: LoadingDetail[];
school_pca_score?: number | null;
school_standardized?: number | null;
pca_mean?: number;
pca_std?: number;
pipeline?: string;
factor_count?: number;
subject_count?: number;
note?: string;
error?: string;
}
// ====== 阶段3:标准化 ======
export interface StandardizedInfo {
score: number;
district_avg: number;
city_avg: number;
city_std: number;
diff_district: number;
diff_city: number;
parent_dimension: string;
}
// ====== 阶段4:水平判定 ======
export interface LevelInfo {
score: number;
thresholds: {
level2: number;
level3: number;
level4: number;
};
level: number;
description: string;
parent_dimension: string;
}
// ====== 阶段5:维度聚合 ======
export interface DimensionInfo {
sub_scores: Record<string, number>;
score: number | null;
method: string;
district_avg: number | null;
}
export interface OverallInfo {
score: number | null;
method: string;
district_avg: number | null;
rank: number | null;
total_schools: number;
}
// ====== 完整Trace数据 ======
export interface TraceData {
school: string;
school_info: Record<string, string>;
district_school_count: number;
city_school_count: number;
stages: {
raw_data: RawDataStage;
scoring: Record<string, ScoringInfo>;
pca_detail: Record<string, PCADetail>;
standardized: Record<string, StandardizedInfo>;
levels: Record<string, LevelInfo>;
dimensions: Record<string, DimensionInfo>;
overall: OverallInfo;
};
all_schools_overall: Record<string, number>;
}
// ====== 阶段枚举 ======
export const STAGE_NAMES = [
'原始数据',
'赋分规则',
'PCA降维',
'全市标准化',
'水平判定',
'维度聚合',
] as const;
export type StageIndex = 0 | 1 | 2 | 3 | 4 | 5;
// ====== 颜色常量 ======
export const STAGE_COLORS = {
raw: '#4fc3f7',
scoring: '#ffd54f',
pca: '#81c784',
standard: '#ba68c8',
level: '#ff8a65',
dimension: '#ff8a65',
total: '#e53935',
particle: '#4fc3f7',
bg: '#0a0e17',
} as const;
export const DIM_COLORS: Record<string, string> = {
'课程领导力': '#4fc3f7',
'教学变革力': '#81c784',
'学生发展指导力': '#ffd54f',
'教师发展支持力': '#ba68c8',
'教育质量评估力': '#ff8a65',
'教育条件保障力': '#26c6da',
'数字化赋能力': '#ef5350',
};
// 阶段X坐标位置
export const STAGE_POSITIONS = [-35, -21, -7, 7, 21, 35] as const;
// 中文字体路径(drei <Text> 底层 troika 需要完整 TTF/OTF/WOFF2
export const CN_FONT = '/fonts/noto-sans-sc.ttf';
@@ -0,0 +1,180 @@
/**
* InfoPanel — 左侧信息面板
* 根据当前阶段显示不同的说明信息
*/
import type { StageIndex, TraceData } from '../types';
interface Props {
currentStep: StageIndex;
data: TraceData;
}
const STEP_COLORS = ['#4fc3f7', '#ffd54f', '#81c784', '#ba68c8', '#ff8a65', '#e53935'];
function SectionTitle({ text, color }: { text: string; color: string }) {
return (
<div style={{ fontSize: 16, fontWeight: 600, color, marginBottom: 6 }}>
{text}
</div>
);
}
function InfoRow({ label, value, color }: { label: string; value: string | number; color?: string }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', margin: '3px 0', fontSize: 12 }}>
<span style={{ color: '#8899aa' }}>{label}</span>
<span style={{ color: color || '#ffffff', fontWeight: 500 }}>{value}</span>
</div>
);
}
export default function InfoPanel({ currentStep, data }: Props) {
const color = STEP_COLORS[currentStep];
const renderContent = () => {
switch (currentStep) {
case 0: {
const raw = data.stages.raw_data;
return (
<>
<SectionTitle text="原始数据" color={color} />
<InfoRow label="B表字段数" value={raw.b_table.field_count} />
<InfoRow label="C表字段数" value={raw.c_table.field_count} />
<InfoRow label="涵盖学科" value={`${raw.c_table.subject_count}`} />
<InfoRow label="总字段数" value={raw.total_fields} />
<div style={{ marginTop: 10, fontSize: 11, color: '#667788', lineHeight: 1.6 }}>
B表<br />
C表
</div>
</>
);
}
case 1: {
const scoring = data.stages.scoring;
const subDims = Object.keys(scoring);
return (
<>
<SectionTitle text="赋分规则" color={color} />
<InfoRow label="子维度总数" value={subDims.length} />
<InfoRow label="赋分方式" value="Likert/分档/二值" />
<div style={{ marginTop: 8 }}>
{subDims.slice(0, 8).map((name) => (
<div key={name} style={{ fontSize: 11, color: '#8899aa', margin: '2px 0', display: 'flex', justifyContent: 'space-between' }}>
<span>{name}</span>
<span style={{ color: '#ffffff' }}>{scoring[name].final_score?.toFixed(1) ?? '-'}</span>
</div>
))}
{subDims.length > 8 && (
<div style={{ fontSize: 11, color: '#556677', marginTop: 4 }}>...{subDims.length}</div>
)}
</div>
</>
);
}
case 2: {
const pca = data.stages.pca_detail;
const pcaEntries = Object.entries(pca);
const hasPCA = pcaEntries.find(([, d]) => d.type === 'pca');
return (
<>
<SectionTitle text="PCA降维" color={color} />
<InfoRow label="全市学校数" value={data.city_school_count} />
{hasPCA && (
<>
<InfoRow label="示例子维度" value={hasPCA[0]} />
<InfoRow label="输入变量数" value={hasPCA[1].n_variables ?? '-'} />
<InfoRow label="解释方差" value={`${((hasPCA[1].explained_variance_ratio ?? 0) * 100).toFixed(0)}%`} color="#81c784" />
<InfoRow label="该校PCA得分" value={hasPCA[1].school_standardized?.toFixed(1) ?? '-'} color="#e53935" />
</>
)}
<div style={{ marginTop: 10, fontSize: 11, color: '#667788', lineHeight: 1.6 }}>
Z标准化 PCA提取第一主成分 线5010
</div>
</>
);
}
case 3: {
const std = data.stages.standardized;
const entries = Object.entries(std).slice(0, 6);
return (
<>
<SectionTitle text="全市标准化" color={color} />
<div style={{ fontSize: 11, color: '#667788', marginBottom: 8, lineHeight: 1.6 }}>
PCA变换后5010
</div>
{entries.map(([name, d]) => (
<div key={name} style={{ fontSize: 11, margin: '2px 0', display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#8899aa' }}>{name}</span>
<span>
<span style={{ color: d.diff_district > 0 ? '#81c784' : '#ef5350' }}>
{d.diff_district > 0 ? '+' : ''}{d.diff_district.toFixed(1)}
</span>
<span style={{ color: '#556677', marginLeft: 4 }}>({d.score.toFixed(1)})</span>
</span>
</div>
))}
</>
);
}
case 4: {
const lvls = data.stages.levels;
const counts = { 1: 0, 2: 0, 3: 0, 4: 0 };
for (const d of Object.values(lvls)) {
const lv = Math.max(1, Math.min(4, d.level)) as 1 | 2 | 3 | 4;
counts[lv]++;
}
return (
<>
<SectionTitle text="水平判定" color={color} />
<InfoRow label="水平4(优秀)" value={`${counts[4]}`} color="#81c784" />
<InfoRow label="水平3(良好)" value={`${counts[3]}`} color="#ffd54f" />
<InfoRow label="水平2(合格)" value={`${counts[2]}`} color="#ff9800" />
<InfoRow label="水平1(尚需努力)" value={`${counts[1]}`} color="#ef5350" />
<div style={{ marginTop: 10, fontSize: 11, color: '#667788', lineHeight: 1.6 }}>
4
</div>
</>
);
}
case 5: {
const dims = data.stages.dimensions;
const overall = data.stages.overall;
return (
<>
<SectionTitle text="维度聚合" color={color} />
{Object.entries(dims).map(([name, d]) => (
<InfoRow
key={name}
label={name}
value={d.score?.toFixed(1) ?? '-'}
color={d.score != null && d.district_avg != null && d.score > d.district_avg ? '#81c784' : '#ef5350'}
/>
))}
<div style={{ height: 1, background: 'rgba(255,255,255,0.1)', margin: '8px 0' }} />
<InfoRow label="总体得分" value={overall.score?.toFixed(1) ?? '-'} color="#e53935" />
<InfoRow label="区内排名" value={`${overall.rank ?? '-'}名 / ${overall.total_schools}`} />
</>
);
}
}
};
return (
<div style={{
position: 'absolute',
top: 80,
left: 16,
width: 260,
background: 'rgba(10,14,23,0.88)',
borderRadius: 12,
padding: 16,
backdropFilter: 'blur(10px)',
border: '1px solid rgba(79,195,247,0.2)',
zIndex: 100,
maxHeight: '70vh',
overflowY: 'auto',
}}>
{renderContent()}
</div>
);
}
@@ -0,0 +1,107 @@
/**
* StepBar — 底部步骤控制条
*/
import React from 'react';
import type { StageIndex } from '../types';
import { STAGE_NAMES } from '../types';
interface Props {
currentStep: StageIndex;
onStepChange: (step: StageIndex) => void;
autoPlaying: boolean;
onToggleAutoPlay: () => void;
onReset: () => void;
onNext: () => void;
onPrev: () => void;
}
const STEP_COLORS = ['#4fc3f7', '#ffd54f', '#81c784', '#ba68c8', '#ff8a65', '#e53935'];
export default function StepBar({
currentStep, onStepChange, autoPlaying, onToggleAutoPlay, onReset, onNext, onPrev,
}: Props) {
return (
<div style={{
position: 'absolute',
bottom: 20,
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
alignItems: 'center',
gap: 6,
background: 'rgba(10,14,23,0.85)',
borderRadius: 16,
padding: '10px 18px',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(79,195,247,0.2)',
zIndex: 100,
}}>
{/* 控制按钮 */}
<button onClick={onReset} style={btnStyle} title="重置(R)"></button>
<button onClick={onPrev} style={btnStyle} title="上一步(←)"></button>
<button onClick={onToggleAutoPlay} style={{
...btnStyle,
background: autoPlaying ? 'rgba(79,195,247,0.3)' : undefined,
}} title="自动播放">
{autoPlaying ? '⏸' : '▶'}
</button>
<button onClick={onNext} style={btnStyle} title="下一步(→)"></button>
{/* 分隔线 */}
<div style={{ width: 1, height: 28, background: 'rgba(79,195,247,0.2)', margin: '0 6px' }} />
{/* 步骤点 */}
{STAGE_NAMES.map((name, i) => (
<div
key={name}
onClick={() => onStepChange(i as StageIndex)}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
cursor: 'pointer',
padding: '4px 8px',
borderRadius: 8,
background: currentStep === i ? 'rgba(79,195,247,0.15)' : 'transparent',
transition: 'all 0.3s',
}}
>
{/* 圆点 */}
<div style={{
width: 14,
height: 14,
borderRadius: '50%',
background: i <= currentStep ? STEP_COLORS[i] : 'rgba(255,255,255,0.1)',
border: currentStep === i ? `2px solid ${STEP_COLORS[i]}` : '2px solid transparent',
boxShadow: currentStep === i ? `0 0 8px ${STEP_COLORS[i]}80` : 'none',
transition: 'all 0.3s',
}} />
{/* 标签 */}
<span style={{
fontSize: 10,
color: i <= currentStep ? STEP_COLORS[i] : '#556677',
marginTop: 4,
whiteSpace: 'nowrap',
transition: 'color 0.3s',
}}>
{name}
</span>
</div>
))}
</div>
);
}
const btnStyle: React.CSSProperties = {
width: 36,
height: 36,
borderRadius: 8,
border: '1px solid rgba(79,195,247,0.2)',
background: 'transparent',
color: '#4fc3f7',
fontSize: 18,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
@@ -0,0 +1,35 @@
/**
* Tooltip — 悬浮提示框(屏幕坐标定位)
*/
import type { TooltipData } from '../Interaction';
interface Props {
data: TooltipData;
}
export default function Tooltip({ data }: Props) {
if (!data.visible || !data.text) return null;
return (
<div style={{
position: 'fixed',
left: data.x,
top: data.y,
maxWidth: 280,
padding: '8px 12px',
background: 'rgba(10,14,23,0.92)',
border: '1px solid rgba(79,195,247,0.35)',
borderRadius: 8,
color: '#e0e0e0',
fontSize: 12,
lineHeight: 1.6,
pointerEvents: 'none',
zIndex: 200,
backdropFilter: 'blur(8px)',
whiteSpace: 'pre-line',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
}}>
{data.text}
</div>
);
}
+107
View File
@@ -0,0 +1,107 @@
import { useState } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Layout, Menu, Typography, Button, Dropdown, Select } from 'antd';
import {
GlobalOutlined,
HistoryOutlined,
BarChartOutlined,
UserOutlined,
LogoutOutlined,
TranslationOutlined,
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../App';
const { Header, Sider, Content } = Layout;
const { Title } = Typography;
export default function AppLayout() {
const navigate = useNavigate();
const location = useLocation();
const [collapsed, setCollapsed] = useState(false);
const { username, logout } = useAuth();
const { t, i18n } = useTranslation();
const handleLogout = async () => {
await logout();
navigate('/login', { replace: true });
};
const handleLangChange = (lang: string) => {
i18n.changeLanguage(lang);
};
const menuItems = [
{ key: '/', icon: <GlobalOutlined />, label: t('layout.menu_districts') },
{ key: '/history', icon: <HistoryOutlined />, label: t('layout.menu_history') },
];
const getSelectedKey = () => {
const path = location.pathname;
if (path === '/history') return '/history';
return '/';
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={setCollapsed}
theme="light" style={{ boxShadow: '2px 0 8px rgba(0,0,0,0.06)' }}>
<div style={{
height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderBottom: '1px solid #f0f0f0',
}}>
<BarChartOutlined style={{ fontSize: 24, color: '#1677ff' }} />
{!collapsed && (
<Title level={5} style={{ margin: '0 0 0 8px', whiteSpace: 'nowrap' }}>
{i18n.language === 'en' ? 'Reports' : '监测报告'}
</Title>
)}
</div>
<Menu mode="inline" selectedKeys={[getSelectedKey()]} items={menuItems}
onClick={({ key }) => navigate(key)} style={{ borderRight: 0 }} />
</Sider>
<Layout>
<Header style={{
background: '#fff', padding: '0 24px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)', zIndex: 1,
}}>
<Title level={4} style={{ margin: 0 }}>
{t('layout.title')}
</Title>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Select
size="small"
value={i18n.language.startsWith('en') ? 'en' : 'zh'}
onChange={handleLangChange}
style={{ width: 110 }}
suffixIcon={<TranslationOutlined />}
options={[
{ value: 'zh', label: '🇨🇳 中文' },
{ value: 'en', label: '🇬🇧 English' },
]}
/>
<Dropdown menu={{
items: [
{
key: 'logout',
icon: <LogoutOutlined />,
label: t('layout.logout'),
onClick: handleLogout,
},
],
}} placement="bottomRight">
<Button type="text" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<UserOutlined />
<span>{username || 'admin'}</span>
</Button>
</Dropdown>
</div>
</Header>
<Content style={{ margin: 16, minHeight: 280 }}>
<Outlet />
</Content>
</Layout>
</Layout>
);
}
+33
View File
@@ -0,0 +1,33 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import zh from './locales/zh.json';
import en from './locales/en.json';
export const SUPPORTED_LANGS = ['zh', 'en'] as const;
export type AdminLang = typeof SUPPORTED_LANGS[number];
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'zh',
supportedLngs: SUPPORTED_LANGS,
defaultNS: 'translation',
interpolation: {
escapeValue: false,
},
detection: {
// 优先 localStorage,其次浏览器,最后 fallback
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
lookupLocalStorage: 'admin_lang',
},
resources: {
zh: { translation: zh },
en: { translation: en },
},
});
export default i18n;
+128
View File
@@ -0,0 +1,128 @@
{
"common": {
"loading": "Loading...",
"verifying_login": "Verifying login...",
"back": "Back",
"back_to_overview": "Back to overview",
"preview": "Preview",
"download": "Download",
"generate": "Generate",
"regenerate": "Regenerate",
"schools_unit": "schools",
"of": "of",
"school": "School",
"district": "District",
"type": "Type",
"score": "Score",
"rank": "Rank",
"city_rank": "Municipal Rank",
"district_rank": "District Rank",
"cluster": "Cluster",
"report": "Report",
"actions": "Actions",
"generated": "Generated",
"not_generated": "Not generated",
"language": "Language",
"lang_zh": "中文",
"lang_en": "English",
"lang_both": "Bilingual",
"open_new_window": "New window",
"print": "Print",
"data_trace": "Data trace",
"city_overview": "Citywide overview",
"skip_llm_label": "Skip LLM narrative (charts only):",
"select_all": "Select all",
"clear_selection": "Clear"
},
"layout": {
"title": "Shanghai Senior Secondary Curriculum Implementation Monitoring · Report Admin",
"menu_districts": "Districts",
"menu_history": "History",
"logout": "Logout"
},
"auth": {
"login_title": "Sign in",
"login_subtitle": "Shanghai Senior Secondary Curriculum Implementation Monitoring · Report Admin",
"username": "Username",
"password": "Password",
"login_button": "Sign in",
"username_placeholder": "Enter your username",
"password_placeholder": "Enter your password",
"login_failed": "Login failed: invalid username or password",
"login_success": "Signed in"
},
"districts": {
"title": "Citywide District Overview",
"subtitle": "16 districts · {{n}} senior secondary schools — curriculum implementation monitoring",
"school_count": "Schools",
"avg_score": "District avg.",
"report_count": "Reports generated",
"report_count_zh_en": "ZH {{zh}} · EN {{en}}",
"view_schools": "View schools",
"fetch_failed": "Failed to fetch district data"
},
"schools": {
"title": "{{district}} · Senior Secondary Curriculum Implementation Monitoring",
"fetch_failed": "Failed to fetch school list",
"stats_school_count": "Schools",
"stats_district_avg": "District average",
"stats_reports": "Reports generated",
"batch_generate": "Batch generate",
"batch_running": "Generating...",
"generation_card_text": "Generating report for {{school}}",
"generation_card_lang_both": "Bilingual · current: {{currentLangLabel}}",
"generation_segments": "{{progress}}/{{total}} segments",
"batch_card_text": "Batch generating: {{school}}",
"batch_progress_text": "{{completed}}/{{total}}",
"preview_zh": "Preview · ZH",
"preview_en": "Preview · EN",
"single_dropdown_zh": "Regenerate Chinese",
"single_dropdown_zh_new": "Generate Chinese",
"single_dropdown_en": "Regenerate English",
"single_dropdown_en_new": "Generate English",
"single_dropdown_both": "Generate bilingual",
"batch_modal_title": "Batch generate · {{district}}",
"batch_modal_ok": "Start ({{n}} schools)",
"batch_modal_lang": "Language:",
"single_started_failed": "Failed to start generation task",
"single_finished": "{{school}} {{langLabel}} report generated. Score: {{score}}",
"single_failed": "Generation failed: {{error}}",
"batch_started_failed": "Failed to start batch generation",
"batch_finished": "Batch generation done: {{ok}}/{{total}}"
},
"preview": {
"report_title": "Curriculum Implementation Monitoring Report",
"loading": "Loading report...",
"load_failed": "Failed to load the report. Has the requested language version been generated?"
},
"history": {
"title": "Report History",
"search_placeholder": "Search school or district...",
"header_school": "School",
"header_district": "District",
"header_type": "Type",
"header_score": "Score",
"header_size": "Size",
"header_generated_at": "Generated at",
"header_lang": "Language",
"header_actions": "Actions",
"fetch_failed": "Failed to fetch report history"
},
"trace": {
"title": "Data Trace",
"back": "Back to report"
},
"chat": {
"title": "AI Report Assistant",
"clear": "Clear conversation",
"close": "Close",
"input_placeholder": "Type your question...",
"request_failed_prefix": "⚠️ Request failed: ",
"welcome_l1": "Hello! I have full access to the curriculum-implementation monitoring data for {{school}}.",
"welcome_l2": "Ask any question directly, or pick a quick prompt below to begin.",
"quick_overall": "How does the school perform overall?",
"quick_gap": "What are the most significant gaps?",
"quick_urgent": "Which improvements are most urgent?",
"quick_compare": "How does it compare with peer-type schools?"
}
}
+128
View File
@@ -0,0 +1,128 @@
{
"common": {
"loading": "加载中...",
"verifying_login": "验证登录状态...",
"back": "返回",
"back_to_overview": "返回全市",
"preview": "预览",
"download": "下载",
"generate": "生成",
"regenerate": "重新生成",
"schools_unit": "所",
"of": "/",
"school": "学校",
"district": "区",
"type": "类型",
"score": "总分",
"rank": "排名",
"city_rank": "全市排名",
"district_rank": "区内排名",
"cluster": "聚类",
"report": "报告",
"actions": "操作",
"generated": "已生成",
"not_generated": "未生成",
"language": "语言",
"lang_zh": "中文",
"lang_en": "English",
"lang_both": "中英双语",
"open_new_window": "新窗口",
"print": "打印",
"data_trace": "数据溯源",
"city_overview": "全市概览",
"skip_llm_label": "跳过LLM文字(仅图表):",
"select_all": "全选",
"clear_selection": "清空"
},
"layout": {
"title": "上海高中课程实施监测 · 报告管理系统",
"menu_districts": "区域概览",
"menu_history": "历史记录",
"logout": "退出登录"
},
"auth": {
"login_title": "登录",
"login_subtitle": "上海高中课程实施监测 · 报告管理系统",
"username": "用户名",
"password": "密码",
"login_button": "登 录",
"username_placeholder": "请输入用户名",
"password_placeholder": "请输入密码",
"login_failed": "登录失败:用户名或密码错误",
"login_success": "登录成功"
},
"districts": {
"title": "全市区域概览",
"subtitle": "上海市16个区·{{n}}所高中课程实施监测",
"school_count": "学校数",
"avg_score": "区均分",
"report_count": "已生成报告",
"report_count_zh_en": "中文 {{zh}} · 英文 {{en}}",
"view_schools": "查看学校",
"fetch_failed": "获取区域数据失败"
},
"schools": {
"title": "{{district}} · 高中课程实施监测",
"fetch_failed": "获取学校列表失败",
"stats_school_count": "学校数",
"stats_district_avg": "区内均分",
"stats_reports": "已生成报告",
"batch_generate": "批量生成",
"batch_running": "生成中...",
"generation_card_text": "正在生成 {{school}} 的报告",
"generation_card_lang_both": "中英双语 · 当前{{currentLangLabel}}",
"generation_segments": "{{progress}}/{{total}} 段",
"batch_card_text": "批量生成中: {{school}}",
"batch_progress_text": "{{completed}}/{{total}}",
"preview_zh": "预览·中",
"preview_en": "预览·EN",
"single_dropdown_zh": "重新生成中文",
"single_dropdown_zh_new": "生成中文",
"single_dropdown_en": "重新生成英文",
"single_dropdown_en_new": "生成英文",
"single_dropdown_both": "生成中英双语",
"batch_modal_title": "批量生成 · {{district}}",
"batch_modal_ok": "开始生成 ({{n}}所)",
"batch_modal_lang": "语言:",
"single_started_failed": "启动生成任务失败",
"single_finished": "{{school}} {{langLabel}}报告生成完成!得分: {{score}}分",
"single_failed": "生成失败: {{error}}",
"batch_started_failed": "启动批量生成失败",
"batch_finished": "批量生成完成: {{ok}}/{{total}}"
},
"preview": {
"report_title": "课程实施监测报告",
"loading": "加载报告中...",
"load_failed": "报告加载失败,可能尚未生成对应语言的版本"
},
"history": {
"title": "报告历史",
"search_placeholder": "搜索学校或区...",
"header_school": "学校",
"header_district": "区",
"header_type": "类型",
"header_score": "得分",
"header_size": "大小",
"header_generated_at": "生成时间",
"header_lang": "语言",
"header_actions": "操作",
"fetch_failed": "获取历史记录失败"
},
"trace": {
"title": "数据溯源",
"back": "返回报告"
},
"chat": {
"title": "AI 报告助手",
"clear": "清空对话",
"close": "关闭",
"input_placeholder": "输入问题...",
"request_failed_prefix": "⚠️ 请求失败: ",
"welcome_l1": "您好!我已了解 {{school}} 课程实施监测报告的全部数据。",
"welcome_l2": "您可以直接提问,或点击下方快捷问题开始。",
"quick_overall": "总体表现如何?",
"quick_gap": "最大的短板是什么?",
"quick_urgent": "最紧迫应该改进什么?",
"quick_compare": "与同类学校对比如何?"
}
}
+24
View File
@@ -0,0 +1,24 @@
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f5f5f5;
}
/* 报告 iframe 全屏样式 */
.report-iframe {
width: 100%;
border: none;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import './i18n'; // 初始化 i18next(必须在任何使用 t() 的组件之前)
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+240
View File
@@ -0,0 +1,240 @@
import { useState, useRef, useEffect } from 'react';
import {
Card, Input, Button, Select, Space, Typography, Spin, message,
Tag, Divider,
} from 'antd';
import {
SendOutlined, RobotOutlined, UserOutlined, ClearOutlined,
BulbOutlined,
} from '@ant-design/icons';
import { getDistricts, getSchoolsInDistrict, sendChatMessage } from '../services/era2Api';
const { Text, Paragraph } = Typography;
const { TextArea } = Input;
interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}
const QUICK_PROMPTS = [
'分析这所学校的优势和劣势',
'给出课程领导力的改进建议',
'对比该校与同类学校的差异',
'总结七大维度的表现特征',
'分析教师发展支持力偏低的原因',
];
export default function Assistant() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
// 上下文选择
const [districts, setDistricts] = useState<string[]>([]);
const [selectedDistrict, setSelectedDistrict] = useState<string | undefined>();
const [schoolOptions, setSchoolOptions] = useState<string[]>([]);
const [selectedSchool, setSelectedSchool] = useState<string | undefined>();
const chatEndRef = useRef<HTMLDivElement>(null);
// 加载区列表
useEffect(() => {
getDistricts().then(res => {
setDistricts(res.data.districts.map(d => d.district));
}).catch(() => {});
}, []);
// 区变化时加载学校
useEffect(() => {
if (!selectedDistrict) {
setSchoolOptions([]);
setSelectedSchool(undefined);
return;
}
getSchoolsInDistrict(selectedDistrict).then(res => {
setSchoolOptions(res.data.schools.map(s => s.name));
}).catch(() => {});
}, [selectedDistrict]);
// 自动滚动
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSend = async (text?: string) => {
const msg = text || input.trim();
if (!msg) return;
const userMsg: ChatMessage = { role: 'user', content: msg };
setMessages(prev => [...prev, userMsg]);
setInput('');
setLoading(true);
// 构建历史
const history = messages.map(m => ({ role: m.role, content: m.content }));
try {
const response = await sendChatMessage(msg, selectedSchool, selectedDistrict, history);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No reader');
const decoder = new TextDecoder();
let assistantContent = '';
// 添加空的assistant消息
setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
if (parsed.content) {
assistantContent += parsed.content;
setMessages(prev => {
const newMsgs = [...prev];
newMsgs[newMsgs.length - 1] = { role: 'assistant', content: assistantContent };
return newMsgs;
});
}
if (parsed.error) {
message.error(`LLM错误: ${parsed.error}`);
}
} catch {
// skip parse errors
}
}
}
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
message.error(`请求失败: ${errorMessage}`);
setMessages(prev => [...prev, {
role: 'assistant',
content: '抱歉,请求处理失败。请检查网络连接和后端服务状态。',
}]);
} finally {
setLoading(false);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 130px)' }}>
{/* 上下文选择 */}
<Card size="small" style={{ marginBottom: 12 }}>
<Space wrap>
<Text strong><BulbOutlined /> :</Text>
<Select placeholder="选择区" value={selectedDistrict} onChange={v => { setSelectedDistrict(v); setSelectedSchool(undefined); }}
allowClear style={{ width: 130 }} showSearch filterOption={(input, option) =>
(option?.children as unknown as string)?.includes(input) || false}>
{districts.map(d => <Select.Option key={d} value={d}>{d}</Select.Option>)}
</Select>
<Select placeholder="选择学校" value={selectedSchool} onChange={setSelectedSchool}
allowClear disabled={!selectedDistrict} style={{ width: 180 }}
showSearch filterOption={(input, option) =>
(option?.children as unknown as string)?.includes(input) || false}>
{schoolOptions.map(s => <Select.Option key={s} value={s}>{s}</Select.Option>)}
</Select>
{selectedSchool && selectedDistrict && (
<Tag color="blue">{selectedDistrict} · {selectedSchool}</Tag>
)}
{messages.length > 0 && (
<Button size="small" icon={<ClearOutlined />} onClick={() => setMessages([])}>
</Button>
)}
</Space>
</Card>
{/* 对话区域 */}
<Card style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
styles={{ body: { flex: 1, overflow: 'auto', padding: '16px' } }}>
{messages.length === 0 ? (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<RobotOutlined style={{ fontSize: 48, color: '#1677ff', marginBottom: 16 }} />
<Paragraph type="secondary" style={{ fontSize: 16 }}>
</Paragraph>
<Paragraph type="secondary">
</Paragraph>
<Divider></Divider>
<Space wrap style={{ justifyContent: 'center' }}>
{QUICK_PROMPTS.map(p => (
<Button key={p} size="small" onClick={() => handleSend(p)}
style={{ borderRadius: 16, marginBottom: 4 }}>
{p}
</Button>
))}
</Space>
</div>
) : (
<div>
{messages.map((msg, idx) => (
<div key={idx} style={{
display: 'flex', gap: 12, marginBottom: 16,
flexDirection: msg.role === 'user' ? 'row-reverse' : 'row',
}}>
<div style={{
width: 36, height: 36, borderRadius: '50%', flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: msg.role === 'user' ? '#1677ff' : '#f0f5ff',
color: msg.role === 'user' ? '#fff' : '#1677ff',
}}>
{msg.role === 'user' ? <UserOutlined /> : <RobotOutlined />}
</div>
<div style={{
maxWidth: '75%', padding: '10px 16px', borderRadius: 12,
background: msg.role === 'user' ? '#1677ff' : '#f5f5f5',
color: msg.role === 'user' ? '#fff' : '#333',
whiteSpace: 'pre-wrap', lineHeight: 1.6,
}}>
{msg.content || (loading && idx === messages.length - 1 ? (
<Spin size="small" />
) : '')}
</div>
</div>
))}
<div ref={chatEndRef} />
</div>
)}
</Card>
{/* 输入区 */}
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
<TextArea
value={input}
onChange={e => setInput(e.target.value)}
placeholder="输入问题... (Shift+Enter换行)"
autoSize={{ minRows: 1, maxRows: 4 }}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
disabled={loading}
style={{ flex: 1, borderRadius: 8 }}
/>
<Button type="primary" icon={<SendOutlined />} onClick={() => handleSend()}
loading={loading} style={{ height: 'auto', borderRadius: 8 }}>
</Button>
</div>
</div>
);
}
+290
View File
@@ -0,0 +1,290 @@
import { useEffect, useState, useCallback } from 'react';
import {
Card, Button, Space, Table, Tag, Progress, Switch, message,
Typography, Alert, Row, Col, Statistic, Divider, Checkbox,
} from 'antd';
import {
ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined,
LoadingOutlined, ClockCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import {
getSchools, batchGenerate, getBatchStatus,
type SchoolSummary, type BatchStatus,
} from '../services/api';
const { Text } = Typography;
export default function BatchGenerate() {
const [schools, setSchools] = useState<SchoolSummary[]>([]);
const [selectedSchools, setSelectedSchools] = useState<string[]>([]);
const [, setLoading] = useState(true);
const [useCache, setUseCache] = useState(true);
const [skipLlm, setSkipLlm] = useState(false);
// 批量生成状态
const [batchId, setBatchId] = useState<string | null>(null);
const [batchStatus, setBatchStatus] = useState<BatchStatus | null>(null);
const [polling, setPolling] = useState(false);
const fetchSchools = useCallback(async () => {
setLoading(true);
try {
const res = await getSchools();
setSchools(res.data.schools);
// 默认全选
setSelectedSchools(res.data.schools.map(s => s.name));
} catch {
message.error('获取学校列表失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchSchools();
}, [fetchSchools]);
// 轮询批量状态
useEffect(() => {
if (!batchId || !polling) return;
const timer = setInterval(async () => {
try {
const res = await getBatchStatus(batchId);
setBatchStatus(res.data);
if (res.data.status === 'completed' || res.data.status === 'failed') {
setPolling(false);
clearInterval(timer);
if (res.data.status === 'completed') {
const success = res.data.results.filter(r => r.status === 'success').length;
message.success(`批量生成完成!成功 ${success}/${res.data.total} 所学校`);
fetchSchools();
}
}
} catch {
// ignore
}
}, 2000);
return () => clearInterval(timer);
}, [batchId, polling, fetchSchools]);
const handleBatchGenerate = async () => {
try {
const schoolsToGen = selectedSchools.length === schools.length
? undefined // 全部
: selectedSchools;
const res = await batchGenerate(schoolsToGen, useCache, skipLlm);
setBatchId(res.data.batch_id);
setBatchStatus({
status: 'running',
schools: res.data.schools,
total: res.data.total,
completed: 0,
results: [],
});
setPolling(true);
message.info(`开始批量生成 ${res.data.total} 所学校的报告...`);
} catch (err) {
message.error('启动批量生成失败');
}
};
const isRunning = batchStatus?.status === 'running';
const resultColumns: ColumnsType<BatchStatus['results'][0]> = [
{
title: '学校',
dataIndex: 'school',
key: 'school',
render: (name: string) => <Text strong>{name}</Text>,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => status === 'success'
? <Tag icon={<CheckCircleOutlined />} color="success"></Tag>
: <Tag icon={<CloseCircleOutlined />} color="error"></Tag>,
},
{
title: '总分',
dataIndex: 'score',
key: 'score',
width: 80,
render: (score?: number) => score != null ? score.toFixed(2) : '-',
},
{
title: '排名',
dataIndex: 'rank',
key: 'rank',
width: 80,
render: (rank?: number) => rank != null ? `${rank}/9` : '-',
},
{
title: '聚类',
dataIndex: 'cluster',
key: 'cluster',
width: 100,
render: (cluster?: string) => cluster
? <Tag color={cluster === '较好' ? 'success' : 'warning'}>{cluster}</Tag>
: '-',
},
{
title: '耗时',
dataIndex: 'time',
key: 'time',
width: 80,
render: (t?: number) => t != null ? `${t}s` : '-',
},
];
return (
<div>
{/* 控制面板 */}
<Card title="批量生成配置" bordered={false} style={{ marginBottom: 16 }}>
<Row gutter={24} align="middle">
<Col>
<Space>
<Text>使</Text>
<Switch checked={useCache} onChange={setUseCache} disabled={isRunning} />
</Space>
</Col>
<Col>
<Space>
<Text>LLM</Text>
<Switch checked={skipLlm} onChange={setSkipLlm} disabled={isRunning} />
</Space>
</Col>
<Col flex="auto" style={{ textAlign: 'right' }}>
<Button
type="primary"
size="large"
icon={isRunning ? <LoadingOutlined /> : <ThunderboltOutlined />}
loading={isRunning}
onClick={handleBatchGenerate}
disabled={isRunning || selectedSchools.length === 0}
>
{isRunning ? '生成中...' : `批量生成 (${selectedSchools.length} 所学校)`}
</Button>
</Col>
</Row>
{/* 学校选择 */}
<Divider style={{ margin: '16px 0' }} />
<Checkbox.Group
value={selectedSchools}
onChange={(v) => setSelectedSchools(v as string[])}
disabled={isRunning}
>
<Row gutter={[16, 8]}>
{schools.map(s => (
<Col span={8} key={s.name}>
<Checkbox value={s.name}>
{s.name}
<Text type="secondary" style={{ marginLeft: 4, fontSize: 12 }}>
({s.type})
</Text>
</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
<div style={{ marginTop: 8 }}>
<Button
size="small"
type="link"
onClick={() => setSelectedSchools(schools.map(s => s.name))}
disabled={isRunning}
>
</Button>
<Button
size="small"
type="link"
onClick={() => setSelectedSchools([])}
disabled={isRunning}
>
</Button>
</div>
</Card>
{/* 生成进度 */}
{batchStatus && (
<Card title="生成进度" bordered={false} style={{ marginBottom: 16 }}>
<Row gutter={24} style={{ marginBottom: 16 }}>
<Col span={6}>
<Statistic
title="总学校数"
value={batchStatus.total}
suffix="所"
/>
</Col>
<Col span={6}>
<Statistic
title="已完成"
value={batchStatus.completed}
suffix={`/ ${batchStatus.total}`}
valueStyle={{ color: '#1677ff' }}
/>
</Col>
<Col span={6}>
<Statistic
title="状态"
value={
batchStatus.status === 'running' ? '生成中' :
batchStatus.status === 'completed' ? '已完成' : '失败'
}
valueStyle={{
color: batchStatus.status === 'completed' ? '#52c41a' :
batchStatus.status === 'running' ? '#1677ff' : '#ff4d4f',
}}
/>
</Col>
<Col span={6}>
{batchStatus.elapsed && (
<Statistic
title="总耗时"
value={batchStatus.elapsed}
suffix="秒"
prefix={<ClockCircleOutlined />}
/>
)}
</Col>
</Row>
<Progress
percent={Math.round((batchStatus.completed / batchStatus.total) * 100)}
status={batchStatus.status === 'completed' ? 'success' :
batchStatus.status === 'running' ? 'active' : 'exception'}
strokeWidth={12}
/>
{batchStatus.current_school && batchStatus.status === 'running' && (
<Alert
type="info"
showIcon
icon={<LoadingOutlined />}
message={`正在生成: ${batchStatus.current_school}`}
style={{ marginTop: 12 }}
/>
)}
</Card>
)}
{/* 生成结果 */}
{batchStatus?.results && batchStatus.results.length > 0 && (
<Card title="生成结果" bordered={false}>
<Table
columns={resultColumns}
dataSource={batchStatus.results}
rowKey="school"
pagination={false}
size="small"
/>
</Card>
)}
</div>
);
}
+285
View File
@@ -0,0 +1,285 @@
import { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card, Table, Tag, Button, Space, Statistic, Row, Col, message, Progress, Typography,
} from 'antd';
import {
EyeOutlined, DownloadOutlined, SyncOutlined,
TrophyOutlined, TeamOutlined, FileTextOutlined, CheckCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import {
getSchools, generateReport, getTaskStatus,
getReportDownloadUrl,
type SchoolSummary,
} from '../services/api';
const { Text } = Typography;
export default function Dashboard() {
const navigate = useNavigate();
const [schools, setSchools] = useState<SchoolSummary[]>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState<string | null>(null);
const [genProgress, setGenProgress] = useState(0);
const [genTotal, setGenTotal] = useState(29);
const fetchSchools = useCallback(async () => {
setLoading(true);
try {
const res = await getSchools();
setSchools(res.data.schools);
} catch (err) {
message.error('获取学校列表失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchSchools();
}, [fetchSchools]);
const handleGenerate = async (school: string, useCache: boolean) => {
try {
setGenerating(school);
setGenProgress(0);
const res = await generateReport(school, useCache);
const taskId = res.data.task_id;
// 轮询状态
const poll = setInterval(async () => {
try {
const status = await getTaskStatus(taskId);
setGenProgress(status.data.progress);
setGenTotal(status.data.total);
if (status.data.status === 'completed') {
clearInterval(poll);
setGenerating(null);
message.success(`${school} 报告生成完成!得分: ${status.data.score}`);
fetchSchools();
} else if (status.data.status === 'failed') {
clearInterval(poll);
setGenerating(null);
message.error(`${school} 报告生成失败: ${status.data.error}`);
}
} catch {
clearInterval(poll);
setGenerating(null);
}
}, 1000);
} catch (err) {
setGenerating(null);
message.error('启动生成任务失败');
}
};
const columns: ColumnsType<SchoolSummary> = [
{
title: '排名',
dataIndex: 'rank',
key: 'rank',
width: 70,
align: 'center',
render: (rank: number) => (
<span style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 28, height: 28, borderRadius: '50%',
background: rank <= 3 ? '#1677ff' : '#f0f0f0',
color: rank <= 3 ? '#fff' : '#666',
fontWeight: 600, fontSize: 13,
}}>
{rank}
</span>
),
},
{
title: '学校名称',
dataIndex: 'name',
key: 'name',
render: (name: string) => <Text strong>{name}</Text>,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
render: (type: string) => {
const colorMap: Record<string, string> = {
'市实验性示范性高中': 'blue',
'区实验性示范性高中': 'cyan',
'特色高中': 'purple',
'公办普通高中': 'green',
'民办高中': 'orange',
};
return <Tag color={colorMap[type] || 'default'}>{type}</Tag>;
},
},
{
title: '总分',
dataIndex: 'score',
key: 'score',
width: 100,
align: 'center',
sorter: (a, b) => a.score - b.score,
render: (score: number) => (
<Text strong style={{ fontSize: 16, color: score >= 50 ? '#52c41a' : '#faad14' }}>
{score}
</Text>
),
},
{
title: '聚类',
dataIndex: 'cluster',
key: 'cluster',
width: 100,
align: 'center',
render: (cluster: string) => (
<Tag color={cluster === '较好' ? 'success' : 'warning'}>{cluster}</Tag>
),
},
{
title: '报告状态',
dataIndex: 'has_report',
key: 'has_report',
width: 200,
align: 'center',
render: (has: boolean, record) => has ? (
<Space direction="vertical" size={0} style={{ lineHeight: 1.4 }}>
<Space size={4}>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<Text type="secondary" style={{ fontSize: 12 }}>
{(record.report_size / 1024).toFixed(0)}KB
</Text>
</Space>
{record.report_generated_at && (
<Text type="secondary" style={{ fontSize: 11 }}>
{record.report_generated_at}
</Text>
)}
</Space>
) : (
<Tag color="default"></Tag>
),
},
{
title: '操作',
key: 'actions',
width: 280,
render: (_, record) => (
<Space size="small">
{record.has_report && (
<>
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
onClick={() => navigate(`/report/${encodeURIComponent(record.name)}`)}
>
</Button>
<Button
size="small"
icon={<DownloadOutlined />}
href={getReportDownloadUrl(record.name)}
>
</Button>
</>
)}
<Button
size="small"
icon={<SyncOutlined spin={generating === record.name} />}
loading={generating === record.name}
onClick={() => handleGenerate(record.name, true)}
>
{record.has_report ? '重新生成' : '生成'}
</Button>
</Space>
),
},
];
// 统计数据
const totalSchools = schools.length;
const avgScore = totalSchools > 0
? (schools.reduce((s, r) => s + r.score, 0) / totalSchools).toFixed(1)
: '0';
const reportCount = schools.filter(s => s.has_report).length;
const goodCount = schools.filter(s => s.cluster === '较好').length;
return (
<div>
{/* 顶部统计卡片 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="学校总数"
value={totalSchools}
prefix={<TeamOutlined />}
suffix="所"
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="区内均分"
value={avgScore}
prefix={<TrophyOutlined />}
precision={1}
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="课程实施较好类"
value={goodCount}
suffix={`/ ${totalSchools}`}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="已生成报告"
value={reportCount}
prefix={<FileTextOutlined />}
suffix={`/ ${totalSchools}`}
valueStyle={{ color: reportCount === totalSchools ? '#52c41a' : '#faad14' }}
/>
</Card>
</Col>
</Row>
{/* 生成进度条 */}
{generating && (
<Card style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}>
<Text> <Text strong>{generating}</Text> ...</Text>
<Progress
percent={Math.round((genProgress / genTotal) * 100)}
status="active"
format={() => `${genProgress}/${genTotal}`}
/>
</Space>
</Card>
)}
{/* 学校列表 */}
<Card title="长宁区高中课程实施监测" bordered={false}>
<Table
columns={columns}
dataSource={schools}
rowKey="name"
loading={loading}
pagination={false}
size="middle"
/>
</Card>
</div>
);
}
+175
View File
@@ -0,0 +1,175 @@
import { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Row, Col, Statistic, Typography, Spin, message } from 'antd';
import {
BankOutlined, TeamOutlined, TrophyOutlined, FileTextOutlined,
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { getDistricts, type DistrictSummary } from '../services/era2Api';
const { Title, Text } = Typography;
// 区中文名 → 英文名(用于 EN 模式下显示)
const DISTRICT_EN: Record<string, string> = {
'黄浦区': 'Huangpu', '徐汇区': 'Xuhui', '长宁区': 'Changning',
'静安区': "Jing'an", '普陀区': 'Putuo', '虹口区': 'Hongkou',
'杨浦区': 'Yangpu', '浦东新区': 'Pudong', '闵行区': 'Minhang',
'宝山区': 'Baoshan', '嘉定区': 'Jiading', '金山区': 'Jinshan',
'松江区': 'Songjiang', '青浦区': 'Qingpu', '奉贤区': 'Fengxian',
'崇明区': 'Chongming',
};
// 区的颜色映射
const DISTRICT_COLORS: Record<string, string> = {
'黄浦区': '#f5222d', '徐汇区': '#fa541c', '长宁区': '#fa8c16',
'静安区': '#faad14', '普陀区': '#a0d911', '虹口区': '#52c41a',
'杨浦区': '#13c2c2', '浦东新区': '#1677ff', '闵行区': '#2f54eb',
'宝山区': '#722ed1', '嘉定区': '#eb2f96', '金山区': '#f759ab',
'松江区': '#597ef7', '青浦区': '#36cfc9', '奉贤区': '#95de64',
'崇明区': '#ffc53d',
};
export default function DistrictList() {
const navigate = useNavigate();
const { t, i18n } = useTranslation();
const isEn = i18n.language?.startsWith('en');
const [districts, setDistricts] = useState<DistrictSummary[]>([]);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
try {
const res = await getDistricts();
setDistricts(res.data.districts);
} catch {
message.error(t('districts.fetch_failed'));
} finally {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { fetchData(); }, [fetchData]);
const totalSchools = districts.reduce((s, d) => s + d.school_count, 0);
const totalReports = districts.reduce((s, d) => s + d.report_count, 0);
const overallAvg = districts.length > 0
? (districts.reduce((s, d) => s + d.avg_score * d.school_count, 0) / totalSchools).toFixed(1)
: '50.0';
const renderDistrictName = (d: string) =>
isEn ? (DISTRICT_EN[d] ? `${DISTRICT_EN[d]}` : d) : d;
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}>
<Spin size="large" tip={t('common.loading')} />
</div>
);
}
return (
<div>
{/* 全市统计 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title={isEn ? 'Districts' : '覆盖区县'}
value={districts.length}
prefix={<BankOutlined />}
suffix={isEn ? '' : '个'}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title={isEn ? 'Total schools' : '学校总数'}
value={totalSchools}
prefix={<TeamOutlined />}
suffix={isEn ? '' : '所'}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title={isEn ? 'Citywide average' : '全市均分'}
value={overallAvg}
prefix={<TrophyOutlined />}
valueStyle={{ color: '#1677ff' }}
precision={1}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title={t('districts.report_count')}
value={totalReports}
prefix={<FileTextOutlined />}
suffix={`/ ${totalSchools}`}
valueStyle={{ color: totalReports > 0 ? '#52c41a' : '#999' }}
/>
</Card>
</Col>
</Row>
{/* 标题 */}
<Title level={4} style={{ marginBottom: 16 }}>
{t('districts.title')}
</Title>
{/* 区卡片网格 */}
<Row gutter={[16, 16]}>
{districts.map(d => {
const color = DISTRICT_COLORS[d.district] || '#1677ff';
const reportPct = d.school_count > 0 ? Math.round((d.report_count / d.school_count) * 100) : 0;
return (
<Col xs={12} sm={8} md={6} key={d.district}>
<Card
hoverable
onClick={() => navigate(`/district/${encodeURIComponent(d.district)}`)}
style={{ borderTop: `3px solid ${color}`, height: '100%' }}
styles={{ body: { padding: '20px 16px' } }}
>
<div style={{ textAlign: 'center' }}>
<Title level={4} style={{ margin: 0, color }}>
{renderDistrictName(d.district)}
</Title>
<div style={{ margin: '16px 0 8px', display: 'flex', justifyContent: 'space-around' }}>
<div>
<div style={{ fontSize: 24, fontWeight: 700, color: '#333' }}>{d.school_count}</div>
<Text type="secondary" style={{ fontSize: 12 }}>{t('districts.school_count')}</Text>
</div>
<div>
<div style={{ fontSize: 24, fontWeight: 700, color: d.avg_score >= 50 ? '#52c41a' : '#faad14' }}>
{d.avg_score.toFixed(1)}
</div>
<Text type="secondary" style={{ fontSize: 12 }}>{t('districts.avg_score')}</Text>
</div>
</div>
<div style={{
marginTop: 8,
padding: '4px 12px',
borderRadius: 12,
background: reportPct === 100 ? '#f6ffed' : reportPct > 0 ? '#fffbe6' : '#f5f5f5',
display: 'inline-block',
}}>
<Text style={{
fontSize: 12,
color: reportPct === 100 ? '#52c41a' : reportPct > 0 ? '#faad14' : '#999',
}}>
<FileTextOutlined /> {d.report_count}/{d.school_count}{' '}
{isEn ? 'reports' : '报告'}
</Text>
</div>
</div>
</Card>
</Col>
);
})}
</Row>
</div>
);
}
+496
View File
@@ -0,0 +1,496 @@
import { useEffect, useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card, Table, Tag, Button, Space, Statistic, Row, Col, message,
Progress, Typography, Breadcrumb, Switch, Checkbox, Modal, Divider,
Dropdown, Radio,
} from 'antd';
import {
ArrowLeftOutlined, EyeOutlined, DownloadOutlined, SyncOutlined,
TrophyOutlined, TeamOutlined, FileTextOutlined, ThunderboltOutlined,
LoadingOutlined, GlobalOutlined, DownOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { MenuProps } from 'antd';
import { useTranslation } from 'react-i18next';
import {
getSchoolsInDistrict, generateReport, getTaskStatus,
batchGenerate, getBatchStatus,
getReportDownloadUrl,
type SchoolInfo, type BatchStatus, type ReportLang,
} from '../services/era2Api';
const { Text } = Typography;
export default function DistrictSchools() {
const { district } = useParams<{ district: string }>();
const navigate = useNavigate();
const { t, i18n } = useTranslation();
const isEn = i18n.language?.startsWith('en');
const districtName = decodeURIComponent(district || '');
const [schools, setSchools] = useState<SchoolInfo[]>([]);
const [loading, setLoading] = useState(true);
// 单校生成
const [generating, setGenerating] = useState<string | null>(null);
const [genProgress, setGenProgress] = useState(0);
const [genTotal, setGenTotal] = useState(29);
const [genLang, setGenLang] = useState<ReportLang>('zh');
const [genCurrentLang, setGenCurrentLang] = useState<string>('zh');
// 批量生成
const [batchModalOpen, setBatchModalOpen] = useState(false);
const [selectedSchools, setSelectedSchools] = useState<string[]>([]);
const [skipLlm, setSkipLlm] = useState(false);
const [batchLang, setBatchLang] = useState<ReportLang>('zh');
const [batchRunning, setBatchRunning] = useState(false);
const [batchStatus, setBatchStatus] = useState<BatchStatus | null>(null);
const fetchSchools = useCallback(async () => {
if (!districtName) return;
setLoading(true);
try {
const res = await getSchoolsInDistrict(districtName);
setSchools(res.data.schools);
} catch {
message.error(t('schools.fetch_failed'));
} finally {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [districtName]);
useEffect(() => { fetchSchools(); }, [fetchSchools]);
// 单校生成
const handleGenerate = async (
school: string,
useCache: boolean,
lang: ReportLang = 'zh',
) => {
try {
setGenerating(school);
setGenLang(lang);
setGenProgress(0);
setGenCurrentLang(lang === 'both' ? 'zh' : lang);
const res = await generateReport(school, districtName, { useCache, lang });
const taskId = res.data.task_id;
const poll = setInterval(async () => {
try {
const status = await getTaskStatus(taskId);
setGenProgress(status.data.progress);
setGenTotal(status.data.total);
if (status.data.current_lang) setGenCurrentLang(status.data.current_lang);
if (status.data.status === 'completed') {
clearInterval(poll);
setGenerating(null);
const langLabel =
lang === 'both' ? t('common.lang_both')
: lang === 'en' ? t('common.lang_en')
: t('common.lang_zh');
message.success(t('schools.single_finished', {
school, langLabel, score: status.data.score,
}));
fetchSchools();
} else if (status.data.status === 'failed') {
clearInterval(poll);
setGenerating(null);
message.error(t('schools.single_failed', { error: status.data.error || '' }));
}
} catch {
clearInterval(poll);
setGenerating(null);
}
}, 1000);
} catch {
setGenerating(null);
message.error(t('schools.single_started_failed'));
}
};
// 批量生成
const handleBatchGenerate = async () => {
try {
const toGen = selectedSchools.length === schools.length ? undefined : selectedSchools;
const res = await batchGenerate(districtName, toGen, { skipLlm, lang: batchLang });
setBatchRunning(true);
setBatchStatus({
status: 'running', district: districtName,
schools: selectedSchools, total: res.data.total,
completed: 0, results: [],
lang: batchLang,
});
setBatchModalOpen(false);
// 轮询
const poll = setInterval(async () => {
try {
const s = await getBatchStatus(res.data.batch_id);
setBatchStatus(s.data);
if (s.data.status === 'completed' || s.data.status === 'failed') {
clearInterval(poll);
setBatchRunning(false);
if (s.data.status === 'completed') {
const ok = s.data.results.filter(r => r.status === 'success').length;
message.success(t('schools.batch_finished', { ok, total: s.data.total }));
fetchSchools();
}
}
} catch {
clearInterval(poll);
setBatchRunning(false);
}
}, 2000);
} catch {
message.error(t('schools.batch_started_failed'));
}
};
// 打开批量Modal
const openBatchModal = () => {
setSelectedSchools(schools.map(s => s.name));
setBatchModalOpen(true);
};
// 学校类型 i18n 显示
const SCHOOL_TYPE_EN: Record<string, string> = {
'市实验性示范性高中': 'Municipal Demonstrative',
'区实验性示范性高中': 'District Demonstrative',
'特色高中': 'Featured',
'公办普通高中': 'Public General',
'民办高中': 'Private',
};
const CLUSTER_EN: Record<string, string> = {
'较好': 'High-Performing',
'中等': 'Mid-Tier',
'待提升': 'Needs Improvement',
};
const columns: ColumnsType<SchoolInfo> = [
{
title: t('common.district_rank'), dataIndex: 'district_rank', key: 'rank', width: 80, align: 'center',
render: (rank: number) => (
<span style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 28, height: 28, borderRadius: '50%',
background: rank <= 3 ? '#1677ff' : '#f0f0f0',
color: rank <= 3 ? '#fff' : '#666', fontWeight: 600, fontSize: 13,
}}>
{rank}
</span>
),
},
{
title: t('common.school'), dataIndex: 'name', key: 'name',
render: (name: string) => <Text strong>{name}</Text>,
},
{
title: t('common.type'), dataIndex: 'type', key: 'type', width: 180,
render: (type: string) => {
const colors: Record<string, string> = {
'市实验性示范性高中': 'blue', '区实验性示范性高中': 'cyan',
'特色高中': 'purple', '公办普通高中': 'green', '民办高中': 'orange',
};
const label = isEn ? (SCHOOL_TYPE_EN[type] || type) : type;
return <Tag color={colors[type] || 'default'}>{label}</Tag>;
},
},
{
title: t('common.score'), dataIndex: 'score', key: 'score', width: 90, align: 'center',
sorter: (a, b) => a.score - b.score,
render: (score: number) => (
<Text strong style={{ fontSize: 15, color: score >= 50 ? '#52c41a' : '#faad14' }}>
{score.toFixed(1)}
</Text>
),
},
{
title: t('common.city_rank'), dataIndex: 'city_rank', key: 'city_rank', width: 100, align: 'center',
render: (rank: number, record) => (
<Text type="secondary">{rank}/{record.total_in_city}</Text>
),
},
{
title: t('common.cluster'), dataIndex: 'cluster', key: 'cluster', width: 110, align: 'center',
render: (c: string) => {
const colors: Record<string, string> = { '较好': 'success', '中等': 'processing', '待提升': 'warning' };
const label = isEn ? (CLUSTER_EN[c] || c) : c;
return <Tag color={colors[c] || 'default'}>{label}</Tag>;
},
},
{
title: t('common.report'), key: 'report', width: 170, align: 'center',
render: (_, record) => {
const hasZh = record.has_report_zh ?? record.has_report;
const hasEn = record.has_report_en ?? false;
const at = record.report_generated_at_zh || record.report_generated_at_en || record.report_generated_at;
return (
<Space direction="vertical" size={2} style={{ lineHeight: 1.3 }}>
<Space size={4}>
<Tag color={hasZh ? 'success' : 'default'} style={{ margin: 0, minWidth: 38, textAlign: 'center' }}>
ZH{hasZh ? ' ✓' : ' ·'}
</Tag>
<Tag color={hasEn ? 'blue' : 'default'} style={{ margin: 0, minWidth: 38, textAlign: 'center' }}>
EN{hasEn ? ' ✓' : ' ·'}
</Tag>
</Space>
{at && <Text type="secondary" style={{ fontSize: 11 }}>{at}</Text>}
</Space>
);
},
},
{
title: t('common.actions'), key: 'actions', width: 360,
render: (_, record) => {
const hasZh = record.has_report_zh ?? record.has_report;
const hasEn = record.has_report_en ?? false;
// 三种语言选项 — 都用「图标 + 文字」让用户一眼看清
const reGenMenu: MenuProps['items'] = [
{
key: 'zh',
icon: <span style={{ fontSize: 14 }}>🇨🇳</span>,
label: hasZh ? t('schools.single_dropdown_zh') : t('schools.single_dropdown_zh_new'),
},
{
key: 'en',
icon: <span style={{ fontSize: 14 }}>🇬🇧</span>,
label: hasEn ? t('schools.single_dropdown_en') : t('schools.single_dropdown_en_new'),
},
{
key: 'both',
icon: <GlobalOutlined />,
label: t('schools.single_dropdown_both'),
},
];
return (
<Space size="small" wrap>
{hasZh && (
<>
<Button size="small" type="primary" icon={<EyeOutlined />}
onClick={() => navigate(`/district/${encodeURIComponent(districtName)}/report/${encodeURIComponent(record.name)}?lang=zh`)}>
{t('schools.preview_zh')}
</Button>
<Button size="small" icon={<DownloadOutlined />}
href={getReportDownloadUrl(districtName, record.name, 'zh')}>
ZH
</Button>
</>
)}
{hasEn && (
<>
<Button size="small" type="primary" ghost icon={<EyeOutlined />}
onClick={() => navigate(`/district/${encodeURIComponent(districtName)}/report/${encodeURIComponent(record.name)}?lang=en`)}>
{t('schools.preview_en')}
</Button>
<Button size="small" icon={<DownloadOutlined />}
href={getReportDownloadUrl(districtName, record.name, 'en')}>
EN
</Button>
</>
)}
{/* 主按钮点击即弹出语言菜单,用户必须选语言再开始生成 */}
<Dropdown
menu={{
items: reGenMenu,
onClick: (info) => handleGenerate(record.name, true, info.key as ReportLang),
}}
trigger={['click']}
disabled={generating === record.name}
>
<Button
size="small"
type="primary"
ghost
icon={
generating === record.name
? <SyncOutlined spin />
: <SyncOutlined />
}
loading={generating === record.name}
>
{hasZh ? t('common.regenerate') : t('common.generate')}
{generating !== record.name && (
<DownOutlined style={{ fontSize: 10, marginLeft: 4 }} />
)}
</Button>
</Dropdown>
</Space>
);
},
},
];
const totalSchools = schools.length;
const avgScore = totalSchools > 0
? (schools.reduce((s, r) => s + r.score, 0) / totalSchools).toFixed(1) : '0';
const reportCountZh = schools.filter(s => (s.has_report_zh ?? s.has_report)).length;
const reportCountEn = schools.filter(s => s.has_report_en).length;
return (
<div>
{/* 面包屑 */}
<Breadcrumb style={{ marginBottom: 16 }} items={[
{ title: <a onClick={() => navigate('/')}>{t('common.city_overview')}</a> },
{ title: districtName },
]} />
{/* 统计 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title={t('schools.stats_school_count')}
value={totalSchools}
prefix={<TeamOutlined />}
suffix={isEn ? '' : '所'}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title={t('schools.stats_district_avg')}
value={avgScore}
prefix={<TrophyOutlined />}
valueStyle={{ color: '#1677ff' }}
precision={1}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title={t('schools.stats_reports')}
value={reportCountZh}
prefix={<FileTextOutlined />}
suffix={`/ ${totalSchools}`}
valueStyle={{ color: reportCountZh === totalSchools ? '#52c41a' : '#faad14' }}
/>
<div style={{ marginTop: 4, fontSize: 12, color: '#999' }}>
{t('districts.report_count_zh_en', { zh: reportCountZh, en: reportCountEn })}
</div>
</Card>
</Col>
<Col span={6}>
<Card style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
<Button type="primary" icon={batchRunning ? <LoadingOutlined /> : <ThunderboltOutlined />}
onClick={openBatchModal} loading={batchRunning} disabled={batchRunning}
style={{ width: '100%', height: 50 }}>
{batchRunning ? t('schools.batch_running') : t('schools.batch_generate')}
</Button>
</Card>
</Col>
</Row>
{/* 生成进度 */}
{generating && (
<Card style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}>
<Text>
{t('schools.generation_card_text', { school: generating })}
{' '}
<Tag color={genLang === 'en' ? 'blue' : (genLang === 'both' ? 'purple' : 'green')}>
{genLang === 'both'
? t('schools.generation_card_lang_both', {
currentLangLabel: genCurrentLang === 'en' ? t('common.lang_en') : t('common.lang_zh'),
})
: genLang === 'en' ? t('common.lang_en') : t('common.lang_zh')}
</Tag>
...
</Text>
<Progress percent={Math.round((genProgress / Math.max(genTotal, 1)) * 100)} status="active"
format={() => t('schools.generation_segments', { progress: genProgress, total: genTotal })} />
</Space>
</Card>
)}
{/* 批量进度 */}
{batchStatus && batchStatus.status === 'running' && (
<Card style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}>
<Text>
{t('schools.batch_card_text', { school: batchStatus.current_school || '' })}
{batchStatus.lang && (
<Tag style={{ marginLeft: 8 }} color={batchStatus.lang === 'en' ? 'blue' : (batchStatus.lang === 'both' ? 'purple' : 'green')}>
{batchStatus.lang === 'both'
? t('schools.generation_card_lang_both', {
currentLangLabel: batchStatus.current_lang === 'en' ? t('common.lang_en') : t('common.lang_zh'),
})
: batchStatus.lang === 'en' ? t('common.lang_en') : t('common.lang_zh')}
</Tag>
)}
</Text>
<Progress percent={Math.round((batchStatus.completed / batchStatus.total) * 100)}
status="active" format={() => t('schools.batch_progress_text', { completed: batchStatus.completed, total: batchStatus.total })} />
</Space>
</Card>
)}
{/* 学校列表 */}
<Card
title={t('schools.title', { district: districtName })}
extra={
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/')}>
{t('common.back_to_overview')}
</Button>
}
bordered={false}
>
<Table columns={columns} dataSource={schools} rowKey="name"
loading={loading} pagination={false} size="middle" />
</Card>
{/* 批量生成Modal */}
<Modal
title={t('schools.batch_modal_title', { district: districtName })}
open={batchModalOpen}
onOk={handleBatchGenerate}
onCancel={() => setBatchModalOpen(false)}
okText={t('schools.batch_modal_ok', { n: selectedSchools.length })}
width={620}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Space>
<GlobalOutlined />
<Text>{t('schools.batch_modal_lang')}</Text>
<Radio.Group value={batchLang} onChange={(e) => setBatchLang(e.target.value)}>
<Radio.Button value="zh">{t('common.lang_zh')}</Radio.Button>
<Radio.Button value="en">{t('common.lang_en')}</Radio.Button>
<Radio.Button value="both">{t('common.lang_both')}</Radio.Button>
</Radio.Group>
</Space>
<Space>
<Text>{t('common.skip_llm_label')}</Text>
<Switch checked={skipLlm} onChange={setSkipLlm} />
</Space>
<Divider style={{ margin: '12px 0' }} />
<div>
<Space style={{ marginBottom: 8 }}>
<Button size="small" type="link"
onClick={() => setSelectedSchools(schools.map(s => s.name))}>{t('common.select_all')}</Button>
<Button size="small" type="link"
onClick={() => setSelectedSchools([])}>{t('common.clear_selection')}</Button>
</Space>
<Checkbox.Group value={selectedSchools}
onChange={v => setSelectedSchools(v as string[])}>
<Row gutter={[8, 4]}>
{schools.map(s => (
<Col span={12} key={s.name}>
<Checkbox value={s.name}>
{s.name}{' '}
<Text type="secondary" style={{ fontSize: 11 }}>
({isEn ? (SCHOOL_TYPE_EN[s.type] || s.type) : s.type})
</Text>
</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</div>
</Space>
</Modal>
</div>
);
}
+132
View File
@@ -0,0 +1,132 @@
import { useState, useRef, useMemo, useEffect } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { Card, Button, Space, Spin, Typography, message, Breadcrumb, Radio } from 'antd';
import {
ArrowLeftOutlined, DownloadOutlined, PrinterOutlined, ExpandOutlined,
NodeIndexOutlined, GlobalOutlined,
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { getReportPreviewUrl, getReportDownloadUrl } from '../services/era2Api';
import ChatFab from '../components/ChatFab';
const { Title } = Typography;
type Lang = 'zh' | 'en';
export default function Era2ReportPreview() {
const { district, school } = useParams<{ district: string; school: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const iframeRef = useRef<HTMLIFrameElement>(null);
const [loading, setLoading] = useState(true);
const [searchParams, setSearchParams] = useSearchParams();
const initialLang: Lang = (searchParams.get('lang') === 'en' ? 'en' : 'zh');
const [lang, setLang] = useState<Lang>(initialLang);
const districtName = decodeURIComponent(district || '');
const schoolName = decodeURIComponent(school || '');
const previewUrl = useMemo(
() => getReportPreviewUrl(districtName, schoolName, lang),
[districtName, schoolName, lang],
);
// 切换语言时重置 loading 并同步 URL 参数
useEffect(() => {
setLoading(true);
const next = new URLSearchParams(searchParams);
if (lang === 'en') next.set('lang', 'en'); else next.delete('lang');
setSearchParams(next, { replace: true });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lang]);
const reportTitleSuffix = lang === 'en'
? 'Curriculum Implementation Monitoring Report'
: '课程实施监测报告';
return (
<div>
<Breadcrumb style={{ marginBottom: 16 }} items={[
{ title: <a onClick={() => navigate('/')}>{t('common.city_overview')}</a> },
{ title: <a onClick={() => navigate(`/district/${encodeURIComponent(districtName)}`)}>{districtName}</a> },
{ title: `${schoolName} ${lang === 'en' ? 'Report' : '报告'}` },
]} />
<Card
title={
<Space>
<Button icon={<ArrowLeftOutlined />}
onClick={() => navigate(`/district/${encodeURIComponent(districtName)}`)} type="text" />
<Title level={5} style={{ margin: 0 }}>
{schoolName} {reportTitleSuffix}
</Title>
</Space>
}
extra={
<Space wrap>
<Space.Compact>
<Button
icon={<GlobalOutlined />}
disabled
style={{ pointerEvents: 'none', background: '#fafafa' }}
/>
<Radio.Group
value={lang}
onChange={(e) => setLang(e.target.value)}
buttonStyle="solid"
>
<Radio.Button value="zh">{t('common.lang_zh')}</Radio.Button>
<Radio.Button value="en">EN</Radio.Button>
</Radio.Group>
</Space.Compact>
<Button
icon={<NodeIndexOutlined />}
onClick={() => navigate(`/district/${encodeURIComponent(districtName)}/trace/${encodeURIComponent(schoolName)}`)}
style={{ color: '#1677ff' }}
>
{t('common.data_trace')}
</Button>
<Button icon={<ExpandOutlined />} onClick={() => window.open(previewUrl, '_blank')}>
{t('common.open_new_window')}
</Button>
<Button icon={<PrinterOutlined />} onClick={() => iframeRef.current?.contentWindow?.print()}>
{t('common.print')}
</Button>
<Button type="primary" icon={<DownloadOutlined />}
href={getReportDownloadUrl(districtName, schoolName, lang)}>
{t('common.download')}
</Button>
</Space>
}
styles={{ body: { padding: 0, height: 'calc(100vh - 200px)' } }}
bordered={false}
>
{loading && (
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: '#fff', zIndex: 1,
}}>
<Spin size="large" tip={t('preview.loading')} />
</div>
)}
<iframe
ref={iframeRef}
key={previewUrl}
src={previewUrl}
className="report-iframe"
style={{ width: '100%', height: '100%', border: 'none' }}
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
message.error(t('preview.load_failed'));
}}
/>
</Card>
{/* AI助理浮动按钮 — 自动绑定当前报告的区+学校+语言 */}
<ChatFab school={schoolName} district={districtName} lang={lang} />
</div>
);
}
+192
View File
@@ -0,0 +1,192 @@
import { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card, Table, Tag, Button, Space, Input, Select, Typography, message, Statistic, Row, Col,
} from 'antd';
import {
EyeOutlined, DownloadOutlined, SearchOutlined,
FileTextOutlined, ClockCircleOutlined, BankOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { useTranslation } from 'react-i18next';
import { getReportHistory, getReportDownloadUrl, type ReportHistoryItem } from '../services/era2Api';
const { Text } = Typography;
export default function History() {
const navigate = useNavigate();
const { t, i18n } = useTranslation();
const isEn = i18n.language?.startsWith('en');
const [reports, setReports] = useState<ReportHistoryItem[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [filterDistrict, setFilterDistrict] = useState<string | undefined>(undefined);
const [filterLang, setFilterLang] = useState<'zh' | 'en' | undefined>(undefined);
const fetchHistory = useCallback(async () => {
setLoading(true);
try {
const res = await getReportHistory();
setReports(res.data.reports);
} catch {
message.error(t('history.fetch_failed'));
} finally {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { fetchHistory(); }, [fetchHistory]);
const SCHOOL_TYPE_EN: Record<string, string> = {
'市实验性示范性高中': 'Municipal Demonstrative',
'区实验性示范性高中': 'District Demonstrative',
'特色高中': 'Featured',
'公办普通高中': 'Public General',
'民办高中': 'Private',
};
// 筛选
const districts = [...new Set(reports.map(r => r.district))].sort();
const filtered = reports.filter(r => {
if (search && !r.school.includes(search) && !r.district.includes(search)) return false;
if (filterDistrict && r.district !== filterDistrict) return false;
if (filterLang && r.lang !== filterLang) return false;
return true;
});
const columns: ColumnsType<ReportHistoryItem> = [
{
title: t('history.header_school'), dataIndex: 'school', key: 'school',
render: (name: string) => <Text strong>{name}</Text>,
sorter: (a, b) => a.school.localeCompare(b.school),
},
{
title: t('history.header_district'), dataIndex: 'district', key: 'district', width: 100,
render: (d: string) => <Tag color="blue">{d}</Tag>,
filters: districts.map(d => ({ text: d, value: d })),
onFilter: (value, record) => record.district === value,
},
{
title: t('history.header_type'), dataIndex: 'type', key: 'type', width: 180,
render: (type: string) => {
const colors: Record<string, string> = {
'市实验性示范性高中': 'blue', '区实验性示范性高中': 'cyan',
'特色高中': 'purple', '公办普通高中': 'green', '民办高中': 'orange',
};
const label = isEn ? (SCHOOL_TYPE_EN[type] || type) : type;
return <Tag color={colors[type] || 'default'}>{label}</Tag>;
},
},
{
title: t('history.header_lang'), dataIndex: 'lang', key: 'lang', width: 80, align: 'center',
render: (l?: string) => l === 'en'
? <Tag color="blue">EN</Tag>
: <Tag color="success">ZH</Tag>,
},
{
title: t('history.header_score'), dataIndex: 'score', key: 'score', width: 80, align: 'center',
sorter: (a, b) => (a.score ?? 0) - (b.score ?? 0),
render: (score: number | null) => score != null ? (
<Text strong style={{ color: score >= 50 ? '#52c41a' : '#faad14' }}>{score.toFixed(1)}</Text>
) : '-',
},
{
title: t('history.header_size'), dataIndex: 'file_size', key: 'size', width: 100, align: 'center',
render: (size: number) => `${(size / 1024).toFixed(0)} KB`,
},
{
title: t('history.header_generated_at'), dataIndex: 'generated_at', key: 'time', width: 180,
sorter: (a, b) => a.generated_at.localeCompare(b.generated_at),
defaultSortOrder: 'descend',
render: (text: string) => (
<Space size={4}>
<ClockCircleOutlined style={{ color: '#999' }} />
<Text type="secondary">{text}</Text>
</Space>
),
},
{
title: t('history.header_actions'), key: 'actions', width: 200,
render: (_, record) => (
<Space size="small">
<Button size="small" type="primary" icon={<EyeOutlined />}
onClick={() => navigate(
`/district/${encodeURIComponent(record.district)}/report/${encodeURIComponent(record.school)}?lang=${record.lang || 'zh'}`
)}>
{t('common.preview')}
</Button>
<Button size="small" icon={<DownloadOutlined />}
href={getReportDownloadUrl(record.district, record.school, (record.lang === 'en' ? 'en' : 'zh'))}>
{t('common.download')}
</Button>
</Space>
),
},
];
return (
<div>
{/* 统计 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={8}>
<Card>
<Statistic
title={isEn ? 'Total reports' : '报告总数'}
value={reports.length}
prefix={<FileTextOutlined />}
suffix={isEn ? '' : '份'}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title={isEn ? 'Districts covered' : '覆盖区'}
value={districts.length}
prefix={<BankOutlined />}
suffix={isEn ? '' : '个'}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title={isEn ? 'Total storage' : '总存储'}
prefix={<FileTextOutlined />}
value={`${(reports.reduce((s, r) => s + r.file_size, 0) / (1024 * 1024)).toFixed(1)} MB`}
/>
</Card>
</Col>
</Row>
<Card title={t('history.title')} bordered={false}
extra={
<Space>
<Input placeholder={t('history.search_placeholder')} prefix={<SearchOutlined />}
value={search} onChange={e => setSearch(e.target.value)}
style={{ width: 220 }} allowClear />
<Select placeholder={t('common.district')} value={filterDistrict}
onChange={setFilterDistrict} allowClear style={{ width: 130 }}>
{districts.map(d => <Select.Option key={d} value={d}>{d}</Select.Option>)}
</Select>
<Select placeholder={t('common.language')} value={filterLang}
onChange={setFilterLang} allowClear style={{ width: 110 }}
options={[
{ value: 'zh', label: t('common.lang_zh') },
{ value: 'en', label: t('common.lang_en') },
]}
/>
</Space>
}>
<Table columns={columns} dataSource={filtered} rowKey={r => `${r.district}_${r.school}_${r.lang || 'zh'}`}
loading={loading} pagination={{
pageSize: 20,
showSizeChanger: true,
showTotal: total => isEn ? `${total} entries` : `${total}`,
}}
size="middle" />
</Card>
</div>
);
}
+123
View File
@@ -0,0 +1,123 @@
/**
* 登录页面
* 简洁的居中卡片式登录表单
*/
import { useState } from 'react';
import { Form, Input, Button, Card, Typography, message, Space } from 'antd';
import { LockOutlined, UserOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { login } from '../services/auth';
const { Title, Text } = Typography;
export default function Login() {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { t } = useTranslation();
const handleLogin = async (values: { username: string; password: string }) => {
setLoading(true);
try {
await login(values.username, values.password);
message.success(t('auth.login_success'));
navigate('/', { replace: true });
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
|| t('auth.login_failed');
message.error(errorMsg);
} finally {
setLoading(false);
}
};
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #1e40af 100%)',
padding: 24,
}}>
<Card
style={{
width: 400,
maxWidth: '100%',
borderRadius: 16,
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
styles={{ body: { padding: '40px 36px 32px' } }}
>
<Space direction="vertical" size={24} style={{ width: '100%' }}>
{/* Logo 区域 */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: 64, height: 64, borderRadius: '50%',
background: 'linear-gradient(135deg, #1e3a5f, #2563eb)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
marginBottom: 12,
}}>
<SafetyCertificateOutlined style={{ fontSize: 32, color: '#fff' }} />
</div>
<Title level={3} style={{ margin: 0, color: '#1e293b' }}>
{t('auth.login_title')}
</Title>
<Text type="secondary" style={{ fontSize: 13 }}>
{t('auth.login_subtitle')}
</Text>
</div>
{/* 登录表单 */}
<Form
name="login"
onFinish={handleLogin}
size="large"
autoComplete="off"
>
<Form.Item
name="username"
rules={[{ required: true, message: t('auth.username_placeholder') }]}
>
<Input
prefix={<UserOutlined style={{ color: '#94a3b8' }} />}
placeholder={t('auth.username')}
autoFocus
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: t('auth.password_placeholder') }]}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#94a3b8' }} />}
placeholder={t('auth.password')}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
block
style={{
height: 44,
borderRadius: 8,
fontWeight: 600,
fontSize: 15,
background: 'linear-gradient(135deg, #1e3a5f, #2563eb)',
border: 'none',
}}
>
{t('auth.login_button')}
</Button>
</Form.Item>
</Form>
</Space>
</Card>
</div>
);
}
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Button, Space, Spin, Typography, message, Breadcrumb } from 'antd';
import {
ArrowLeftOutlined, DownloadOutlined, PrinterOutlined, ExpandOutlined,
} from '@ant-design/icons';
import { getReportPreviewUrl, getReportDownloadUrl } from '../services/api';
const { Title } = Typography;
export default function ReportPreview() {
const { school } = useParams<{ school: string }>();
const navigate = useNavigate();
const iframeRef = useRef<HTMLIFrameElement>(null);
const [loading, setLoading] = useState(true);
const schoolName = decodeURIComponent(school || '');
const previewUrl = getReportPreviewUrl(schoolName);
useEffect(() => {
setLoading(true);
}, [schoolName]);
const handlePrint = () => {
const iframe = iframeRef.current;
if (iframe?.contentWindow) {
iframe.contentWindow.print();
}
};
const handleNewTab = () => {
window.open(previewUrl, '_blank');
};
return (
<div>
<Breadcrumb
style={{ marginBottom: 16 }}
items={[
{ title: <a onClick={() => navigate('/')}></a> },
{ title: `${schoolName} 报告预览` },
]}
/>
<Card
title={
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
type="text"
/>
<Title level={5} style={{ margin: 0 }}>{schoolName} </Title>
</Space>
}
extra={
<Space>
<Button icon={<ExpandOutlined />} onClick={handleNewTab}>
</Button>
<Button icon={<PrinterOutlined />} onClick={handlePrint}>
</Button>
<Button
type="primary"
icon={<DownloadOutlined />}
href={getReportDownloadUrl(schoolName)}
>
</Button>
</Space>
}
bodyStyle={{ padding: 0, height: 'calc(100vh - 200px)' }}
bordered={false}
>
{loading && (
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: '#fff', zIndex: 1,
}}>
<Spin size="large" tip="加载报告中..." />
</div>
)}
<iframe
ref={iframeRef}
src={previewUrl}
className="report-iframe"
style={{
width: '100%',
height: '100%',
border: 'none',
}}
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
message.error('报告加载失败');
}}
/>
</Card>
</div>
);
}
+154
View File
@@ -0,0 +1,154 @@
import axios from 'axios';
import { getToken, clearAuth } from './auth';
// 同源部署:前端和 API 由同一个 FastAPI 服务提供,使用相对路径
const api = axios.create({
baseURL: '/api',
timeout: 30000,
});
// 请求拦截器:自动附加 JWT token
api.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:401 时跳转登录页
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
clearAuth();
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
export interface SchoolSummary {
name: string;
type: string;
code: string;
nature: string;
score: number;
rank: number;
cluster: string;
has_report: boolean;
report_size: number;
report_generated_at: string;
}
export interface GenerateResponse {
task_id: string;
school: string;
status: string;
}
export interface BatchResponse {
batch_id: string;
schools: string[];
total: number;
}
export interface TaskStatus {
status: string;
school: string;
progress: number;
total: number;
current_segment: string;
elapsed?: number;
score?: number;
rank?: number;
error?: string;
}
export interface BatchStatus {
status: string;
schools: string[];
total: number;
completed: number;
current_school?: string;
elapsed?: number;
results: Array<{
school: string;
status: string;
score?: number;
rank?: number;
cluster?: string;
time?: number;
error?: string;
}>;
}
// 学校列表
export const getSchools = () =>
api.get<{ schools: SchoolSummary[]; total: number }>('/schools');
// 学校详情
export const getSchoolDetail = (name: string) =>
api.get(`/schools/${encodeURIComponent(name)}`);
// 学校维度数据
export const getSchoolDimensions = (name: string) =>
api.get(`/schools/${encodeURIComponent(name)}/dimensions`);
// 生成报告
export const generateReport = (school: string, useCache = true, skipLlm = false) =>
api.post<GenerateResponse>('/reports/generate', {
school,
use_cache: useCache,
skip_llm: skipLlm,
});
// 批量生成
export const batchGenerate = (schools?: string[], useCache = true, skipLlm = false) =>
api.post<BatchResponse>('/reports/batch', {
schools,
use_cache: useCache,
skip_llm: skipLlm,
});
// 查询生成状态
export const getTaskStatus = (taskId: string) =>
api.get<TaskStatus>(`/reports/generate/${taskId}/status`);
// 查询批量状态
export const getBatchStatus = (batchId: string) =>
api.get<BatchStatus>(`/reports/batch/${batchId}/status`);
// 报告预览 URL(自动附带 token)
export const getReportPreviewUrl = (school: string) => {
const base = `/api/reports/${encodeURIComponent(school)}/preview`;
const token = getToken();
return token ? `${base}?token=${encodeURIComponent(token)}` : base;
};
// 报告下载 URL(自动附带 token)
export const getReportDownloadUrl = (school: string) => {
const base = `/api/reports/${encodeURIComponent(school)}/download`;
const token = getToken();
return token ? `${base}?token=${encodeURIComponent(token)}` : base;
};
// SSE 进度流
export const getTaskStreamUrl = (taskId: string) =>
`/api/reports/generate/${taskId}/stream`;
export const getBatchStreamUrl = (batchId: string) =>
`/api/reports/batch/${batchId}/stream`;
// 批量汇总
export const getReportSummary = () =>
api.get('/reports/summary');
// 测评框架
export const getFramework = () =>
api.get('/config/framework');
export default api;
+85
View File
@@ -0,0 +1,85 @@
/**
* 认证服务:登录、登出、Token 管理
*/
import axios from 'axios';
const TOKEN_KEY = 'access_token';
export interface LoginResponse {
access_token: string;
token_type: string;
expires_in: number;
}
/** 登录 */
export async function login(username: string, password: string): Promise<LoginResponse> {
const res = await axios.post<LoginResponse>('/api/auth/login', { username, password });
const { access_token } = res.data;
localStorage.setItem(TOKEN_KEY, access_token);
return res.data;
}
/** 登出 */
export async function logout(): Promise<void> {
try {
await axios.post('/api/auth/logout');
} catch {
// 即使服务端失败也清理本地
}
localStorage.removeItem(TOKEN_KEY);
}
/** 获取本地存储的 token */
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
/** 验证 token 是否仍然有效 */
export async function verifyToken(): Promise<boolean> {
const token = getToken();
if (!token) return false;
try {
const res = await axios.get('/api/auth/verify', {
headers: { Authorization: `Bearer ${token}` },
});
return res.data?.valid === true;
} catch {
return false;
}
}
/** 清除认证信息 */
export function clearAuth(): void {
localStorage.removeItem(TOKEN_KEY);
}
/**
* 为 axios 实例添加认证拦截器
* - 请求拦截:自动附加 Authorization header
* - 响应拦截:401 时跳转登录页
*/
export function setupAuthInterceptors(instance: typeof axios) {
// 请求拦截器
instance.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
instance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
clearAuth();
// 如果当前不在登录页,跳转到登录页
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
}
+263
View File
@@ -0,0 +1,263 @@
import axios from 'axios';
import { getToken, clearAuth } from './auth';
const api = axios.create({
baseURL: '/api/era2',
timeout: 30000,
});
// 请求拦截器:自动附加 JWT token
api.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:401 时跳转登录页
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
clearAuth();
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
// ==================== 类型定义 ====================
/** 报告语言:"zh"=中文,"en"=英文,"both"=同时生成中英两份 */
export type ReportLang = 'zh' | 'en' | 'both';
export interface DistrictSummary {
district: string;
school_count: number;
avg_score: number;
report_count: number;
report_count_zh?: number;
report_count_en?: number;
}
export interface SchoolInfo {
name: string;
district: string;
type: string;
nature: string;
area: string;
score: number;
district_rank: number;
city_rank: number;
total_in_district: number;
total_in_city: number;
cluster: string;
// 兼容旧字段
has_report: boolean;
report_size: number;
report_generated_at: string;
// 中英分开
has_report_zh?: boolean;
has_report_en?: boolean;
report_size_zh?: number;
report_size_en?: number;
report_generated_at_zh?: string;
report_generated_at_en?: string;
}
export interface TaskStatus {
status: string;
school: string;
district: string;
progress: number;
total: number;
current_segment: string;
elapsed?: number;
score?: number;
rank?: number;
error?: string;
lang?: string; // "zh" | "en" | "both"
langs?: string[]; // ["zh"] / ["en"] / ["zh","en"]
current_lang?: string;
outputs?: Record<string, string>;
}
export interface BatchStatus {
status: string;
district: string;
schools: string[];
total: number;
completed: number;
current_school?: string;
current_lang?: string;
lang?: string;
langs?: string[];
elapsed?: number;
results: Array<{
school: string;
status: string;
score?: number;
rank?: number;
time?: number;
error?: string;
langs?: string[];
outputs?: Record<string, string>;
}>;
}
export interface ReportHistoryItem {
school: string;
district: string;
type: string;
score: number | null;
file_size: number;
generated_at: string;
file_name: string;
lang?: 'zh' | 'en';
}
// ==================== API 调用 ====================
// 区列表
export const getDistricts = () =>
api.get<{ districts: DistrictSummary[]; total: number }>('/districts');
// 某区的学校列表
export const getSchoolsInDistrict = (district: string) =>
api.get<{ district: string; schools: SchoolInfo[]; total: number }>(
`/districts/${encodeURIComponent(district)}/schools`
);
// 生成报告
export const generateReport = (
school: string,
district: string,
options: {
useCache?: boolean;
skipLlm?: boolean;
enableAgent?: boolean;
lang?: ReportLang;
} = {}
) =>
api.post<{
task_id: string;
school: string;
district: string;
lang?: string;
langs?: string[];
}>('/reports/generate', {
school,
district,
use_cache: options.useCache ?? true,
skip_llm: options.skipLlm ?? false,
enable_agent: options.enableAgent ?? false, // AI助理已移至前端ChatFab,静态HTML不再内嵌
lang: options.lang ?? 'zh',
});
// 批量生成
export const batchGenerate = (
district: string,
schools?: string[],
options: {
useCache?: boolean;
skipLlm?: boolean;
enableAgent?: boolean;
lang?: ReportLang;
} = {}
) =>
api.post<{
batch_id: string;
district: string;
total: number;
lang?: string;
langs?: string[];
}>('/reports/batch', {
district,
schools,
use_cache: options.useCache ?? true,
skip_llm: options.skipLlm ?? false,
enable_agent: options.enableAgent ?? false, // AI助理已移至前端ChatFab,静态HTML不再内嵌
lang: options.lang ?? 'zh',
});
// 任务状态
export const getTaskStatus = (taskId: string) =>
api.get<TaskStatus>(`/reports/generate/${taskId}/status`);
// 批量状态
export const getBatchStatus = (batchId: string) =>
api.get<BatchStatus>(`/reports/batch/${batchId}/status`);
/**
* 报告预览URL(自动附带 token,供 iframe / window.open 使用)
* lang: 'zh' (默认) 或 'en'
*/
export const getReportPreviewUrl = (
district: string,
school: string,
lang: 'zh' | 'en' = 'zh',
) => {
const base = `/api/era2/reports/${encodeURIComponent(district)}/${encodeURIComponent(school)}/preview`;
const token = getToken();
const params = new URLSearchParams();
params.set('lang', lang);
if (token) params.set('token', token);
return `${base}?${params.toString()}`;
};
/**
* 报告下载URL(自动附带 token,供 <a href> 使用)
* lang: 'zh' (默认) 或 'en'
*/
export const getReportDownloadUrl = (
district: string,
school: string,
lang: 'zh' | 'en' = 'zh',
) => {
const base = `/api/era2/reports/${encodeURIComponent(district)}/${encodeURIComponent(school)}/download`;
const token = getToken();
const params = new URLSearchParams();
params.set('lang', lang);
if (token) params.set('token', token);
return `${base}?${params.toString()}`;
};
// 报告历史
export const getReportHistory = () =>
api.get<{ reports: ReportHistoryItem[] }>('/reports/history');
// LLM聊天 — SSE流式
export const getChatStreamUrl = () => '/api/era2/chat';
/**
* 发送聊天消息 — 原生 fetch + SSE 流式读取
* @param signal 可选 AbortSignal,用于取消请求(组件卸载 / 用户中断)
*/
export const sendChatMessage = (
message: string,
school?: string,
district?: string,
history: Array<{ role: string; content: string }> = [],
signal?: AbortSignal,
lang: 'zh' | 'en' = 'zh',
) => {
const token = getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return fetch(getChatStreamUrl(), {
method: 'POST',
headers,
body: JSON.stringify({ message, school, district, history, lang }),
signal,
});
};
export default api;
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/output': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})
+127
View File
@@ -0,0 +1,127 @@
"""
i18n 包:报告中英文国际化支持
设计原则:
1. 数据层 key 保持中文(不破坏 PCA / 缓存兼容性)
2. 仅在显示层(模板/LLM prompt)通过 t() 翻译
3. 找不到 key 时 fallback 到中文原值,保证渐进式迁移不会爆 Jinja
"""
from typing import Dict, Any
from . import zh_CN, en_US
_BUNDLES = {
"zh": zh_CN.STRINGS,
"en": en_US.STRINGS,
}
DEFAULT_LANG = "zh"
class Translator:
"""
可点号访问的翻译器,供 Jinja 模板使用:
Jinja 模板用法:
{{ t.score }} → "得分" / "Score"
{{ t.dim('课程领导力') }} → "课程领导力" / "Curriculum Leadership"
{{ t.sub('国家标准遵循') }} → "国家标准遵循" / "National Standards Compliance"
{{ t.level_desc('国家标准遵循', 4) }} → 水平 4 的描述
{{ t.part('part3') }} → "第三部分 课程领导力表现" / "Part III. Curriculum Leadership"
"""
def __init__(self, lang: str = DEFAULT_LANG):
self.lang = lang if lang in _BUNDLES else DEFAULT_LANG
self._bundle: Dict[str, Any] = _BUNDLES[self.lang]
self._fallback: Dict[str, Any] = _BUNDLES[DEFAULT_LANG]
# ---- 字符串字段(UI label ----
def __getattr__(self, key: str) -> str:
# 首先在 UI 字典中找,找不到回退到中文
ui = self._bundle.get("UI", {})
if key in ui:
return ui[key]
ui_zh = self._fallback.get("UI", {})
return ui_zh.get(key, key)
def get(self, key: str, default: str = "") -> str:
ui = self._bundle.get("UI", {})
if key in ui:
return ui[key]
ui_zh = self._fallback.get("UI", {})
return ui_zh.get(key, default or key)
# ---- 维度名翻译 ----
def dim(self, name_zh: str) -> str:
"""7 大二级维度名翻译"""
d = self._bundle.get("DIMENSIONS", {})
return d.get(name_zh, name_zh)
def sub(self, name_zh: str) -> str:
"""20 个三级维度名翻译"""
d = self._bundle.get("SUB_DIMENSIONS", {})
return d.get(name_zh, name_zh)
def part(self, part_id: str) -> str:
"""部分标题:part0/part1/part3..part9/part10"""
d = self._bundle.get("PARTS", {})
return d.get(part_id, part_id)
def part_number(self, part_id: str) -> str:
"""部分编号:第三部分 / Part III."""
d = self._bundle.get("PART_NUMBERS", {})
return d.get(part_id, part_id)
def level_desc(self, sub_dim_zh: str, level: int) -> str:
"""子维度水平描述(4 档)"""
d = self._bundle.get("LEVEL_DESCRIPTIONS", {})
sub = d.get(sub_dim_zh, {})
if not sub:
sub = self._fallback.get("LEVEL_DESCRIPTIONS", {}).get(sub_dim_zh, {})
return sub.get(level, sub.get(str(level), ""))
def dim_def(self, name_zh: str) -> str:
"""7 大维度的概念定义(用于 LLM prompt + 模板说明)"""
d = self._bundle.get("DIMENSION_DEFINITIONS", {})
return d.get(name_zh, "")
def sub_def(self, name_zh: str) -> str:
"""20 个三级维度的概念定义"""
d = self._bundle.get("SUB_DIMENSION_DEFINITIONS", {})
return d.get(name_zh, "")
def cluster_label(self, kind: str) -> str:
"""聚类标签:good / weak / mid"""
d = self._bundle.get("CLUSTERS", {})
return d.get(kind, kind)
def school_type(self, type_zh: str) -> str:
"""学校类型翻译(市实验性示范性高中等)"""
d = self._bundle.get("SCHOOL_TYPES", {})
return d.get(type_zh, type_zh)
def district(self, name_zh: str) -> str:
"""区域名翻译(长宁区等)"""
d = self._bundle.get("DISTRICTS", {})
return d.get(name_zh, name_zh)
def date(self, dt) -> str:
"""日期格式化(按语言)"""
from datetime import datetime
if isinstance(dt, str):
return dt
if self.lang == "en":
return dt.strftime("%B %d, %Y")
return dt.strftime("%Y年%m月%d")
def get_translator(lang: str = DEFAULT_LANG) -> Translator:
"""获取翻译器实例"""
return Translator(lang)
def get_bundle(lang: str = DEFAULT_LANG) -> Dict[str, Any]:
"""获取语言原始字典(供低层代码使用)"""
return _BUNDLES.get(lang, _BUNDLES[DEFAULT_LANG])
__all__ = ["Translator", "get_translator", "get_bundle", "DEFAULT_LANG"]
+569
View File
@@ -0,0 +1,569 @@
"""
English dictionary (en_US)
Style: OECD/PISA-aligned formal academic English, addressing school leadership.
NOTE on terminology choices:
- "Curriculum Leadership" follows OECD ILE (Innovative Learning Environments) usage.
- "Tertiary indicator / sub-dimension" used interchangeably to match Chinese 三级维度.
- "Level 1-4" maintained as ordinal labels (no rephrase to "tier"), matching the
monitoring framework published by the Shanghai Academy of Educational Sciences.
- The school is addressed as "your school" (consistent with the Chinese 贵校).
"""
# 7 secondary dimensions
DIMENSIONS = {
"课程领导力": "Curriculum Leadership",
"教学变革力": "Instructional Reform Capacity",
"学生发展指导力": "Student Development Guidance",
"教师发展支持力": "Teacher Development Support",
"教育质量评估力": "Educational Quality Assessment",
"教育条件保障力": "Educational Conditions and Resources",
"数字化赋能力": "Digital Empowerment",
}
# 20 tertiary sub-dimensions
SUB_DIMENSIONS = {
"国家标准遵循": "National Standards Compliance",
"课程结构建设": "Curriculum Structure Design",
"课程规范落实": "Curriculum Governance Implementation",
"教学方式变革": "Pedagogical Reform",
"作业设计与管理变革": "Homework Design and Management",
"学科发展的个性化辅导": "Personalized Subject Tutoring",
"学生生涯发展指导": "Student Career Development Guidance",
"培训支持": "Professional Training Support",
"教研支持": "Teaching Research Support",
"项目支持": "Research Project Support",
"科学评价观": "Scientific Assessment Perspective",
"学业质量评估": "Academic Quality Assessment",
"综合素质评估": "Holistic Competency Assessment",
"实践活动评估": "Practice-Based Activity Assessment",
"区域推进": "District-Level Implementation Drive",
"环境支持": "Environmental Support",
"资源支持": "Resource Support",
"教学方式创新": "Innovative Instructional Methods",
"评价精准化与个性化": "Precise and Personalized Assessment",
"课程迭代优化": "Iterative Curriculum Optimization",
}
# Section titles
PARTS = {
"cover": "Cover",
"part0": "Part I. Background and Methodology",
"part1": "Part II. Overall Performance",
"part3": "Part III. Curriculum Leadership",
"part4": "Part IV. Instructional Reform Capacity",
"part5": "Part V. Student Development Guidance",
"part6": "Part VI. Teacher Development Support",
"part7": "Part VII. Educational Quality Assessment",
"part8": "Part VIII. Educational Conditions and Resources",
"part9": "Part IX. Digital Empowerment",
"part10": "Part X. Conclusions and Recommendations",
"action_guide": "Implementation Action Guide",
}
PART_NUMBERS = {
"part0": "Part I",
"part1": "Part II",
"part3": "Part III",
"part4": "Part IV",
"part5": "Part V",
"part6": "Part VI",
"part7": "Part VII",
"part8": "Part VIII",
"part9": "Part IX",
"part10": "Part X",
}
# Dimension conceptual definitions (used in LLM prompts and template explanations)
DIMENSION_DEFINITIONS = {
"课程领导力": "Curriculum Leadership refers to a school's core capacity to faithfully implement the national curriculum and to construct a competency-oriented school-based curriculum aligned with its educational vision. It emphasizes curricular diversity and distinctiveness, and the construction of a school-specific curriculum system anchored in core competencies. It comprises National Standards Compliance, Curriculum Structure Design, and Curriculum Governance Implementation.",
"教学变革力": "Instructional Reform Capacity denotes a school's transformative power in shifting from a knowledge-centered to a competency-oriented model of teaching. It directly shapes the cultivation of students' core competencies and emphasizes moving away from rote learning, mechanical training, and passive reception. It comprises Pedagogical Reform and Homework Design and Management.",
"学生发展指导力": "Student Development Guidance refers to a school's capacity to provide comprehensive and personalized guidance for students, encompassing precise subject-area tutoring and forward-looking career planning. It comprises Personalized Subject Tutoring and Student Career Development Guidance.",
"教师发展支持力": "Teacher Development Support refers to a school's institutional safeguards and resource investments for teachers' professional growth, emphasizing diversified development pathways and supporting platforms. It comprises Professional Training Support, Teaching Research Support, and Research Project Support.",
"教育质量评估力": "Educational Quality Assessment refers to a school's capacity to establish a scientific evaluation system and to monitor educational quality holistically. Anchored in core competencies, it integrates multiple modes of assessment. It comprises Scientific Assessment Perspective, Academic Quality Assessment, Holistic Competency Assessment, and Practice-Based Activity Assessment.",
"教育条件保障力": "Educational Conditions and Resources refers to a school's capacity to safeguard curriculum implementation through district-level policy support, hardware infrastructure, and resource allocation. It comprises District-Level Implementation Drive, Environmental Support, and Resource Support.",
"数字化赋能力": "Digital Empowerment refers to a school's capacity to leverage digital technologies for instructional innovation, precision in assessment, and continuous curriculum optimization, reflecting the depth and breadth of its digital transformation. It comprises Innovative Instructional Methods, Precise and Personalized Assessment, and Iterative Curriculum Optimization.",
}
# 20 sub-dimension conceptual definitions
SUB_DIMENSION_DEFINITIONS = {
"国家标准遵循": "National Standards Compliance is the foundational condition of Curriculum Leadership, emphasizing the importance of fully and adequately offering the national curriculum on campus. Benchmarked against the General Senior High School Curriculum Plan (2017 Edition, 2020 Revision) and the Shanghai Implementation Plan, it reflects the school's compliance with class-hour and credit requirements across course types.",
"课程结构建设": "Curriculum Structure Design emphasizes the deliberate consideration of curricular composition and proportion across subjects and course types, advancing the scientific rigor and rationality of curriculum design. It reflects the structural soundness and richness of the school's offerings across the three categories of courses.",
"课程规范落实": "Curriculum Governance Implementation is the value foundation and institutional safeguard of the curriculum, focusing on the completeness of formal curriculum documents and the establishment and use of process-tracking archival records.",
"教学方式变革": "Pedagogical Reform emphasizes students' active participation, inquiry, and collaboration; it guides students to learn through practice and to engage in deep learning under teachers' guidance, with routine integration of cross-disciplinary work and information technology.",
"作业设计与管理变革": "Homework Design and Management addresses the systemic level of the school's work in innovative homework design, time-allocation management, marking and feedback, and attribute tagging.",
"学科发展的个性化辅导": "Personalized Subject Tutoring concerns a school's capacity to deliver precise, individualized academic tutoring, including tutoring duration, the manner in which content is determined, and tutoring formats.",
"学生生涯发展指导": "Student Career Development Guidance concerns the implementation modes, coverage, faculty composition, and resource backing of the school's career-planning education.",
"培训支持": "Professional Training Support reflects the intensity of off-site training opportunities the school provides for teachers, with the average number of off-site training participants per subject as the core indicator.",
"教研支持": "Teaching Research Support reflects both the quantity and quality of teaching-research activities, embodying the depth of the school's teaching-research culture and the effectiveness of its mechanisms.",
"项目支持": "Research Project Support reflects how the school uses funded research projects to drive teachers' professional development, with the coverage of school-level-or-above projects across subjects as the core indicator.",
"科学评价观": "Scientific Assessment Perspective reflects the breadth and depth of attention the school pays to the development of students' core competencies across the various dimensions of curriculum and instruction.",
"学业质量评估": "Academic Quality Assessment focuses on the systematicity of the school's development and use of school-based assessment tools and its semestral examination quality analyses.",
"综合素质评估": "Holistic Competency Assessment focuses on the construction of school-based holistic-competency evaluation systems, the supporting platforms, and the application of evaluation results.",
"实践活动评估": "Practice-Based Activity Assessment focuses on the development and use of school-based assessment tools for inquiry-based learning, social investigation, and subject-based practical activities.",
"区域推进": "District-Level Implementation Drive reflects the strength with which the school's district education bureau drives high-school curriculum and instruction work, including meeting frequency, governance documents, and supporting measures.",
"环境支持": "Environmental Support reflects how the school's information environment and physical infrastructure underpin curriculum and instruction.",
"资源支持": "Resource Support reflects the integrated condition of the school's internal and external resource allocation as well as its faculty profile.",
"教学方式创新": "Innovative Instructional Methods focuses on the depth of integration between information technology and instruction, and on teachers' routine use of information technology in teaching.",
"评价精准化与个性化": "Precise and Personalized Assessment focuses on the school's capability tier in leveraging information-technology platforms to support subject-level diagnostics and holistic-competency evaluation.",
"课程迭代优化": "Iterative Curriculum Optimization focuses on the school's planning institutions for digital transformation in instruction and the extent to which information systems are embedded across business workflows.",
}
# Level descriptions (4 tiers per sub-dimension)
LEVEL_DESCRIPTIONS = {
"国家标准遵循": {
4: "All required courses are fully offered as mandated, and elective-required and elective tracks both meet the standards.",
3: "Required courses for examination subjects are fully offered, and elective-required and elective tracks meet the standards.",
2: "Required courses are not fully offered as mandated; one of the elective-required or elective tracks meets the standards.",
1: "Required courses are not fully offered as mandated, and neither the elective-required nor the elective tracks meet the standards.",
},
"课程结构建设": {
4: "Total deviation across required subject courses is within 30%, total deviation across the three course categories is below 150%, and school-based and integrated-practice courses are well developed.",
3: "Total deviation across the three course categories is below 250%, with school-based and integrated-practice courses above the average level.",
2: "Total deviation across the three course categories is below 300%, but school-based and integrated-practice courses are weak.",
1: "Total deviation across the three course categories is high, and school-based and integrated-practice courses fall in the bottom 25%.",
},
"课程规范落实": {
4: "Both formal curriculum documents and archival records have been fully established.",
3: "Formal curriculum documents are in place and process-tracking archives have been fully established, though some are not yet in active use.",
2: "Formal curriculum documents are partially in place, and most archives have been established but are not yet in use.",
1: "Formal curriculum documents are largely absent, and archives have not yet been established.",
},
"教学方式变革": {
4: "All teachers share consensus and engage in research, with the ability to systematically design and effectively implement instruction; at least three routine implementation formats are present.",
3: "Most teachers share consensus and engage in research, and can implement the relevant requirements set forth in the textbooks; at least two routine implementation formats are present.",
2: "Individual teachers engage in research and occasionally guide students in inquiry; at least one routine implementation format is present.",
1: "There is essentially no research on instructional methods, related learning is rarely organized, and either none or only one implementation format exists.",
},
"作业设计与管理变革": {
4: "At least three types of innovative assignments are mastered in each category; assignment duration is centrally managed; regular grading and feedback are conducted; multi-attribute tagging is in place.",
3: "At least two types of innovative assignments are mastered in each category; duration is occasionally managed; face-to-face grading dominates; at least two attribute tags are used.",
2: "At least one type of innovative assignment is mastered or one to two types are well developed; students self-manage duration; feedback is sparse; attribute tagging is present.",
1: "Only one type of innovative assignment is well developed; students self-manage duration; feedback is virtually absent; no attribute tagging is used.",
},
"学科发展的个性化辅导": {
4: "Average tutoring time per subject exceeds 2 hours per week; teachers determine content based on learner profiles; tutoring is delivered individually.",
3: "Average tutoring time per subject is 1-2 hours per week; teachers determine content based on learner profiles; tutoring is mainly individualized and dispersed.",
2: "Average tutoring time per subject is below 1 hour per week; teachers tutor in response to student requests; tutoring is grouped or dispersed.",
1: "Tutoring time is below 1 hour per week or absent; teachers tutor only on request; tutoring is delivered to the entire class.",
},
"学生生涯发展指导": {
4: "A dedicated career-guidance course is offered, covering 90% or more of students over three years, with a combined in-house and external faculty team and adequate internal-and-external resource support.",
3: "Implementation relies on guest lectures, covering 70-90% of students over three years, supported by an external faculty team with some internal-and-external resources.",
2: "Implementation is integrated with social investigation or volunteer service, covering 50-70% of students, with an external faculty team and almost no resource support.",
1: "Implementation is integrated with social investigation or volunteer service, covering below 50% of students; no stable faculty team is in place; resource support is virtually absent.",
},
"培训支持": {
4: "Average number of teachers per subject participating in off-site training is no fewer than 2.5.",
3: "Average number of teachers per subject participating in off-site training is no fewer than 1.5.",
2: "Average number of teachers per subject participating in off-site training is no fewer than 1.",
1: "Teachers across subjects rarely participate in off-site training.",
},
"教研支持": {
4: "Both the quantity and quality of teaching-research activities are at a high level.",
3: "Teaching-research activities are present and of relatively good quality.",
2: "Teaching-research activities are present but of average quality.",
1: "Both the quantity and quality of activities are at a low level.",
},
"项目支持": {
4: "Each subject leads at least one school-level-or-above project.",
3: "Some subjects lead at least one school-level-or-above project.",
2: "No subject leads a school-level-or-above project; some subjects lead at least one school-level project.",
1: "No school-level or higher projects exist across subjects.",
},
"科学评价观": {
4: "Across all dimensions, attention is given to the development of at least two competency components.",
3: "Across at least three dimensions, attention is given to the development of at least two competency components.",
2: "Considerable attention is given to student development.",
1: "Limited attention is given to student development.",
},
"学业质量评估": {
4: "School-based assessment tools have been developed and are in use; semestral examination quality analyses with comprehensive attribute tagging are in place.",
3: "School-based assessment tools have been developed and are in use; semestral examination quality analyses are not mandatory.",
2: "At least one school-based assessment tool has been developed; semestral examination quality analyses are not mandatory.",
1: "Academic quality assessment is not adequately prioritized; school-based assessment tools are largely absent.",
},
"综合素质评估": {
4: "School-based holistic-competency evaluation systems are fully established and in use, with platform support and scientifically expressed results.",
3: "School-based holistic-competency evaluation systems are established and in use; platform support and the use of evaluation results require improvement.",
2: "A school-based holistic-competency evaluation plan is established, but specific evaluation tools remain to be developed.",
1: "School-based holistic-competency evaluation is not adequately prioritized; the plan, tools, and use of results are all underdeveloped.",
},
"实践活动评估": {
4: "School-based assessment tools for inquiry-based learning, social investigation, and subject-based practical activities have all been developed and are in use.",
3: "School-based assessment tools for subject-based practical activities have been developed and are in use; tools for at least one of inquiry-based learning or social investigation have been developed but are not yet in use.",
2: "School-based assessment tools for subject-based practical activities have been developed.",
1: "School-based assessment tools for inquiry-based learning, social investigation, and subject-based practical activities have not yet been developed or applied.",
},
"区域推进": {
4: "Meetings are held an average of two or more times per month; both the number of governance documents and supporting measures are at the maximum levels.",
3: "Meetings are held an average of once or more per month, with three or more governance documents and three supporting measures.",
2: "At least one of meeting convening, governance documents, or supporting measures is well developed.",
1: "Meeting convening, governance documents, and supporting measures are all underdeveloped.",
},
"环境支持": {
4: "Both informatization support and hardware support are at a high level.",
3: "Both informatization support and hardware support are around the average level.",
2: "At least one of informatization support or hardware support is well developed.",
1: "Both informatization support and hardware support are underdeveloped.",
},
"资源支持": {
4: "Both internal-and-external resources and faculty quality are at a high level.",
3: "At least one component of internal-and-external resources is well supplied; faculty quality is good.",
2: "At least one component of internal-and-external resources is slightly below average; faculty quality is slightly below average.",
1: "Internal-and-external resources and faculty quality are all underdeveloped.",
},
"教学方式创新": {
4: "Information technology is deeply integrated with instruction; teachers use information technology in a routine manner.",
3: "Information technology is relatively well integrated with instruction; teachers can use information technology.",
2: "Information technology is moderately integrated with instruction; the school has built information platforms.",
1: "Information technology is poorly integrated with instruction; teachers rarely use information technology.",
},
"评价精准化与个性化": {
4: "Self-built information platforms support subject-level diagnostics and holistic-competency assessment.",
3: "Third-party platforms are leveraged to support subject-level diagnostics and holistic-competency assessment.",
2: "Information platforms are in place to support academic assessment.",
1: "No information platform is available to support assessment.",
},
"课程迭代优化": {
4: "The school has a dedicated professional-development plan for digital transformation in instruction and conducts related activities; the vast majority of business workflows use information systems.",
3: "The school has not yet established a dedicated professional-development plan; the vast majority of business workflows use information systems.",
2: "The school undertakes few digital-transformation activities; only a minority of business workflows use information systems.",
1: "The school undertakes virtually no digital-transformation activities; an information-management system has not yet been established.",
},
}
CLUSTERS = {
"good": "High-Performing Cluster",
"weak": "Improvement-Needed Cluster",
"mid": "Mid-Tier Cluster",
# 直接映射 stats_engine 输出的原始标签
"较好": "High-Performing",
"待提升": "Improvement-Needed",
"中等": "Mid-Tier",
}
SCHOOL_TYPES = {
"市实验性示范性高中": "Municipal Experimental Demonstrative High School",
"区实验性示范性高中": "District Experimental Demonstrative High School",
"特色高中": "Featured High School",
"公办普通高中": "Public General High School",
"民办高中": "Private High School",
"": "",
}
DISTRICTS = {
"长宁区": "Changning District",
"杨浦区": "Yangpu District",
"闵行区": "Minhang District",
"浦东新区": "Pudong New Area",
"嘉定区": "Jiading District",
"宝山区": "Baoshan District",
"金山区": "Jinshan District",
"静安区": "Jing'an District",
"奉贤区": "Fengxian District",
"普陀区": "Putuo District",
"徐汇区": "Xuhui District",
"all": "Citywide",
}
UI = {
# Report titles
"report_title": "Curriculum Implementation Monitoring Data Analysis Report",
"report_subtitle": "A Seven-Dimensional Analysis from a School Leadership Perspective",
# Generic labels
"score": "Score",
"rank": "Rank",
"level": "Level",
"your_school": "your school",
"school_name": "School",
"district": "District",
"city": "Municipality",
"total_score": "Overall Score",
"overall_score": "Overall Score",
"district_avg": "District Average",
"city_avg": "Municipal Average",
"same_type_avg": "Peer-Type Average",
"rank_in_district": "District Rank",
"rank_in_city": "Municipal Rank",
"vs_district_avg": "vs. District Average",
"vs_city_avg": "vs. Municipal Average",
"dimension": "Dimension",
"sub_dimension": "Sub-dimension",
"indicator": "Indicator",
"performance": "Performance",
"diff": "Δ",
"level_1": "Level 1",
"level_2": "Level 2",
"level_3": "Level 3",
"level_4": "Level 4",
"level_label": "Level",
"loading": "Analysis loading...",
"page": "Page",
# Performance labels
"perf_good": "Strong Performance",
"perf_above": "Slightly Above District Average",
"perf_neutral": "Near District Average",
"perf_below": "Slightly Below District Average",
"perf_weak": "Needs Improvement",
# Section sub-titles
"sec_overall_perf": "Overall Performance",
"sec_subdim_analysis": "Detailed Sub-Dimension Analysis",
"sec_level_compare": "Sub-Dimension Level Comparison",
"sec_top3": "Top Three Priorities",
"sec_cross_dim": "Cross-Dimensional Analysis",
"sec_improvement_room": "Improvement Potential Analysis",
"sec_review": "Synthesis and Improvement Pathways",
# Part I (Background)
"p0_h1_background": "I. Assessment Background",
"p0_h1_framework": "II. Assessment Framework",
"p0_h1_implement": "III. Assessment Implementation",
"p0_h2_target": "(i) Target Population",
"p0_h2_method": "(ii) Methodology",
"p0_h2_analysis": "(iii) Data Analysis",
"p0_h2_levels": "(iv) Tertiary-Indicator Level Definitions",
# Tables
"tbl_indicator_system": "Table 1-1. Indicator System",
"tbl_level_definition": "Table 1-2. Tertiary-Indicator Level Definitions",
"th_secondary_dim": "Secondary Dimension",
"th_tertiary_dim": "Tertiary Indicator",
"th_indicator_interp": "Indicator Description",
# Conclusion
"top3_intro": "The following three priorities are identified by jointly considering severity, leverage, and feasibility, and are recommended as the school's core focus areas for the current term.",
# Figure / table prefix
"fig": "Figure",
"tbl": "Table",
# School attributes
"school_type": "School Type",
"school_cluster": "Cluster",
# Ranking
"rank_format": "Rank {rank} of {total}",
"rank_in_district_label": "District Rank",
"rank_in_city_label": "Municipal Rank",
# Common units
"of": "/",
"schools_unit": "schools",
"points": "pts",
# Radar / comparison labels
"radar_legend_self": "This School",
"radar_legend_district": "District Average",
"radar_legend_same_type": "Peer-Type Average",
# Improvement waterfall
"current_total": "Current Score",
"potential_total": "Potential Score",
"gain_label": "Lift to Level 3",
# Footer
"generated_on": "Generated on",
# Action guide
"action_critical": "Critical Attention",
"action_attention": "Needs Attention",
"action_maintain": "Maintain",
"action_excel": "Sustain Leadership",
"action_timeline": "Implementation Timeline",
# Part II overview
"p1_h1_overall_status": "I. Overall Curriculum Implementation Status",
"p1_h1_dim_status": "II. Performance by Dimension",
"cluster_type": "Implementation Cluster",
"vs_same_type": "vs. Peer Type",
"rank_in_top_pct": "Top {pct}%",
# Figure titles (Part II)
"fig2_0a": "Figure 2-0a. School Curriculum Implementation Profile",
"fig2_0b": "Figure 2-0b. Strength-Gap Quadrant Analysis (Tertiary Indicators)",
"fig2_1": "Figure 2-1. Seven-Dimension Score Comparison",
"fig2_2": "Figure 2-2. Seven-Dimension Radar Chart",
"fig2_3": "Figure 2-3. Distribution of School Implementation Clusters",
"fig2_4": "Figure 2-4. Profile Comparison of the Two Clusters",
"fig2_5": "Figure 2-5. Cluster Profile Comparison (Radar)",
"fig2_6": "Figure 2-6. District-Wide Overall Score Ranking",
"fig2_7": "Figure 2-7. Inter-Dimension Correlation Analysis",
# Alert boxes
"alert_title": "Critical Alert: The Following Sub-Dimensions Require Immediate Attention",
"positioning_gap_title": "Analysis: Gap Between School Positioning and Actual Performance",
# Dimension detail (generic)
"dim_score_label": "{name} Score",
"dim_section_overall": "I. Overall Performance",
"dim_section_subdims": "{name}: Sub-Dimension Level Comparison",
"diff_vs_district": "Δ (vs. District)",
# Dimension figure titles
"dim_fig_score": "Figure {n}-1. {name}: Score Overview",
"dim_fig_subradar": "Figure {n}-2. {name}: Sub-Dimension Scores",
"dim_fig_subbar": "Figure {n}-3. {name}: Sub-Dimension Comparison",
"dim_fig_levels": "Figure {n}-4. {name}: Sub-Dimension Level Distribution",
"dim_fig_scatter": "Figure {n}-5. {name}: Cluster Scatter Plot",
# Sub-dimension
"sd_score": "Score",
"sd_district_avg": "District Average",
"sd_level_grade": "Level Tier",
"sd_rank": "Rank",
"sd_city_rank_prefix": "Citywide",
"sd_level_meaning": "Meaning of Level {lv}",
# Conclusion
"p10_h1": "Part X. Conclusions and Recommendations",
"p10_h2_top3": "Top Three Priorities",
"p10_h2_cross": "I. Cross-Dimensional Analysis",
"p10_h2_improve": "Improvement Potential Analysis",
"p10_h2_review": "Synthesis and Improvement Pathways",
"p10_fig_waterfall": "Figure 10-1. Improvement Potential: Estimated Gains from Lifting Sub-Indicators to Level 3",
# Action guide
"p11_h1": "Part XI. Implementation Action Guide",
"p11_intro": "This part organises improvement actions by urgency, drawing on the data analysis above. Senior leadership should concentrate limited resources on the most pressing priorities rather than spreading effort evenly.",
"p11_focus_title": "Strategic Focus: Three Priorities for the Current Term",
"p11_focus_intro": "The three priorities below jointly weigh data severity, leverage of improvement, and feasibility. They are recommended as the school's <strong>essential and highest-priority</strong> commitments for the current term, and the detailed plans in subsequent sections elaborate on them.",
"p11_critical": "Critical Breakthroughs (Level-1 Sub-Dimensions) — Immediate Action",
"p11_attention": "Focused Push (Level-2 Sub-Dimensions) — Initiate This Term",
"p11_maintain": "Steady Consolidation (Level-3 Sub-Dimensions) — Sustained Effort",
"p11_excel": "Deepening Leadership (Level-4 Sub-Dimensions) — Knowledge Diffusion",
"p11_timeline": "Term Action Timeline",
"p11_no_weak": "Your school currently exhibits no Level-1 or Level-2 weaknesses; the foundation is solid, and the focus below shifts to consolidation and the diffusion of leading practices.",
"p11_timeline_loading": "Timeline being generated...",
"p11_disclaimer": "<strong>Note:</strong> The action recommendations above are generated by AI based on monitoring data and are intended as a reference framework. Concrete implementation plans should be co-developed by the school's leadership team and subject experts in light of local conditions, and we recommend localised adaptation under the guidance of educational specialists.",
# Part I (Background) body text
"p0_para1": "In response to a sequence of national education-policy directives — including the Ministry of Education's Guiding Opinions on the Implementation of New Curricula and Textbooks for Senior Secondary Schools (No. 15, 2018), the State Council General Office's Guiding Opinions on Reforming the Way Senior Secondary Schools Educate Students in the New Era (No. 29, 2019), and the Action Plan for Deepening the Reform of Curriculum and Instruction in Basic Education (Letter No. 3 of the Department of Teaching Materials, 2023) — and to operationalise the Ministry of Education General Office's Notice on Carrying Out Monitoring of Curriculum Implementation and Textbook Use (Letter No. 5 of the Department of Teaching Materials, 2023), the Shanghai Municipal Education Commission has issued targeted policies to strengthen the translation of the national curriculum into practice, and has called for the establishment of a sound monitoring and feedback mechanism for curriculum implementation that, guided by evidence-based decision making, continuously refines curriculum planning and implementation pathways.",
"p0_para2": "School leadership is the core driver of school development. The competency orientation of implementation management and the quality of decision making directly shape the overall performance and outcomes of curriculum and instruction at the school. The pivotal task of implementation management lies in the school's curriculum planning and policy choices, and in the genuine landing of those decisions within instructional practice. We therefore adopt a school-leadership perspective and ensure that the seven dimensions — Curriculum Leadership, Instructional Reform Capacity, Student Development Guidance, Teacher Development Support, Educational Quality Assessment, Educational Conditions and Resources, and Digital Empowerment — operate synergistically across the full landscape of curriculum implementation.",
"p0_framework_intro": "Adopting a school-leadership perspective, this report systematically analyses and reconstructs the curriculum-implementation monitoring indicators and establishes a seven-dimensional indicator system (see Table 1-1).",
"p0_target_text": "The current monitoring exercise targets general senior secondary schools in {district}; a total of {total} schools participated. From each participating school, representatives of the administrative leadership (principals, vice-principals, department heads) and chairs of subject teaching-research groups were sampled to complete the questionnaire.",
"p0_method_text": "Following the research framework of the Shanghai Academy of Educational Sciences (Shanghai Curriculum Research Office) for monitoring curriculum implementation in primary and secondary schools, the study collected information through questionnaire surveys, covering school basic information, overall curriculum implementation, and subject-level curriculum implementation.",
"p0_analysis_intro": "The data analysis pipeline is as follows:",
"p0_step1": "<strong>Step 1. Indicator System Reconstruction.</strong> The mapping between dimensions, indicators, and individual items is reconstructed under the seven-dimension school-leadership perspective, forming a multi-tier indicator system.",
"p0_step2": "<strong>Step 2. Principal Component Analysis (PCA) Synthesis.</strong> Each item in the questionnaire is standardised, and PCA is applied to combine correlated variables into principal components, yielding the score of each tertiary indicator.",
"p0_step3": "<strong>Step 3. Standardisation to a Common Scale.</strong> The principal components are standardised to a normal distribution with mean 50 and standard deviation 10. Within this scaling, 68.27% of schools fall in the [40, 60] range and 84.45% fall in [30, 70]; a school scoring 60 thus outperforms approximately 84% of all schools.",
"p0_step4": "<strong>Step 4. Level Definition for Tertiary Indicators.</strong> Drawing on the conceptual content of each indicator and the empirical distribution of school performance, item-level cut-points are determined and each school is assigned to one of four levels (see Table 1-2).",
"p0_step5": "<strong>Step 5. Cluster Analysis.</strong> Schools are grouped on the basis of their standardised scores; schools with similar profiles are clustered together to identify distinct school typologies along each dimension.",
# Indicator-system interpretations (Table 1-1)
"p0_interp_curriculum": "1. Ensure full and adequate provision of national courses; 2. Examine the rationality and diversity of curriculum design across subject courses, school-based courses, integrated practical activities, and labour courses; 3. Attend to the formal construction of competency-oriented curricula.",
"p0_interp_instruction": "1. In-class pedagogical innovation that promotes deep learning and prizes individualised education; 2. Scientifically efficient design and management of out-of-class assignments.",
"p0_interp_student": "1. Course-selection guidance and individualised tutoring tailored to student profiles; 2. Personalised career-guidance services and a comprehensive career-development support system; 3. Attention to the cultivation of students' holistic competencies.",
"p0_interp_teacher": "1. Provision of induction, in-service, and off-site professional learning for teachers; 2. Regular teaching-research activities and the development of instructional resource libraries; 3. Support for teachers' participation in funded research and projects.",
"p0_interp_quality": "1. A scientific perspective on assessment oriented toward students' core competencies and holistic development; 2. Attention to academic quality and benchmark-aligned evaluation; 3. Multi-faceted evaluation of holistic competencies; 4. Attention to students' performance in practice-based activities.",
"p0_interp_condition": "1. Visibility into how the district education bureau drives senior-secondary teaching and learning; 2. Assessment of information-technology environments, instructional equipment, and physical facilities; 3. Coordination of faculty deployment and internal-and-community resources.",
"p0_interp_digital": "1. Use of digital and intelligent resources to support and empower curriculum, instruction, and assessment; 2. Construction of digital and intelligent resources, information platforms, and information systems within and beyond the school.",
# AI Chat Widget UI
"chat_fab_title": "AI Report Assistant",
"chat_title": "AI Report Assistant",
"chat_subtitle_prefix": "Based on the report data of",
"chat_subtitle_suffix": "",
"chat_clear": "Clear conversation",
"chat_close": "Close",
"chat_welcome_p1_a": "Hello! I am the AI assistant for the ",
"chat_welcome_p1_b": " Curriculum Implementation Monitoring Report.",
"chat_welcome_p2": "I have full access to this report's data. You may:",
"chat_welcome_li1": "Ask about the performance of specific dimensions and comparisons",
"chat_welcome_li2": "Explore strengths and areas for improvement",
"chat_welcome_li3": "Request interpretations of the charts",
"chat_welcome_li4": "Seek concrete improvement recommendations",
"chat_welcome_p3": "What would you like to know?",
"chat_sg_overall": "Overall Performance",
"chat_sg_overall_q": "How does the school perform overall and how does it rank within the district?",
"chat_sg_strengths": "Strengths & Gaps",
"chat_sg_strengths_q": "Which dimensions are strengths and which need improvement?",
"chat_sg_gap": "Gap Analysis",
"chat_sg_gap_q": "Which dimensions show the largest gaps compared with the district average?",
"chat_sg_advice": "Recommendations",
"chat_sg_advice_q": "Provide the three most important improvement recommendations.",
"chat_input_placeholder": "Type your question...",
"chat_send_title": "Send",
"chat_request_failed": "Request failed",
"chat_retry": "Please try again later.",
"chat_apikey_failed": "Failed to decode API key",
"chat_lang_hint": "(Please answer in formal English.)",
# ECharts labels
"ec_district_avg": "District Average",
"ec_same_type_avg": "Peer-Type Average",
"ec_cluster_good": "High-Performing Cluster",
"ec_cluster_weak": "Improvement-Needed Cluster",
"ec_cluster_good_full": "High-Performing Cluster ({n} schools)",
"ec_cluster_weak_full": "Improvement-Needed Cluster ({n} schools)",
"ec_unit_schools": "schools",
"ec_correlation": "Correlation",
"ec_belongs_to": "belongs to",
"ec_cluster_good_short": "High-Performing",
"ec_cluster_weak_short": "Improvement-Needed",
"ec_level": "Level",
"ec_score_label": "Score",
"ec_pieces": "items",
"ec_quadrant_q1": "Core Strengths",
"ec_quadrant_q2": "Potential",
"ec_quadrant_q3": "Critical Improvement",
"ec_quadrant_q4": "Hidden Risks",
"ec_self": "This School",
"ec_district_position": "District Average Reference",
"ec_overall_score": "Overall Score",
"ec_district_avg_short": "Dist. Avg.",
"ec_school_self": "This School",
"ec_lift_to_lv3": "Lift to Level 3",
"ec_total_now": "Current Score",
"ec_total_potential": "Potential Score",
"ec_thermo_self": "This School",
"ec_lv4_threshold": "Level 4 Threshold",
"ec_lv3_threshold": "Level 3 Threshold",
"ec_lv2_threshold": "Level 2 Threshold",
"ec_dim_score": "Dimension Score",
"ec_avg_level": "Average Level",
"ec_min_level": "Weakest Level",
"ec_dim_rank": "Dimension Rank",
"ec_baseline_50": "Mean Baseline (50)",
"ec_lvl_one": "Level 1",
"ec_lvl_two": "Level 2",
"ec_lvl_three": "Level 3",
"ec_lvl_four": "Level 4",
"ec_at_level": "{name}: Level {lv}",
"ec_same_type": "Peer Schools",
"ec_good_type_short": "High-Performing",
"ec_weak_type_short": "Improvement-Needed",
"ec_district_rank_n": "Rank {rank} in District",
"ec_level_dist_summary": "Level Distribution",
"ec_lv_short": "L",
"ec_quad_x_axis": "Score",
"ec_quad_y_axis": "Δ vs. District Average",
"ec_quad_district_avg_marker": "District Avg.",
"ec_quad_score": "Score",
"ec_quad_diff": "Δ",
"ec_quad_level": "Level",
"ec_thermo_self_marker": "",
"ec_thermo_dist_marker": "▲Dist.",
"ec_thermo_same_marker": "▲Peer",
"ec_waterfall_current": "Current Score",
"ec_waterfall_potential": "Potential Score",
"ec_waterfall_contrib": "Estimated Contribution",
"ec_waterfall_pts_unit": "pts",
}
STRINGS = {
"DIMENSIONS": DIMENSIONS,
"SUB_DIMENSIONS": SUB_DIMENSIONS,
"PARTS": PARTS,
"PART_NUMBERS": PART_NUMBERS,
"DIMENSION_DEFINITIONS": DIMENSION_DEFINITIONS,
"SUB_DIMENSION_DEFINITIONS": SUB_DIMENSION_DEFINITIONS,
"LEVEL_DESCRIPTIONS": LEVEL_DESCRIPTIONS,
"CLUSTERS": CLUSTERS,
"SCHOOL_TYPES": SCHOOL_TYPES,
"DISTRICTS": DISTRICTS,
"UI": UI,
}
+462
View File
@@ -0,0 +1,462 @@
"""
中文字典(zh_CN
注意:所有 key 与 en_US 保持一致;维度名字典是「中文 → 中文」(即恒等),
仅为对称设计,便于 t.dim() 在 zh 模式下也能正常调用。
"""
DIMENSIONS = {
"课程领导力": "课程领导力",
"教学变革力": "教学变革力",
"学生发展指导力": "学生发展指导力",
"教师发展支持力": "教师发展支持力",
"教育质量评估力": "教育质量评估力",
"教育条件保障力": "教育条件保障力",
"数字化赋能力": "数字化赋能力",
}
SUB_DIMENSIONS = {
"国家标准遵循": "国家标准遵循",
"课程结构建设": "课程结构建设",
"课程规范落实": "课程规范落实",
"教学方式变革": "教学方式变革",
"作业设计与管理变革": "作业设计与管理变革",
"学科发展的个性化辅导": "学科发展的个性化辅导",
"学生生涯发展指导": "学生生涯发展指导",
"培训支持": "培训支持",
"教研支持": "教研支持",
"项目支持": "项目支持",
"科学评价观": "科学评价观",
"学业质量评估": "学业质量评估",
"综合素质评估": "综合素质评估",
"实践活动评估": "实践活动评估",
"区域推进": "区域推进",
"环境支持": "环境支持",
"资源支持": "资源支持",
"教学方式创新": "教学方式创新",
"评价精准化与个性化": "评价精准化与个性化",
"课程迭代优化": "课程迭代优化",
}
# 部分标题
PARTS = {
"cover": "封面",
"part0": "第一部分 测评背景与实施",
"part1": "第二部分 总体表现",
"part3": "第三部分 课程领导力表现",
"part4": "第四部分 教学变革力表现",
"part5": "第五部分 学生发展指导力表现",
"part6": "第六部分 教师发展支持力表现",
"part7": "第七部分 教育质量评估力表现",
"part8": "第八部分 教育条件保障力表现",
"part9": "第九部分 数字化赋能力表现",
"part10": "第十部分 总结与改进建议",
"action_guide": "实践落地行动指南",
}
# 部分编号前缀(用于动态拼接 "第X部分 维度名表现"
PART_NUMBERS = {
"part0": "第一部分",
"part1": "第二部分",
"part3": "第三部分",
"part4": "第四部分",
"part5": "第五部分",
"part6": "第六部分",
"part7": "第七部分",
"part8": "第八部分",
"part9": "第九部分",
"part10": "第十部分",
}
# 7 大维度概念定义(LLM 用)
DIMENSION_DEFINITIONS = {
"课程领导力": "课程领导力是指学校在高质量落实国家课程,根据学校培养目标构建核心素养导向校本课程体系上的关键能力,强调对课程多样性及特色性的重视,强调在核心素养引领下建构富有学校特色的课程育人体系。具体可分为国家标准遵循、课程结构建设和课程规范落实。",
"教学变革力": "教学变革力是指学校由知识本位转向素养本位的变革力量,它在一定程度上决定了学生核心素养培育的成效,强调对过分重视接受学习、死记硬背、机械训练现状的转变。具体可分为教学方式变革和作业设计与管理变革。",
"学生发展指导力": "学生发展指导力是指学校为学生提供全面、个性化发展指导的能力,涵盖学科学习的精准辅导和面向未来的生涯规划。具体可分为学科发展的个性化辅导和学生生涯发展指导。",
"教师发展支持力": "教师发展支持力是指学校在教师专业成长方面的制度保障与资源投入能力,强调为教师提供多元化的发展路径与支持平台。具体可分为培训支持、教研支持和项目支持。",
"教育质量评估力": "教育质量评估力是指学校建立科学评价体系、全面监测教育质量的能力,强调以核心素养为导向,综合运用多种评价方式。具体可分为科学评价观、学业质量评估、综合素质评估和实践活动评估。",
"教育条件保障力": "教育条件保障力是指学校在区域政策支持、硬件环境和资源配置方面为课程实施提供保障的能力。具体可分为区域推进、环境支持和资源支持。",
"数字化赋能力": "数字化赋能力是指学校运用数字技术推动教学创新、评价精准化和课程持续优化的能力,反映学校数字化转型的深度与广度。具体可分为教学方式创新、评价精准化与个性化和课程迭代优化。",
}
# 20 个三级维度概念定义(LLM 用)
SUB_DIMENSION_DEFINITIONS = {
"国家标准遵循": "国家标准遵循是课程领导力的首要条件,强调学校在校内开足开齐开好国家课程的重要性,以《普通高中课程方案(2017年版2020年修订)》《上海市普通高中课程实施方案》为标准,反映学校在各类型课程课时、学分上的达标情况。",
"课程结构建设": "课程结构建设强调学校在课程设置中需着重考虑各学科、各类型的课程结构与比例,强调课程设置的科学性与合理性,反映学校在三类课程中的结构合理性与内容丰富性。",
"课程规范落实": "课程规范落实强调学校课程规范落实作为课程的价值定位和制度保证,关注建设规范文本的完备性和过程性档案记录的建成与使用情况。",
"教学方式变革": "教学方式变革强调学生主动参与和探究合作,引导学生在实践中学习、在教师指导下深度学习,强调跨学科和信息技术常态化应用的学习。",
"作业设计与管理变革": "作业设计与管理变革关注学校在创新性作业设计、作业时长管理、批改反馈和属性标注等方面的系统化程度。",
"学科发展的个性化辅导": "学科发展的个性化辅导关注学校为学生提供学科学习方面的精准化、个性化辅导的能力,包括辅导时长、辅导内容确定方式和辅导形式。",
"学生生涯发展指导": "学生生涯发展指导关注学校为学生提供生涯规划教育的实施方式、覆盖率、师资队伍和资源支持情况。",
"培训支持": "培训支持反映学校为教师提供外出培训机会的力度,以各学科教师平均外出培训人数为核心指标。",
"教研支持": "教研支持反映学校教研活动的数量和质量,体现学校教研文化的深度和教研机制的有效性。",
"项目支持": "项目支持反映学校以课题项目引领教师专业发展的情况,以各学科负责校级以上项目的覆盖情况为核心指标。",
"科学评价观": "科学评价观反映学校在课程教学各方面对学生核心素养发展的关注程度和广度。",
"学业质量评估": "学业质量评估关注学校在校本化评价工具研制、使用以及学期考试质量分析的系统性。",
"综合素质评估": "综合素质评估关注学校在校本化综合素质评价体系的建设、平台支持和评价结果运用情况。",
"实践活动评估": "实践活动评估关注学校在研究性学习、社会考察和学科实践活动等领域校本化评价工具的研制与使用。",
"区域推进": "区域推进反映学校所在区教育局对高中课程教学工作的推动力度,包括会议频次、管理文件和配套措施。",
"环境支持": "环境支持反映学校信息化环境和硬件设施对课程教学的支撑情况。",
"资源支持": "资源支持反映学校校内外资源配置和师资水平的综合状况。",
"教学方式创新": "教学方式创新关注信息技术与教学融合的深度以及教师常态化使用信息技术开展教学的情况。",
"评价精准化与个性化": "评价精准化与个性化关注学校运用信息技术平台支持学科诊断与综合素质评价的能力层次。",
"课程迭代优化": "课程迭代优化关注学校教学数字化转型的规划制度和信息化系统在业务流程中的应用程度。",
}
# 水平质性描述(与 config_era2.LEVEL_DESCRIPTIONS 完全一致)
LEVEL_DESCRIPTIONS = {
"国家标准遵循": {4: "所有科目必修课程开齐开足,选必和选修满足要求", 3: "考试类科目必修开齐,选必和选修满足要求", 2: "必修未能开齐开足,选必和选修有一个满足要求", 1: "必修未能开齐开足,选必和选修均不满足要求"},
"课程结构建设": {4: "学科类必修课程离差总和在30%以内,总体三类课程在150%以下,校本课程和综合实践较好", 3: "总体三类课程离差总和在250%以下,校本课程和综合实践高于平均水平", 2: "总体三类课程离差总和在300%以下,校本课程和综合实践较差", 1: "总体三类课程的离差和很高,校本课程和综合实践在末尾25%"},
"课程规范落实": {4: "都有建设规范文本和档案记录", 3: "都有建设规范文本,但过程性档案记录已经全部建成,部分有尚未使用", 2: "部分有建设规范文本,档案大部分已经建成但未使用", 1: "基本没有建设规范文本,档案尚未建成"},
"教学方式变革": {4: "所有老师有共识和研究,并能够系统设计、有效实施,至少有3种常态化落实形式", 3: "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式", 2: "个别老师有研究,并能够偶尔引导学生开展学习,至少有1种常态化落实形式", 1: "基本没有教学方法等相关研究,基本不组织相关学习,没有或仅有1种落实形式"},
"作业设计与管理变革": {4: "在每类创新性作业中至少掌握3种类型,作业时长有统一控制管理,定期批改评价,有多元化属性标注", 3: "在每类创新性作业中至少掌握2种类型,偶有时长控制管理,面批为主,有至少两种属性标注", 2: "至少掌握1种类型或擅长某1-2种创新作业,学生自己控制时长,很少反馈,有属性标注", 1: "擅长某种创新作业,学生自己控制时长,几乎不反馈,无属性标注"},
"学科发展的个性化辅导": {4: "各学科平均辅导时长每周2小时以上,教师根据学情确定内容,采取个别辅导方式", 3: "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导", 2: "各学科平均辅导时长每周1小时以内,学生提出需求后教师辅导,分组统一或分散辅导", 1: "辅导时长每周1小时以内或不辅导,学生提出需求后教师辅导,班级统一辅导"},
"学生生涯发展指导": {4: "专设生涯指导课程,三年覆盖90%+学生,本校+外聘教师队伍,校内外资源足够支持", 3: "外请讲座实施,三年覆盖70-90%学生,外聘教师队伍,校内外资源有一些支持", 2: "与社会考察/志愿服务结合实施,三年覆盖50-70%学生,外聘教师,几乎无资源支持", 1: "与社会考察/志愿服务结合实施,三年覆盖50%以下,未形成稳定队伍,几乎无资源支持"},
"培训支持": {4: "各学科教师平均外出培训人数≥2.5人", 3: "各学科教师平均外出培训人数≥1.5人", 2: "各学科教师平均外出培训人数≥1人", 1: "各学科教师几乎不进行外出培训"},
"教研支持": {4: "教研的数量和质量均较高", 3: "有教研活动,质量较好", 2: "有教研活动但质量一般", 1: "活动数量和质量均较低"},
"项目支持": {4: "各学科均有负责的校级以上项目至少一个", 3: "有部分学科负责校级以上项目至少一个", 2: "各学科没有负责的校级以上项目,部分学科有校级项目至少一个", 1: "各学科校级及校级以上的项目均没有"},
"科学评价观": {4: "在所有方面均能至少关注两项素养发展", 3: "至少有三个方面关注两项素养发展", 2: "能较多关注学生发展", 1: "较少关注学生发展"},
"学业质量评估": {4: "校本化评价工具已研制并使用,学期考试质量分析并标注属性较为全面", 3: "校本化评价工具已研制并使用,学期考试质量分析不做硬性要求", 2: "至少已研制一项校本化评价工具,学期考试质量分析不做硬性要求", 1: "未能重视学业质量评估,校本化评价工具均欠缺"},
"综合素质评估": {4: "校本化综合素质评价体系均有建设和使用,有平台支持且评价结果表达科学", 3: "校本化综合素质评价体系均有建设和使用,支持平台和评价结果使用有待提高", 2: "校本化综合素质评价方案建成,但具体评价工具有待开发", 1: "未能重视校本化综合素质评价,方案、工具和结果使用等均欠缺"},
"实践活动评估": {4: "研究性学习、社会考察和学科实践活动的校本化评价工具已研制并使用", 3: "学科实践活动的校本化评价工具已研制并使用,研究性学习/社会考察至少一项已研制但尚未使用", 2: "学科实践活动的校本化评价工具已研制", 1: "研究性学习、社会考察和学科实践活动的校本化评价工具均尚未研制和使用"},
"区域推进": {4: "召开会议平均一个月2次及以上,区域管理文件数量和措施均为最大值", 3: "召开会议平均一个月1次及以上,区域管理文件3个以上,措施达3项", 2: "召开会议、文件和措施至少有一项建设较好", 1: "召开会议、文件和措施三项均建设较差"},
"环境支持": {4: "信息化支持和硬件支持均处于较高水平", 3: "信息化支持和硬件在平均水平附近", 2: "信息化支持和硬件支持至少有一项建设较好", 1: "信息化支持和硬件支持均建设较差"},
"资源支持": {4: "校内外资源和师资水平均处于较高水平", 3: "校内外资源至少有一项供给较好,师资水平较好", 2: "校内外资源至少有一项仅略低于平均水平,师资水平略低于平均水平", 1: "校内外资源和师资水平三项均建设较差"},
"教学方式创新": {4: "信息技术与教学融合程度高,教师能常态化使用信息技术", 3: "信息技术与教学融合程度较高,教师能使用信息技术", 2: "信息技术与教学融合程度一般,学校有信息化平台建设", 1: "信息技术与教学融合程度较差,教师基本不使用信息技术"},
"评价精准化与个性化": {4: "有自建信息技术平台支持学科诊断与综合素质评价", 3: "借助第三方平台支持学科诊断与综合素质评价", 2: "有信息技术平台支持学业评价", 1: "没有信息技术平台支持评价"},
"课程迭代优化": {4: "学校对促进教学数字化转型有专项研修计划并开展相关活动,业务绝大部分使用信息化系统", 3: "学校尚未制定专项研修计划,业务绝大部分使用信息化系统", 2: "学校较少开展数字化转型活动,少数业务使用信息化系统", 1: "学校几乎不开展数字化转型活动,尚未建成信息化管理系统"},
}
CLUSTERS = {
"good": "课程实施较好类",
"weak": "课程实施待提升类",
"mid": "中等水平类",
# 直接映射 stats_engine 输出的原始标签
"较好": "较好",
"待提升": "待提升",
"中等": "中等",
}
SCHOOL_TYPES = {
"市实验性示范性高中": "市实验性示范性高中",
"区实验性示范性高中": "区实验性示范性高中",
"特色高中": "特色高中",
"公办普通高中": "公办普通高中",
"民办高中": "民办高中",
"": "",
}
DISTRICTS = {
"长宁区": "长宁区",
"杨浦区": "杨浦区",
"闵行区": "闵行区",
"浦东新区": "浦东新区",
"嘉定区": "嘉定区",
"宝山区": "宝山区",
"金山区": "金山区",
"静安区": "静安区",
"奉贤区": "奉贤区",
"普陀区": "普陀区",
"徐汇区": "徐汇区",
"all": "全市",
}
UI = {
# 报告主标题
"report_title": "课程实施监测数据分析报告",
"report_subtitle": "基于学校领导力视角的七维度分析",
# 通用 label
"score": "得分",
"rank": "排名",
"level": "水平",
"your_school": "贵校",
"school_name": "学校",
"district": "",
"city": "全市",
"total_score": "总体得分",
"overall_score": "总体得分",
"district_avg": "区均值",
"city_avg": "市均值",
"same_type_avg": "同类学校均值",
"rank_in_district": "区内排名",
"rank_in_city": "全市排名",
"vs_district_avg": "vs 区均值",
"vs_city_avg": "vs 市均值",
"dimension": "维度",
"sub_dimension": "子维度",
"indicator": "指标",
"performance": "表现",
"diff": "差值",
"level_1": "水平1",
"level_2": "水平2",
"level_3": "水平3",
"level_4": "水平4",
"level_label": "水平",
"loading": "分析加载中...",
"page": "",
# 表现标签
"perf_good": "表现较好",
"perf_above": "略高于区均",
"perf_neutral": "接近区均",
"perf_below": "略低于区均",
"perf_weak": "待提升",
# 章节子标题
"sec_overall_perf": "整体表现",
"sec_subdim_analysis": "子维度详细分析",
"sec_level_compare": "各子维度水平对比",
"sec_top3": "最紧迫的三件事",
"sec_cross_dim": "跨维度综合分析",
"sec_improvement_room": "进步空间分析",
"sec_review": "综合评述与改进方向",
# 第一部分(背景)
"p0_h1_background": "一、测评背景",
"p0_h1_framework": "二、测评框架",
"p0_h1_implement": "三、测评实施",
"p0_h2_target": "(一)测评对象",
"p0_h2_method": "(二)测评方法",
"p0_h2_analysis": "(三)数据分析",
"p0_h2_levels": "(四)三级维度水平划分",
# 表 1-1 表头
"tbl_indicator_system": "表1-1 指标体系表",
"tbl_level_definition": "表1-2 三级维度水平划分表",
"th_secondary_dim": "二级维度",
"th_tertiary_dim": "三级维度",
"th_indicator_interp": "指标解读",
# 总结部分
"top3_intro": "以下三项是综合严重程度、杠杆效应和可操作性后,建议学校本学期优先推进的核心事项。",
# 图标题前缀
"fig": "",
"tbl": "",
# 学校属性
"school_type": "学校类型",
"school_cluster": "聚类类型",
# 排名
"rank_format": "{rank}名 / 共{total}",
"rank_in_district_label": "区内排名",
"rank_in_city_label": "全市排名",
# 总结/常用文字
"of": "/",
"schools_unit": "",
"points": "",
# 雷达图/对比标签
"radar_legend_self": "本校",
"radar_legend_district": "区均值",
"radar_legend_same_type": "同类学校均值",
# 进步空间瀑布图
"current_total": "当前总分",
"potential_total": "潜在总分",
"gain_label": "提升至水平3",
# 通用 footer
"generated_on": "生成于",
# 行动指南
"action_critical": "急需关注",
"action_attention": "需要关注",
"action_maintain": "保持现状",
"action_excel": "持续领先",
"action_timeline": "实施时间表",
# part1_overview 总览
"p1_h1_overall_status": "一、学校课程实施总体状况",
"p1_h1_dim_status": "二、分维度状况",
"cluster_type": "课程实施类型",
"vs_same_type": "vs 同类",
"rank_in_top_pct": "位列前 {pct}%",
# 图标题(part1
"fig2_0a": "图2-0a 学校课程实施画像总览",
"fig2_0b": "图2-0b 三级维度优势-短板象限分析",
"fig2_1": "图2-1 课程实施七维度得分对比",
"fig2_2": "图2-2 课程实施七维度雷达图",
"fig2_3": "图2-3 学校课程实施总体类型分布",
"fig2_4": "图2-4 两类学校课程实施特征对比",
"fig2_5": "图2-5 学校课程实施类型特征对比(雷达图)",
"fig2_6": "图2-6 区内各校总体得分排名",
"fig2_7": "图2-7 课程实施维度间相关性分析",
# 警示框
"alert_title": "紧急预警:以下子维度需立即关注",
"positioning_gap_title": "学校定位与实际表现差距分析",
# 维度详情通用
"dim_score_label": "{name}得分",
"dim_section_overall": "一、整体表现",
"dim_section_subdims": "{name}各子维度水平对比",
"diff_vs_district": "差异(vs区)",
# 维度内图标题
"dim_fig_score": "{n}-1 {name}得分情况",
"dim_fig_subradar": "{n}-2 {name}各子维度得分情况",
"dim_fig_subbar": "{n}-3 {name}各子维度对比详情",
"dim_fig_levels": "{n}-4 {name}各子维度水平分布",
"dim_fig_scatter": "{n}-5 {name}聚类分析散点图",
# sub_dimension
"sd_score": "得分",
"sd_district_avg": "区均值",
"sd_level_grade": "水平等级",
"sd_rank": "排名",
"sd_city_rank_prefix": "全市",
"sd_level_meaning": "水平{lv}含义",
# 总结
"p10_h1": "第十部分 总结与改进建议",
"p10_h2_top3": "最紧迫的三件事",
"p10_h2_cross": "一、跨维度综合分析",
"p10_h2_improve": "进步空间分析",
"p10_h2_review": "综合评述与改进方向",
"p10_fig_waterfall": "图10-1 进步空间分析:提升至水平3的潜在收益",
# 行动指南
"p11_h1": "第十一部分 实践落地行动指南",
"p11_intro": "本部分基于前述数据分析结果,按\"紧迫程度\"排列改进行动。学校管理团队应将有限资源优先投入到最紧迫的事项上,而非平均用力。",
"p11_focus_title": "战略聚焦:本学期必须完成的三件事",
"p11_focus_intro": "以下三项是综合数据严重程度、改进杠杆效应和可操作性后,建议学校本学期<strong>优先且必须</strong>推进的核心事项。后续各节的详细方案均围绕此展开。",
"p11_critical": "急需突破(水平1维度)——立即行动",
"p11_attention": "重点攻关(水平2维度)——本学期启动",
"p11_maintain": "稳步巩固(水平3维度)——持续推进",
"p11_excel": "深化引领(水平4维度)——经验输出",
"p11_timeline": "学期行动时间表",
"p11_no_weak": "贵校当前无水平1或水平2的薄弱维度,整体基础扎实,以下重点关注巩固提升与深化引领。",
"p11_timeline_loading": "时间表生成中...",
"p11_disclaimer": "<strong>说明:</strong>以上行动建议由AI基于监测数据自动生成,旨在提供思路框架与参考方向。具体实施方案需结合学校实际情况,由学校管理团队与专业教师共同研讨确定,建议在教育专家指导下进行校本化调整。",
# 第一部分(背景)正文
"p0_para1": "为深入贯彻《教育部关于做好普通高中新课程新教材实施工作的指导意见》(教基〔2018〕15号)、《关于新时代推进普通高中育人方式改革的指导意见》(国办发〔2019〕29号)及《基础教育课程教学改革深化行动方案》(教材厅函〔2023〕3号)等一系列国家教育政策导向,积极响应《教育部办公厅关于开展课程实施与教材使用监测工作的通知》(教材厅函〔2023〕5号)的具体要求,上海市教委积极行动,发布了针对性的政策文件,进一步强化国家课程方案的实施转化,并提出建立健全课程实施的监测与反馈机制,以循证决策为引领,持续优化与改进课程规划与实施路径。",
"p0_para2": "学校领导力是学校发展的核心驱动力。实施管理的素养导向立意与决策质量直接影响学校课程教学的整体表现与成效。课程实施管理的重点在于学校课程管理规划与施策,以及这些决策在教学活动中的切实落地。鉴于此,我们从学校领导力视角出发,确保课程领导力、教学变革力、学生发展指导力、教师发展支持力、教育质量评估力、教育条件保障力和数字化赋能力七大维度相互协同,共同作用于学校课程实施的全局。",
"p0_framework_intro": "本报告从学校领导力视角出发,对课程实施监测指标数据进行系统性的分析与指标再建构,构建了学校课程实施的七维度指标体系(见表1-1)。",
"p0_target_text": "本次监测面向{district}普通高中学校,共有{total}所高中学校参与本次监测。每所参与监测的高中学校,均抽取了行政管理部门的学校管理者代表(包括校长、副校长、部门主任等)、各学科教研组组长参加问卷调查。",
"p0_method_text": "基于上海市教师教育学院(上海市教委教研室)开展的中小学课程实施监测的研究框架,采用问卷调查方式采集学校课程实施信息,涵盖学校基础信息、课程实施情况和学科课程实施情况三个维度的数据。",
"p0_analysis_intro": "数据分析流程如下:",
"p0_step1": "<strong>第一,指标体系重构。</strong>基于学校领导力视角的七维度重构维度、指标及具体题目的映射关系,形成多级指标体系。",
"p0_step2": "<strong>第二,PCA(主成分分析)合成。</strong>对问卷中的各个题目进行标准化处理,利用主成分分析方法,将多个相关变量合成为主成分,提取各三级维度得分。",
"p0_step3": "<strong>第三,标准化得分统一量纲。</strong>对PCA合成后的主成分进行标准化处理,将数据统一到均值50、标准差10的正态分布上。68.27%的样本处于[40分, 60分]区间,84.45%处于[30分, 70分],如某所学校得分为60分,意味着其表现超过了约84%的学校。",
"p0_step4": "<strong>第四,对三级维度划分水平。</strong>依据维度内涵和学校表现分布,从题目层面确定水平划分的分界点分数,对各学校划分水平1~4(见表1-2)。",
"p0_step5": "<strong>第五,聚类分析。</strong>基于标准化得分,对学校进行聚类分析,将具有相似特征的学校归为一类,识别出各维度下不同类型的学校群体。",
# 七维度指标解读(用于表1-1
"p0_interp_curriculum": "1.确保开齐开足国家课程;2.考察学校课程设置的合理性与多样性,包括学科类课程、校本课程、综合实践活动与劳动课程各类课程的课程安排与学生实践;3.关注核心素养导向课程的规范化建设情况",
"p0_interp_instruction": "1.课中教学方式革新,倡导深度学习,重视个性化教育;2.课后作业设计、管理科学高效",
"p0_interp_student": "1.根据学生的特点和需要,教师进行选课指导、个性化辅导等;2.学校提供个性化的生涯指导服务,建立完善的生涯发展支持体系;3.关注学生综合素质发展",
"p0_interp_teacher": "1.给教师提供入职培训、在职培训以及外出学习机会;2.学校定期组织教研活动,建立教学资源库;3.给教师提供参与科研项目和研究活动的支持",
"p0_interp_quality": "1.面向学生核心素养与综合素质发展,确立科学评价观;2.关注学业质量,对标课程标准科学评价学生表现;3.关注综合素质多方面评价;4.关注学生实践活动表现",
"p0_interp_condition": "1.了解学校所在区域教育局对高中教育教学工作的推动情况;2.评估学校信息技术环境、教学设备、场馆设施的支持情况;3.关注学校如何统筹师资配置、校内资源、社区资源等",
"p0_interp_digital": "1.关注数智化资源支持与赋能课程、教学、评价的情况;2.了解学校校内外的数智化资源、信息化平台与信息系统建设情况",
# AI 对话助手 UI
"chat_fab_title": "AI 报告助手",
"chat_title": "AI 报告助手",
"chat_subtitle_prefix": "基于",
"chat_subtitle_suffix": "报告数据",
"chat_clear": "清空对话",
"chat_close": "关闭",
"chat_welcome_p1_a": "您好!我是 ",
"chat_welcome_p1_b": " 课程实施监测报告的 AI 助手。",
"chat_welcome_p2": "我已了解这份报告的全部数据,您可以:",
"chat_welcome_li1": "询问具体维度的表现和对比",
"chat_welcome_li2": "了解优势和改进方向",
"chat_welcome_li3": "获得图表数据的解读",
"chat_welcome_li4": "请教具体的改进建议",
"chat_welcome_p3": "请问有什么想了解的?",
"chat_sg_overall": "总体表现",
"chat_sg_overall_q": "总体表现如何?在区内处于什么水平?",
"chat_sg_strengths": "优势与短板",
"chat_sg_strengths_q": "哪些维度是优势?哪些需要改进?",
"chat_sg_gap": "差距分析",
"chat_sg_gap_q": "与区均值相比,各维度差距最大的是哪些?",
"chat_sg_advice": "改进建议",
"chat_sg_advice_q": "给出三条最重要的改进建议",
"chat_input_placeholder": "输入您的问题...",
"chat_send_title": "发送",
"chat_request_failed": "请求失败",
"chat_retry": "请稍后重试。",
"chat_apikey_failed": "API Key 解码失败",
"chat_lang_hint": "(请用中文回答)",
# ECharts 图表标签
"ec_district_avg": "区均值",
"ec_same_type_avg": "同类学校均值",
"ec_cluster_good": "课程实施较好类",
"ec_cluster_weak": "课程实施待提升类",
"ec_cluster_good_full": "课程实施较好类({n}所)",
"ec_cluster_weak_full": "课程实施待提升类({n}所)",
"ec_unit_schools": "",
"ec_correlation": "相关系数",
"ec_belongs_to": "属于",
"ec_cluster_good_short": "较好类",
"ec_cluster_weak_short": "待提升类",
"ec_level": "水平",
"ec_score_label": "得分",
"ec_pieces": "",
"ec_quadrant_q1": "核心优势",
"ec_quadrant_q2": "潜力项",
"ec_quadrant_q3": "急需改进",
"ec_quadrant_q4": "隐性风险",
"ec_self": "本校",
"ec_district_position": "区均值参照",
"ec_overall_score": "总体得分",
"ec_district_avg_short": "区均值",
"ec_school_self": "本校",
"ec_lift_to_lv3": "提升至水平3",
"ec_total_now": "当前总分",
"ec_total_potential": "潜在总分",
"ec_thermo_self": "本校",
"ec_lv4_threshold": "水平4线",
"ec_lv3_threshold": "水平3线",
"ec_lv2_threshold": "水平2线",
"ec_dim_score": "维度得分",
"ec_avg_level": "平均水平",
"ec_min_level": "最弱水平",
"ec_dim_rank": "维度排名",
"ec_baseline_50": "均值基线(50)",
"ec_lvl_one": "水平一",
"ec_lvl_two": "水平二",
"ec_lvl_three": "水平三",
"ec_lvl_four": "水平四",
"ec_at_level": "{name}:水平{lv}",
"ec_same_type": "同类学校",
"ec_good_type_short": "较好类",
"ec_weak_type_short": "待提升类",
"ec_district_rank_n": "区内第{rank}",
"ec_level_dist_summary": "水平分布",
"ec_lv_short": "Lv",
"ec_quad_x_axis": "得分",
"ec_quad_y_axis": "与区均值差异",
"ec_quad_district_avg_marker": "区均值",
"ec_quad_score": "得分",
"ec_quad_diff": "差异",
"ec_quad_level": "水平",
"ec_thermo_self_marker": "",
"ec_thermo_dist_marker": "▲区均",
"ec_thermo_same_marker": "▲同类",
"ec_waterfall_current": "当前总分",
"ec_waterfall_potential": "潜在总分",
"ec_waterfall_contrib": "预估总分贡献",
"ec_waterfall_pts_unit": "",
}
STRINGS = {
"DIMENSIONS": DIMENSIONS,
"SUB_DIMENSIONS": SUB_DIMENSIONS,
"PARTS": PARTS,
"PART_NUMBERS": PART_NUMBERS,
"DIMENSION_DEFINITIONS": DIMENSION_DEFINITIONS,
"SUB_DIMENSION_DEFINITIONS": SUB_DIMENSION_DEFINITIONS,
"LEVEL_DESCRIPTIONS": LEVEL_DESCRIPTIONS,
"CLUSTERS": CLUSTERS,
"SCHOOL_TYPES": SCHOOL_TYPES,
"DISTRICTS": DISTRICTS,
"UI": UI,
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+587
View File
@@ -0,0 +1,587 @@
{
"district_filter": "长宁区",
"school_count": 9,
"sub_dimension_scores": {
"国家标准遵循": {
"仙霞高中": 55.2261,
"华政附中": 48.8041,
"复旦中学": 50.3006,
"天山学校": 54.1258,
"市三女中": 41.0937,
"延安中学": 56.1359,
"建青实验": 54.9257,
"民办新虹桥": 40.7642,
"西郊学校": 48.6239
},
"课程结构建设": {
"仙霞高中": 45.7208,
"华政附中": 59.3194,
"复旦中学": 46.0837,
"天山学校": 46.318,
"市三女中": 54.2715,
"延安中学": 45.0087,
"建青实验": 40.4431,
"民办新虹桥": 59.9915,
"西郊学校": 52.8433
},
"课程规范落实": {
"仙霞高中": 44.42,
"华政附中": 43.4085,
"复旦中学": 52.0579,
"天山学校": 51.9388,
"市三女中": 51.0727,
"延安中学": 47.2866,
"建青实验": 51.9388,
"民办新虹桥": 51.9388,
"西郊学校": 55.9379
},
"教学方式变革": {
"仙霞高中": 50.062,
"华政附中": 49.477,
"复旦中学": 50.7955,
"天山学校": 50.9855,
"市三女中": 48.3053,
"延安中学": 50.6592,
"建青实验": 49.8711,
"民办新虹桥": 46.681,
"西郊学校": 52.3253
},
"作业设计与管理变革": {
"仙霞高中": 44.968,
"华政附中": 47.0617,
"复旦中学": 48.2712,
"天山学校": 48.5877,
"市三女中": 54.3336,
"延安中学": 51.8595,
"建青实验": 57.133,
"民办新虹桥": 47.18,
"西郊学校": 50.9857
},
"学科发展的个性化辅导": {
"仙霞高中": 53.5034,
"华政附中": 44.9556,
"复旦中学": 51.5584,
"天山学校": 48.6644,
"市三女中": 47.2845,
"延安中学": 56.6478,
"建青实验": 49.9196,
"民办新虹桥": 47.4249,
"西郊学校": 48.6094
},
"学生生涯发展指导": {
"仙霞高中": 40.3601,
"华政附中": 54.23,
"复旦中学": 50.0,
"天山学校": 51.7965,
"市三女中": 57.6311,
"延安中学": 51.7965,
"建青实验": 61.265,
"民办新虹桥": 40.3601,
"西郊学校": 42.5608
},
"培训支持": {
"仙霞高中": 49.1277,
"华政附中": 49.8011,
"复旦中学": 48.7739,
"天山学校": 51.3166,
"市三女中": 48.123,
"延安中学": 60.1806,
"建青实验": 48.2716,
"民办新虹桥": 46.4335,
"西郊学校": 47.4447
},
"教研支持": {
"仙霞高中": 50.3324,
"华政附中": 47.6997,
"复旦中学": 53.8667,
"天山学校": 52.79,
"市三女中": 50.124,
"延安中学": 53.8217,
"建青实验": 47.9023,
"民办新虹桥": 46.3074,
"西郊学校": 47.4299
},
"项目支持": {
"仙霞高中": 46.3819,
"华政附中": 58.2267,
"复旦中学": 54.3458,
"天山学校": 52.9845,
"市三女中": 50.7995,
"延安中学": 67.954,
"建青实验": 44.8004,
"民办新虹桥": 34.3835,
"西郊学校": 40.1237
},
"科学评价观": {
"仙霞高中": 55.2838,
"华政附中": 51.1834,
"复旦中学": 39.0478,
"天山学校": 53.5481,
"市三女中": 53.9277,
"延安中学": 47.7458,
"建青实验": 51.5574,
"民办新虹桥": 31.352,
"西郊学校": 66.3541
},
"学业质量评估": {
"仙霞高中": 41.6331,
"华政附中": 47.7924,
"复旦中学": 44.7948,
"天山学校": 49.9498,
"市三女中": 48.0109,
"延安中学": 68.484,
"建青实验": 63.6725,
"民办新虹桥": 37.2325,
"西郊学校": 48.4301
},
"综合素质评估": {
"仙霞高中": 39.3144,
"华政附中": 55.5523,
"复旦中学": 38.1422,
"天山学校": 55.0228,
"市三女中": 36.4468,
"延安中学": 62.4542,
"建青实验": 56.8134,
"民办新虹桥": 52.256,
"西郊学校": 53.998
},
"实践活动评估": {
"仙霞高中": 34.9408,
"华政附中": 57.1456,
"复旦中学": 35.2102,
"天山学校": 55.2305,
"市三女中": 46.1554,
"延安中学": 65.1241,
"建青实验": 55.689,
"民办新虹桥": 50.4131,
"西郊学校": 50.0913
},
"区域推进": {
"仙霞高中": 38.9607,
"华政附中": 30.891,
"复旦中学": 49.6889,
"天山学校": 47.0304,
"市三女中": 63.1227,
"延安中学": 57.7586,
"建青实验": 52.3945,
"民办新虹桥": 57.7586,
"西郊学校": 52.3945
},
"环境支持": {
"仙霞高中": 53.6302,
"华政附中": 34.3936,
"复旦中学": 40.344,
"天山学校": 61.4189,
"市三女中": 61.4189,
"延安中学": 50.2782,
"建青实验": 60.6526,
"民办新虹桥": 42.9222,
"西郊学校": 44.9415
},
"资源支持": {
"仙霞高中": 44.5911,
"华政附中": 49.3374,
"复旦中学": 49.3856,
"天山学校": 54.1908,
"市三女中": 57.6986,
"延安中学": 60.7473,
"建青实验": 52.7116,
"民办新虹桥": 32.6634,
"西郊学校": 48.6742
},
"教学方式创新": {
"仙霞高中": 48.7259,
"华政附中": 45.9517,
"复旦中学": 49.7523,
"天山学校": 49.3392,
"市三女中": 51.5614,
"延安中学": 49.06,
"建青实验": 50.9795,
"民办新虹桥": 48.3275,
"西郊学校": 56.4383
},
"评价精准化与个性化": {
"仙霞高中": 49.6399,
"华政附中": 52.1607,
"复旦中学": 45.8586,
"天山学校": 58.4629,
"市三女中": 49.0097,
"延安中学": 52.1607,
"建青实验": 52.1607,
"民办新虹桥": 45.8586,
"西郊学校": 52.1607
},
"课程迭代优化": {
"仙霞高中": 42.9448,
"华政附中": 37.6999,
"复旦中学": 52.8487,
"天山学校": 42.9448,
"市三女中": 52.8487,
"延安中学": 62.7527,
"建青实验": 62.7527,
"民办新虹桥": 37.6999,
"西郊学校": 57.5078
}
},
"dimension_scores": {
"课程领导力": {
"仙霞高中": 48.4556,
"华政附中": 50.5107,
"复旦中学": 49.4807,
"天山学校": 50.7942,
"市三女中": 48.8126,
"延安中学": 49.4771,
"建青实验": 49.1025,
"民办新虹桥": 50.8982,
"西郊学校": 52.4684
},
"教学变革力": {
"仙霞高中": 47.515,
"华政附中": 48.2694,
"复旦中学": 49.5334,
"天山学校": 49.7866,
"市三女中": 51.3195,
"延安中学": 51.2594,
"建青实验": 53.5021,
"民办新虹桥": 46.9305,
"西郊学校": 51.6555
},
"学生发展指导力": {
"仙霞高中": 46.9318,
"华政附中": 49.5928,
"复旦中学": 50.7792,
"天山学校": 50.2304,
"市三女中": 52.4578,
"延安中学": 54.2221,
"建青实验": 55.5923,
"民办新虹桥": 43.8925,
"西郊学校": 45.5851
},
"教师发展支持力": {
"仙霞高中": 48.614,
"华政附中": 51.9092,
"复旦中学": 52.3288,
"天山学校": 52.3637,
"市三女中": 49.6822,
"延安中学": 60.6521,
"建青实验": 46.9914,
"民办新虹桥": 42.3748,
"西郊学校": 44.9994
},
"教育质量评估力": {
"仙霞高中": 42.793,
"华政附中": 52.9184,
"复旦中学": 39.2987,
"天山学校": 53.4378,
"市三女中": 46.1352,
"延安中学": 60.952,
"建青实验": 56.9331,
"民办新虹桥": 42.8134,
"西郊学校": 54.7184
},
"教育条件保障力": {
"仙霞高中": 45.7273,
"华政附中": 38.2073,
"复旦中学": 46.4728,
"天山学校": 54.2133,
"市三女中": 60.7468,
"延安中学": 56.2614,
"建青实验": 55.2529,
"民办新虹桥": 44.4481,
"西郊学校": 48.6701
},
"数字化赋能力": {
"仙霞高中": 47.1035,
"华政附中": 45.2708,
"复旦中学": 49.4865,
"天山学校": 50.249,
"市三女中": 51.1399,
"延安中学": 54.6578,
"建青实验": 55.2976,
"民办新虹桥": 43.962,
"西郊学校": 55.3689
},
"总体得分": {
"仙霞高中": 46.7343,
"华政附中": 48.0969,
"复旦中学": 48.1972,
"天山学校": 51.5821,
"市三女中": 51.4706,
"延安中学": 55.3546,
"建青实验": 53.2389,
"民办新虹桥": 45.0456,
"西郊学校": 50.4951
}
},
"levels": {
"国家标准遵循": {
"仙霞高中": 3,
"华政附中": 2,
"复旦中学": 2,
"天山学校": 3,
"市三女中": 1,
"延安中学": 3,
"建青实验": 3,
"民办新虹桥": 1,
"西郊学校": 2
},
"课程结构建设": {
"仙霞高中": 2,
"华政附中": 4,
"复旦中学": 2,
"天山学校": 2,
"市三女中": 3,
"延安中学": 2,
"建青实验": 1,
"民办新虹桥": 4,
"西郊学校": 3
},
"课程规范落实": {
"仙霞高中": 2,
"华政附中": 1,
"复旦中学": 3,
"天山学校": 3,
"市三女中": 3,
"延安中学": 2,
"建青实验": 3,
"民办新虹桥": 3,
"西郊学校": 3
},
"教学方式变革": {
"仙霞高中": 3,
"华政附中": 3,
"复旦中学": 3,
"天山学校": 3,
"市三女中": 2,
"延安中学": 3,
"建青实验": 3,
"民办新虹桥": 2,
"西郊学校": 3
},
"作业设计与管理变革": {
"仙霞高中": 1,
"华政附中": 2,
"复旦中学": 2,
"天山学校": 2,
"市三女中": 4,
"延安中学": 3,
"建青实验": 4,
"民办新虹桥": 2,
"西郊学校": 3
},
"学科发展的个性化辅导": {
"仙霞高中": 3,
"华政附中": 1,
"复旦中学": 3,
"天山学校": 2,
"市三女中": 2,
"延安中学": 3,
"建青实验": 2,
"民办新虹桥": 2,
"西郊学校": 2
},
"学生生涯发展指导": {
"仙霞高中": 1,
"华政附中": 3,
"复旦中学": 2,
"天山学校": 3,
"市三女中": 4,
"延安中学": 3,
"建青实验": 4,
"民办新虹桥": 1,
"西郊学校": 2
},
"培训支持": {
"仙霞高中": 3,
"华政附中": 3,
"复旦中学": 2,
"天山学校": 4,
"市三女中": 2,
"延安中学": 4,
"建青实验": 2,
"民办新虹桥": 1,
"西郊学校": 1
},
"教研支持": {
"仙霞高中": 3,
"华政附中": 2,
"复旦中学": 3,
"天山学校": 3,
"市三女中": 3,
"延安中学": 3,
"建青实验": 2,
"民办新虹桥": 1,
"西郊学校": 2
},
"项目支持": {
"仙霞高中": 2,
"华政附中": 4,
"复旦中学": 3,
"天山学校": 3,
"市三女中": 3,
"延安中学": 4,
"建青实验": 2,
"民办新虹桥": 1,
"西郊学校": 1
},
"科学评价观": {
"仙霞高中": 3,
"华政附中": 3,
"复旦中学": 1,
"天山学校": 3,
"市三女中": 3,
"延安中学": 2,
"建青实验": 3,
"民办新虹桥": 1,
"西郊学校": 4
},
"学业质量评估": {
"仙霞高中": 2,
"华政附中": 3,
"复旦中学": 2,
"天山学校": 3,
"市三女中": 3,
"延安中学": 4,
"建青实验": 4,
"民办新虹桥": 1,
"西郊学校": 3
},
"综合素质评估": {
"仙霞高中": 2,
"华政附中": 3,
"复旦中学": 1,
"天山学校": 3,
"市三女中": 1,
"延安中学": 4,
"建青实验": 3,
"民办新虹桥": 3,
"西郊学校": 3
},
"实践活动评估": {
"仙霞高中": 1,
"华政附中": 4,
"复旦中学": 1,
"天山学校": 4,
"市三女中": 2,
"延安中学": 4,
"建青实验": 4,
"民办新虹桥": 4,
"西郊学校": 4
},
"区域推进": {
"仙霞高中": 1,
"华政附中": 1,
"复旦中学": 2,
"天山学校": 2,
"市三女中": 4,
"延安中学": 3,
"建青实验": 3,
"民办新虹桥": 3,
"西郊学校": 3
},
"环境支持": {
"仙霞高中": 3,
"华政附中": 1,
"复旦中学": 1,
"天山学校": 4,
"市三女中": 4,
"延安中学": 3,
"建青实验": 4,
"民办新虹桥": 2,
"西郊学校": 2
},
"资源支持": {
"仙霞高中": 2,
"华政附中": 3,
"复旦中学": 3,
"天山学校": 3,
"市三女中": 4,
"延安中学": 4,
"建青实验": 3,
"民办新虹桥": 1,
"西郊学校": 2
},
"教学方式创新": {
"仙霞高中": 3,
"华政附中": 3,
"复旦中学": 3,
"天山学校": 3,
"市三女中": 3,
"延安中学": 3,
"建青实验": 3,
"民办新虹桥": 3,
"西郊学校": 4
},
"评价精准化与个性化": {
"仙霞高中": 2,
"华政附中": 3,
"复旦中学": 2,
"天山学校": 3,
"市三女中": 2,
"延安中学": 3,
"建青实验": 3,
"民办新虹桥": 2,
"西郊学校": 3
},
"课程迭代优化": {
"仙霞高中": 1,
"华政附中": 1,
"复旦中学": 3,
"天山学校": 1,
"市三女中": 3,
"延安中学": 4,
"建青实验": 4,
"民办新虹桥": 1,
"西郊学校": 4
}
},
"clusters": {
"labels": [
1,
1,
1,
0,
0,
0,
0,
1,
0
],
"n_clusters": 2,
"school_clusters": {
"仙霞高中": "待提升",
"华政附中": "待提升",
"复旦中学": "待提升",
"天山学校": "较好",
"市三女中": "较好",
"延安中学": "较好",
"建青实验": "较好",
"民办新虹桥": "待提升",
"西郊学校": "较好"
},
"cluster_means": {
"0": {
"课程领导力": 50.131,
"教学变革力": 51.5046,
"学生发展指导力": 51.6175,
"教师发展支持力": 50.9378,
"教育质量评估力": 54.4353,
"教育条件保障力": 55.0289,
"数字化赋能力": 53.3427
},
"1": {
"课程领导力": 49.8363,
"教学变革力": 48.0621,
"学生发展指导力": 47.7991,
"教师发展支持力": 48.8067,
"教育质量评估力": 44.4559,
"教育条件保障力": 43.7139,
"数字化赋能力": 46.4557
}
},
"cluster_names": {
"0": "较好",
"1": "待提升"
}
}
}
@@ -0,0 +1,886 @@
{
"school": "上大嘉高",
"school_info": {
"type": "区实验性示范性高中",
"code": "B类学校",
"nature": "公办",
"feature": "城市郊区学校"
},
"overall": {
"score": 52.87,
"district_avg": 51.74,
"same_type_avg": 49.81,
"rank_in_district": 4,
"total_schools": 11,
"rank_in_city": 58,
"total_schools_in_city": 266,
"cluster": "较好",
"school_type": "区实验性示范性高中",
"same_type_count": 66,
"rank_in_same_type": 10
},
"dimensions": {
"课程领导力": {
"score": 55.82,
"district_avg": 52.21,
"same_type_avg": 51.21,
"diff_district": 3.61,
"rank_in_district": 3,
"rank_in_city": 18,
"t_test_vs_district": {
"t": 1.344,
"p": 0.3112,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"教学变革力": {
"score": 54.09,
"district_avg": 50.75,
"same_type_avg": 49.35,
"diff_district": 3.34,
"rank_in_district": 2,
"rank_in_city": 34,
"t_test_vs_district": {
"t": 3.487,
"p": 0.1778,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"学生发展指导力": {
"score": 53.29,
"district_avg": 51.42,
"same_type_avg": 49.71,
"diff_district": 1.87,
"rank_in_district": 3,
"rank_in_city": 61,
"t_test_vs_district": {
"t": 3.69,
"p": 0.1685,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"教师发展支持力": {
"score": 51.05,
"district_avg": 51.62,
"same_type_avg": 49.45,
"diff_district": -0.58,
"rank_in_district": 6,
"rank_in_city": 87,
"t_test_vs_district": {
"t": -0.459,
"p": 0.6911,
"significant": false,
"n": 3
},
"cluster": "中等"
},
"教育质量评估力": {
"score": 58.01,
"district_avg": 54.39,
"same_type_avg": 49.59,
"diff_district": 3.62,
"rank_in_district": 4,
"rank_in_city": 50,
"t_test_vs_district": {
"t": 1.622,
"p": 0.2032,
"significant": false,
"n": 4
},
"cluster": "较好"
},
"教育条件保障力": {
"score": 51.68,
"district_avg": 50.45,
"same_type_avg": 49.76,
"diff_district": 1.24,
"rank_in_district": 7,
"rank_in_city": 109,
"t_test_vs_district": {
"t": 0.745,
"p": 0.5341,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"数字化赋能力": {
"score": 46.14,
"district_avg": 51.35,
"same_type_avg": 49.57,
"diff_district": -5.22,
"rank_in_district": 10,
"rank_in_city": 207,
"t_test_vs_district": {
"t": -0.819,
"p": 0.4989,
"significant": false,
"n": 3
},
"cluster": "待提升"
}
},
"sub_dimensions": {
"国家标准遵循": {
"parent_dimension": "课程领导力",
"score": 58.63,
"district_avg": 48.25,
"diff_district": 10.39,
"rank_in_district": 1,
"rank_in_city": 14,
"level": 4,
"level_description": "所有科目必修课程开齐开足,选必和选修满足要求",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 4,
"水平4": 1
}
},
"课程结构建设": {
"parent_dimension": "课程领导力",
"score": 50.45,
"district_avg": 52.96,
"diff_district": -2.5,
"rank_in_district": 7,
"rank_in_city": 130,
"level": 3,
"level_description": "总体三类课程离差总和在250%以下,校本课程和综合实践高于平均水平",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 4,
"水平4": 4
}
},
"课程规范落实": {
"parent_dimension": "课程领导力",
"score": 58.38,
"district_avg": 55.44,
"diff_district": 2.94,
"rank_in_district": 7,
"rank_in_city": 69,
"level": 4,
"level_description": "都有建设规范文本和档案记录",
"level_distribution": {
"水平1": 0,
"水平2": 2,
"水平3": 2,
"水平4": 7
}
},
"教学方式变革": {
"parent_dimension": "教学变革力",
"score": 53.13,
"district_avg": 49.74,
"diff_district": 3.39,
"rank_in_district": 1,
"rank_in_city": 56,
"level": 3,
"level_description": "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 7,
"水平4": 0
}
},
"作业设计与管理变革": {
"parent_dimension": "教学变革力",
"score": 55.05,
"district_avg": 51.75,
"diff_district": 3.29,
"rank_in_district": 3,
"rank_in_city": 36,
"level": 4,
"level_description": "在每类创新性作业中至少掌握3种类型,作业时长有统一控制管理,定期批改评价,有多元化属性标注",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 5,
"水平4": 3
}
},
"学科发展的个性化辅导": {
"parent_dimension": "学生发展指导力",
"score": 53.8,
"district_avg": 51.61,
"diff_district": 2.19,
"rank_in_district": 4,
"rank_in_city": 32,
"level": 3,
"level_description": "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 9,
"水平4": 0
}
},
"学生生涯发展指导": {
"parent_dimension": "学生发展指导力",
"score": 52.79,
"district_avg": 51.23,
"diff_district": 1.55,
"rank_in_district": 6,
"rank_in_city": 119,
"level": 3,
"level_description": "外请讲座实施,三年覆盖70-90%学生,外聘教师队伍,校内外资源有一些支持",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 6,
"水平4": 1
}
},
"培训支持": {
"parent_dimension": "教师发展支持力",
"score": 49.33,
"district_avg": 49.45,
"diff_district": -0.12,
"rank_in_district": 6,
"rank_in_city": 135,
"level": 3,
"level_description": "各学科教师平均外出培训人数≥1.5人",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 6,
"水平4": 1
}
},
"教研支持": {
"parent_dimension": "教师发展支持力",
"score": 53.49,
"district_avg": 51.34,
"diff_district": 2.14,
"rank_in_district": 5,
"rank_in_city": 59,
"level": 3,
"level_description": "有教研活动,质量较好",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 8,
"水平4": 1
}
},
"项目支持": {
"parent_dimension": "教师发展支持力",
"score": 50.32,
"district_avg": 54.07,
"diff_district": -3.75,
"rank_in_district": 5,
"rank_in_city": 87,
"level": 3,
"level_description": "有部分学科负责校级以上项目至少一个",
"level_distribution": {
"水平1": 3,
"水平2": 3,
"水平3": 2,
"水平4": 3
}
},
"科学评价观": {
"parent_dimension": "教育质量评估力",
"score": 56.59,
"district_avg": 54.65,
"diff_district": 1.94,
"rank_in_district": 4,
"rank_in_city": 69,
"level": 3,
"level_description": "至少有三个方面关注两项素养发展",
"level_distribution": {
"水平1": 0,
"水平2": 1,
"水平3": 7,
"水平4": 3
}
},
"学业质量评估": {
"parent_dimension": "教育质量评估力",
"score": 52.35,
"district_avg": 53.6,
"diff_district": -1.26,
"rank_in_district": 7,
"rank_in_city": 91,
"level": 3,
"level_description": "校本化评价工具已研制并使用,学期考试质量分析不做硬性要求",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 3,
"水平4": 5
}
},
"综合素质评估": {
"parent_dimension": "教育质量评估力",
"score": 61.05,
"district_avg": 54.6,
"diff_district": 6.45,
"rank_in_district": 3,
"rank_in_city": 55,
"level": 4,
"level_description": "校本化综合素质评价体系均有建设和使用,有平台支持且评价结果表达科学",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 3,
"水平4": 5
}
},
"实践活动评估": {
"parent_dimension": "教育质量评估力",
"score": 62.07,
"district_avg": 54.72,
"diff_district": 7.35,
"rank_in_district": 3,
"rank_in_city": 35,
"level": 4,
"level_description": "研究性学习、社会考察和学科实践活动的校本化评价工具已研制并使用",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 1,
"水平4": 7
}
},
"区域推进": {
"parent_dimension": "教育条件保障力",
"score": 53.13,
"district_avg": 50.78,
"diff_district": 2.35,
"rank_in_district": 6,
"rank_in_city": 142,
"level": 3,
"level_description": "召开会议平均一个月1次及以上,区域管理文件3个以上,措施达3项",
"level_distribution": {
"水平1": 3,
"水平2": 2,
"水平3": 4,
"水平4": 2
}
},
"环境支持": {
"parent_dimension": "教育条件保障力",
"score": 48.37,
"district_avg": 48.38,
"diff_district": -0.01,
"rank_in_district": 6,
"rank_in_city": 153,
"level": 2,
"level_description": "信息化支持和硬件支持至少有一项建设较好",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 2,
"水平4": 3
}
},
"资源支持": {
"parent_dimension": "教育条件保障力",
"score": 53.54,
"district_avg": 52.18,
"diff_district": 1.36,
"rank_in_district": 6,
"rank_in_city": 103,
"level": 3,
"level_description": "校内外资源至少有一项供给较好,师资水平较好",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 6,
"水平4": 2
}
},
"教学方式创新": {
"parent_dimension": "数字化赋能力",
"score": 54.14,
"district_avg": 50.71,
"diff_district": 3.43,
"rank_in_district": 1,
"rank_in_city": 38,
"level": 4,
"level_description": "信息技术与教学融合程度高,教师能常态化使用信息技术",
"level_distribution": {
"水平1": 0,
"水平2": 1,
"水平3": 9,
"水平4": 1
}
},
"评价精准化与个性化": {
"parent_dimension": "数字化赋能力",
"score": 50.72,
"district_avg": 50.8,
"diff_district": -0.08,
"rank_in_district": 8,
"rank_in_city": 138,
"level": 3,
"level_description": "借助第三方平台支持学科诊断与综合素质评价",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 8,
"水平4": 0
}
},
"课程迭代优化": {
"parent_dimension": "数字化赋能力",
"score": 33.54,
"district_avg": 52.55,
"diff_district": -19.0,
"rank_in_district": 11,
"rank_in_city": 250,
"level": 1,
"level_description": "学校几乎不开展数字化转型活动,尚未建成信息化管理系统",
"level_distribution": {
"水平1": 3,
"水平2": 1,
"水平3": 1,
"水平4": 6
}
}
},
"correlation": {
"课程领导力": {
"课程领导力": 1.0,
"教学变革力": 0.672,
"学生发展指导力": 0.6348,
"教师发展支持力": 0.3326,
"教育质量评估力": 0.7864,
"教育条件保障力": 0.0945,
"数字化赋能力": -0.26
},
"教学变革力": {
"课程领导力": 0.672,
"教学变革力": 1.0,
"学生发展指导力": 0.6986,
"教师发展支持力": -0.0989,
"教育质量评估力": 0.6205,
"教育条件保障力": 0.2865,
"数字化赋能力": -0.2133
},
"学生发展指导力": {
"课程领导力": 0.6348,
"教学变革力": 0.6986,
"学生发展指导力": 1.0,
"教师发展支持力": 0.4532,
"教育质量评估力": 0.5252,
"教育条件保障力": 0.6626,
"数字化赋能力": 0.0931
},
"教师发展支持力": {
"课程领导力": 0.3326,
"教学变革力": -0.0989,
"学生发展指导力": 0.4532,
"教师发展支持力": 1.0,
"教育质量评估力": 0.3441,
"教育条件保障力": 0.4972,
"数字化赋能力": 0.266
},
"教育质量评估力": {
"课程领导力": 0.7864,
"教学变革力": 0.6205,
"学生发展指导力": 0.5252,
"教师发展支持力": 0.3441,
"教育质量评估力": 1.0,
"教育条件保障力": 0.17,
"数字化赋能力": -0.4923
},
"教育条件保障力": {
"课程领导力": 0.0945,
"教学变革力": 0.2865,
"学生发展指导力": 0.6626,
"教师发展支持力": 0.4972,
"教育质量评估力": 0.17,
"教育条件保障力": 1.0,
"数字化赋能力": 0.3431
},
"数字化赋能力": {
"课程领导力": -0.26,
"教学变革力": -0.2133,
"学生发展指导力": 0.0931,
"教师发展支持力": 0.266,
"教育质量评估力": -0.4923,
"教育条件保障力": 0.3431,
"数字化赋能力": 1.0
}
},
"all_schools_dim_scores": {
"课程领导力": {
"上大嘉高": 55.8221,
"上师嘉高": 55.9541,
"中光高中": 50.8827,
"交附嘉分": 48.6695,
"华旭双语": 40.8664,
"嘉一实高": 53.2333,
"嘉定一中": 55.1274,
"嘉定二中": 52.5684,
"安亭高中": 55.9181,
"封浜高中": 51.445,
"远东学校": 53.8526
},
"教学变革力": {
"上大嘉高": 54.0884,
"上师嘉高": 50.8551,
"中光高中": 47.6303,
"交附嘉分": 50.3776,
"华旭双语": 47.0445,
"嘉一实高": 51.6315,
"嘉定一中": 55.2398,
"嘉定二中": 47.6333,
"安亭高中": 52.0058,
"封浜高中": 48.5253,
"远东学校": 53.1962
},
"学生发展指导力": {
"上大嘉高": 53.2948,
"上师嘉高": 50.4215,
"中光高中": 47.9962,
"交附嘉分": 53.7047,
"华旭双语": 44.5019,
"嘉一实高": 52.3558,
"嘉定一中": 58.2771,
"嘉定二中": 52.1477,
"安亭高中": 51.6517,
"封浜高中": 51.4117,
"远东学校": 49.9078
},
"教师发展支持力": {
"上大嘉高": 51.045,
"上师嘉高": 48.0635,
"中光高中": 49.5901,
"交附嘉分": 51.2765,
"华旭双语": 43.9048,
"嘉一实高": 46.4977,
"嘉定一中": 54.4372,
"嘉定二中": 66.1305,
"安亭高中": 55.5971,
"封浜高中": 54.3137,
"远东学校": 46.9749
},
"教育质量评估力": {
"上大嘉高": 58.0141,
"上师嘉高": 57.859,
"中光高中": 48.4617,
"交附嘉分": 46.0685,
"华旭双语": 45.3775,
"嘉一实高": 51.2123,
"嘉定一中": 60.9451,
"嘉定二中": 55.5835,
"安亭高中": 56.6083,
"封浜高中": 58.437,
"远东学校": 59.7467
},
"教育条件保障力": {
"上大嘉高": 51.6818,
"上师嘉高": 45.0946,
"中光高中": 33.5063,
"交附嘉分": 57.0443,
"华旭双语": 48.7779,
"嘉一实高": 53.7194,
"嘉定一中": 59.6613,
"嘉定二中": 57.439,
"安亭高中": 53.8666,
"封浜高中": 52.2734,
"远东学校": 41.8456
},
"数字化赋能力": {
"上大嘉高": 46.1352,
"上师嘉高": 46.0825,
"中光高中": 52.3909,
"交附嘉分": 54.6031,
"华旭双语": 51.9656,
"嘉一实高": 54.8404,
"嘉定一中": 52.275,
"嘉定二中": 54.0275,
"安亭高中": 55.0284,
"封浜高中": 48.4166,
"远东学校": 49.1184
},
"总体得分": {
"上大嘉高": 52.8688,
"上师嘉高": 50.6186,
"中光高中": 47.2083,
"交附嘉分": 51.6777,
"华旭双语": 46.0627,
"嘉一实高": 51.9272,
"嘉定一中": 56.5661,
"嘉定二中": 55.0757,
"安亭高中": 54.3823,
"封浜高中": 52.1175,
"远东学校": 50.6632
}
},
"all_schools_sub_scores": {
"国家标准遵循": {
"上大嘉高": 58.6348,
"上师嘉高": 53.8003,
"中光高中": 52.8238,
"交附嘉分": 53.715,
"华旭双语": 29.7395,
"嘉一实高": 29.7395,
"嘉定一中": 52.402,
"嘉定二中": 43.6837,
"安亭高中": 55.8095,
"封浜高中": 45.2298,
"远东学校": 55.1366
},
"课程结构建设": {
"上大嘉高": 50.4521,
"上师嘉高": 55.6826,
"中光高中": 49.6795,
"交附嘉分": 45.8565,
"华旭双语": 45.5415,
"嘉一实高": 71.5809,
"嘉定一中": 55.7386,
"嘉定二中": 55.6422,
"安亭高中": 53.5653,
"封浜高中": 50.7256,
"远东学校": 48.0416
},
"课程规范落实": {
"上大嘉高": 58.3794,
"上师嘉高": 58.3794,
"中光高中": 50.1448,
"交附嘉分": 46.437,
"华旭双语": 47.3182,
"嘉一实高": 58.3794,
"嘉定一中": 57.2416,
"嘉定二中": 58.3794,
"安亭高中": 58.3794,
"封浜高中": 58.3794,
"远东学校": 58.3794
},
"教学方式变革": {
"上大嘉高": 53.1306,
"上师嘉高": 51.3148,
"中光高中": 46.7735,
"交附嘉分": 51.3598,
"华旭双语": 47.5365,
"嘉一实高": 51.8384,
"嘉定一中": 49.421,
"嘉定二中": 47.9311,
"安亭高中": 50.8481,
"封浜高中": 47.8703,
"远东学校": 49.1541
},
"作业设计与管理变革": {
"上大嘉高": 55.0463,
"上师嘉高": 50.3954,
"中光高中": 48.4872,
"交附嘉分": 49.3953,
"华旭双语": 46.5525,
"嘉一实高": 51.4246,
"嘉定一中": 61.0586,
"嘉定二中": 47.3355,
"安亭高中": 53.1636,
"封浜高中": 49.1803,
"远东学校": 57.2383
},
"学科发展的个性化辅导": {
"上大嘉高": 53.8015,
"上师嘉高": 53.9687,
"中光高中": 53.004,
"交附嘉分": 51.6517,
"华旭双语": 42.6447,
"嘉一实高": 55.5789,
"嘉定一中": 55.9948,
"嘉定二中": 51.5074,
"安亭高中": 50.5154,
"封浜高中": 50.0354,
"远东学校": 49.0558
},
"学生生涯发展指导": {
"上大嘉高": 52.788,
"上师嘉高": 46.8742,
"中光高中": 42.9885,
"交附嘉分": 55.7578,
"华旭双语": 46.3592,
"嘉一实高": 49.1327,
"嘉定一中": 60.5594,
"嘉定二中": 52.788,
"安亭高中": 52.788,
"封浜高中": 52.788,
"远东学校": 50.7599
},
"培训支持": {
"上大嘉高": 49.3282,
"上师嘉高": 49.2221,
"中光高中": 47.9852,
"交附嘉分": 50.4372,
"华旭双语": 48.0083,
"嘉一实高": 47.3989,
"嘉定一中": 54.6069,
"嘉定二中": 49.3731,
"安亭高中": 49.383,
"封浜高中": 50.4137,
"远东学校": 47.7772
},
"教研支持": {
"上大嘉高": 53.4863,
"上师嘉高": 49.1602,
"中光高中": 53.5168,
"交附嘉分": 55.4284,
"华旭双语": 42.2685,
"嘉一实高": 50.6564,
"嘉定一中": 53.8884,
"嘉定二中": 50.1347,
"安亭高中": 51.2671,
"封浜高中": 53.9753,
"远东学校": 50.9813
},
"项目支持": {
"上大嘉高": 50.3206,
"上师嘉高": 45.8082,
"中光高中": 47.2683,
"交附嘉分": 47.964,
"华旭双语": 41.4377,
"嘉一实高": 41.4377,
"嘉定一中": 54.8164,
"嘉定二中": 98.8836,
"安亭高中": 66.1413,
"封浜高中": 58.552,
"远东学校": 42.1661
},
"科学评价观": {
"上大嘉高": 56.5913,
"上师嘉高": 56.3756,
"中光高中": 50.8894,
"交附嘉分": 51.0055,
"华旭双语": 46.09,
"嘉一实高": 59.388,
"嘉定一中": 60.1653,
"嘉定二中": 55.9226,
"安亭高中": 60.8951,
"封浜高中": 53.0,
"远东学校": 50.8434
},
"学业质量评估": {
"上大嘉高": 52.3459,
"上师嘉高": 56.2348,
"中光高中": 41.1032,
"交附嘉分": 48.9711,
"华旭双语": 42.8051,
"嘉一实高": 52.5669,
"嘉定一中": 64.6782,
"嘉定二中": 45.532,
"安亭高中": 63.9947,
"封浜高中": 56.516,
"远东学校": 64.8695
},
"综合素质评估": {
"上大嘉高": 61.0498,
"上师嘉高": 61.0498,
"中光高中": 54.4457,
"交附嘉分": 41.7541,
"华旭双语": 50.433,
"嘉一实高": 47.7947,
"嘉定一中": 58.6924,
"嘉定二中": 59.9228,
"安亭高中": 45.9556,
"封浜高中": 61.0498,
"远东学校": 58.4487
},
"实践活动评估": {
"上大嘉高": 62.0694,
"上师嘉高": 57.7758,
"中光高中": 47.4085,
"交附嘉分": 42.5434,
"华旭双语": 42.1819,
"嘉一实高": 45.0998,
"嘉定一中": 60.2447,
"嘉定二中": 60.9566,
"安亭高中": 55.5878,
"封浜高中": 63.1821,
"远东学校": 64.8251
},
"区域推进": {
"上大嘉高": 53.1324,
"上师嘉高": 53.1324,
"中光高中": 38.3577,
"交附嘉分": 53.1324,
"华旭双语": 38.3577,
"嘉一实高": 55.2336,
"嘉定一中": 70.0083,
"嘉定二中": 48.3801,
"安亭高中": 61.5704,
"封浜高中": 48.8968,
"远东学校": 38.3577
},
"环境支持": {
"上大嘉高": 48.3723,
"上师嘉高": 36.2255,
"中光高中": 22.4955,
"交附嘉分": 64.6605,
"华旭双语": 57.43,
"嘉一实高": 52.1748,
"嘉定一中": 45.1475,
"嘉定二中": 64.1637,
"安亭高中": 46.064,
"封浜高中": 51.2805,
"远东学校": 44.199
},
"资源支持": {
"上大嘉高": 53.5406,
"上师嘉高": 45.9257,
"中光高中": 39.6658,
"交附嘉分": 53.3398,
"华旭双语": 50.546,
"嘉一实高": 53.7499,
"嘉定一中": 63.8282,
"嘉定二中": 59.7733,
"安亭高中": 53.9656,
"封浜高中": 56.6429,
"远东学校": 42.98
},
"教学方式创新": {
"上大嘉高": 54.1414,
"上师嘉高": 52.5568,
"中光高中": 49.1384,
"交附嘉分": 51.6319,
"华旭双语": 44.6587,
"嘉一实高": 51.4943,
"嘉定一中": 52.6504,
"嘉定二中": 50.0355,
"安亭高中": 51.2091,
"封浜高中": 50.1269,
"远东学校": 50.1797
},
"评价精准化与个性化": {
"上大嘉高": 50.7195,
"上师嘉高": 49.1516,
"中光高中": 50.7195,
"交附嘉分": 50.8502,
"华旭双语": 56.795,
"嘉一实高": 51.6994,
"嘉定一中": 44.1632,
"嘉定二中": 50.7195,
"安亭高中": 52.5487,
"封浜高中": 51.6994,
"远东学校": 49.7396
},
"课程迭代优化": {
"上大嘉高": 33.5447,
"上师嘉高": 36.5391,
"中光高中": 57.3147,
"交附嘉分": 61.3274,
"华旭双语": 54.4431,
"嘉一实高": 61.3274,
"嘉定一中": 60.0114,
"嘉定二中": 61.3274,
"安亭高中": 61.3274,
"封浜高中": 43.4234,
"远东学校": 47.436
}
},
"district": "嘉定区",
"total_schools_in_district": 11
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,886 @@
{
"school": "嘉一实高",
"school_info": {
"type": "区实验性示范性高中",
"code": "B类学校",
"nature": "公办",
"feature": "城市郊区学校"
},
"overall": {
"score": 51.93,
"district_avg": 51.74,
"same_type_avg": 49.81,
"rank_in_district": 6,
"total_schools": 11,
"rank_in_city": 88,
"total_schools_in_city": 266,
"cluster": "较好",
"school_type": "区实验性示范性高中",
"same_type_count": 66,
"rank_in_same_type": 16
},
"dimensions": {
"课程领导力": {
"score": 53.23,
"district_avg": 52.21,
"same_type_avg": 51.21,
"diff_district": 1.02,
"rank_in_district": 6,
"rank_in_city": 72,
"t_test_vs_district": {
"t": 0.083,
"p": 0.9417,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"教学变革力": {
"score": 51.63,
"district_avg": 50.75,
"same_type_avg": 49.35,
"diff_district": 0.88,
"rank_in_district": 5,
"rank_in_city": 77,
"t_test_vs_district": {
"t": 4.27,
"p": 0.1464,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"学生发展指导力": {
"score": 52.36,
"district_avg": 51.42,
"same_type_avg": 49.71,
"diff_district": 0.93,
"rank_in_district": 4,
"rank_in_city": 84,
"t_test_vs_district": {
"t": 0.289,
"p": 0.8209,
"significant": false,
"n": 2
},
"cluster": "中等"
},
"教师发展支持力": {
"score": 46.5,
"district_avg": 51.62,
"same_type_avg": 49.45,
"diff_district": -5.12,
"rank_in_district": 10,
"rank_in_city": 207,
"t_test_vs_district": {
"t": -1.898,
"p": 0.1981,
"significant": false,
"n": 3
},
"cluster": "待提升"
},
"教育质量评估力": {
"score": 51.21,
"district_avg": 54.39,
"same_type_avg": 49.59,
"diff_district": -3.18,
"rank_in_district": 8,
"rank_in_city": 120,
"t_test_vs_district": {
"t": -1.015,
"p": 0.3847,
"significant": false,
"n": 4
},
"cluster": "中等"
},
"教育条件保障力": {
"score": 53.72,
"district_avg": 50.45,
"same_type_avg": 49.76,
"diff_district": 3.27,
"rank_in_district": 5,
"rank_in_city": 80,
"t_test_vs_district": {
"t": 3.706,
"p": 0.0657,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"数字化赋能力": {
"score": 54.84,
"district_avg": 51.35,
"same_type_avg": 49.57,
"diff_district": 3.49,
"rank_in_district": 2,
"rank_in_city": 25,
"t_test_vs_district": {
"t": 1.075,
"p": 0.3948,
"significant": false,
"n": 3
},
"cluster": "待提升"
}
},
"sub_dimensions": {
"国家标准遵循": {
"parent_dimension": "课程领导力",
"score": 29.74,
"district_avg": 48.25,
"diff_district": -18.51,
"rank_in_district": 11,
"rank_in_city": 266,
"level": 1,
"level_description": "必修未能开齐开足,选必和选修均不满足要求",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 4,
"水平4": 1
}
},
"课程结构建设": {
"parent_dimension": "课程领导力",
"score": 71.58,
"district_avg": 52.96,
"diff_district": 18.63,
"rank_in_district": 1,
"rank_in_city": 1,
"level": 4,
"level_description": "学科类必修课程离差总和在30%以内,总体三类课程在150%以下,校本课程和综合实践较好",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 4,
"水平4": 4
}
},
"课程规范落实": {
"parent_dimension": "课程领导力",
"score": 58.38,
"district_avg": 55.44,
"diff_district": 2.94,
"rank_in_district": 7,
"rank_in_city": 69,
"level": 4,
"level_description": "都有建设规范文本和档案记录",
"level_distribution": {
"水平1": 0,
"水平2": 2,
"水平3": 2,
"水平4": 7
}
},
"教学方式变革": {
"parent_dimension": "教学变革力",
"score": 51.84,
"district_avg": 49.74,
"diff_district": 2.09,
"rank_in_district": 2,
"rank_in_city": 83,
"level": 3,
"level_description": "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 7,
"水平4": 0
}
},
"作业设计与管理变革": {
"parent_dimension": "教学变革力",
"score": 51.42,
"district_avg": 51.75,
"diff_district": -0.33,
"rank_in_district": 5,
"rank_in_city": 80,
"level": 3,
"level_description": "在每类创新性作业中至少掌握2种类型,偶有时长控制管理,面批为主,有至少两种属性标注",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 5,
"水平4": 3
}
},
"学科发展的个性化辅导": {
"parent_dimension": "学生发展指导力",
"score": 55.58,
"district_avg": 51.61,
"diff_district": 3.96,
"rank_in_district": 2,
"rank_in_city": 19,
"level": 3,
"level_description": "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 9,
"水平4": 0
}
},
"学生生涯发展指导": {
"parent_dimension": "学生发展指导力",
"score": 49.13,
"district_avg": 51.23,
"diff_district": -2.1,
"rank_in_district": 8,
"rank_in_city": 156,
"level": 2,
"level_description": "与社会考察/志愿服务结合实施,三年覆盖50-70%学生,外聘教师,几乎无资源支持",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 6,
"水平4": 1
}
},
"培训支持": {
"parent_dimension": "教师发展支持力",
"score": 47.4,
"district_avg": 49.45,
"diff_district": -2.05,
"rank_in_district": 11,
"rank_in_city": 241,
"level": 1,
"level_description": "各学科教师几乎不进行外出培训",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 6,
"水平4": 1
}
},
"教研支持": {
"parent_dimension": "教师发展支持力",
"score": 50.66,
"district_avg": 51.34,
"diff_district": -0.69,
"rank_in_district": 8,
"rank_in_city": 118,
"level": 3,
"level_description": "有教研活动,质量较好",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 8,
"水平4": 1
}
},
"项目支持": {
"parent_dimension": "教师发展支持力",
"score": 41.44,
"district_avg": 54.07,
"diff_district": -12.63,
"rank_in_district": 11,
"rank_in_city": 266,
"level": 1,
"level_description": "各学科校级及校级以上的项目均没有",
"level_distribution": {
"水平1": 3,
"水平2": 3,
"水平3": 2,
"水平4": 3
}
},
"科学评价观": {
"parent_dimension": "教育质量评估力",
"score": 59.39,
"district_avg": 54.65,
"diff_district": 4.74,
"rank_in_district": 3,
"rank_in_city": 34,
"level": 4,
"level_description": "在所有方面均能至少关注两项素养发展",
"level_distribution": {
"水平1": 0,
"水平2": 1,
"水平3": 7,
"水平4": 3
}
},
"学业质量评估": {
"parent_dimension": "教育质量评估力",
"score": 52.57,
"district_avg": 53.6,
"diff_district": -1.03,
"rank_in_district": 6,
"rank_in_city": 88,
"level": 3,
"level_description": "校本化评价工具已研制并使用,学期考试质量分析不做硬性要求",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 3,
"水平4": 5
}
},
"综合素质评估": {
"parent_dimension": "教育质量评估力",
"score": 47.79,
"district_avg": 54.6,
"diff_district": -6.8,
"rank_in_district": 9,
"rank_in_city": 149,
"level": 2,
"level_description": "校本化综合素质评价方案建成,但具体评价工具有待开发",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 3,
"水平4": 5
}
},
"实践活动评估": {
"parent_dimension": "教育质量评估力",
"score": 45.1,
"district_avg": 54.72,
"diff_district": -9.62,
"rank_in_district": 9,
"rank_in_city": 170,
"level": 2,
"level_description": "学科实践活动的校本化评价工具已研制",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 1,
"水平4": 7
}
},
"区域推进": {
"parent_dimension": "教育条件保障力",
"score": 55.23,
"district_avg": 50.78,
"diff_district": 4.46,
"rank_in_district": 3,
"rank_in_city": 90,
"level": 3,
"level_description": "召开会议平均一个月1次及以上,区域管理文件3个以上,措施达3项",
"level_distribution": {
"水平1": 3,
"水平2": 2,
"水平3": 4,
"水平4": 2
}
},
"环境支持": {
"parent_dimension": "教育条件保障力",
"score": 52.17,
"district_avg": 48.38,
"diff_district": 3.79,
"rank_in_district": 4,
"rank_in_city": 96,
"level": 3,
"level_description": "信息化支持和硬件在平均水平附近",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 2,
"水平4": 3
}
},
"资源支持": {
"parent_dimension": "教育条件保障力",
"score": 53.75,
"district_avg": 52.18,
"diff_district": 1.57,
"rank_in_district": 5,
"rank_in_city": 101,
"level": 3,
"level_description": "校内外资源至少有一项供给较好,师资水平较好",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 6,
"水平4": 2
}
},
"教学方式创新": {
"parent_dimension": "数字化赋能力",
"score": 51.49,
"district_avg": 50.71,
"diff_district": 0.78,
"rank_in_district": 5,
"rank_in_city": 99,
"level": 3,
"level_description": "信息技术与教学融合程度较高,教师能使用信息技术",
"level_distribution": {
"水平1": 0,
"水平2": 1,
"水平3": 9,
"水平4": 1
}
},
"评价精准化与个性化": {
"parent_dimension": "数字化赋能力",
"score": 51.7,
"district_avg": 50.8,
"diff_district": 0.9,
"rank_in_district": 3,
"rank_in_city": 51,
"level": 3,
"level_description": "借助第三方平台支持学科诊断与综合素质评价",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 8,
"水平4": 0
}
},
"课程迭代优化": {
"parent_dimension": "数字化赋能力",
"score": 61.33,
"district_avg": 52.55,
"diff_district": 8.78,
"rank_in_district": 4,
"rank_in_city": 18,
"level": 4,
"level_description": "学校对促进教学数字化转型有专项研修计划并开展相关活动,业务绝大部分使用信息化系统",
"level_distribution": {
"水平1": 3,
"水平2": 1,
"水平3": 1,
"水平4": 6
}
}
},
"correlation": {
"课程领导力": {
"课程领导力": 1.0,
"教学变革力": 0.672,
"学生发展指导力": 0.6348,
"教师发展支持力": 0.3326,
"教育质量评估力": 0.7864,
"教育条件保障力": 0.0945,
"数字化赋能力": -0.26
},
"教学变革力": {
"课程领导力": 0.672,
"教学变革力": 1.0,
"学生发展指导力": 0.6986,
"教师发展支持力": -0.0989,
"教育质量评估力": 0.6205,
"教育条件保障力": 0.2865,
"数字化赋能力": -0.2133
},
"学生发展指导力": {
"课程领导力": 0.6348,
"教学变革力": 0.6986,
"学生发展指导力": 1.0,
"教师发展支持力": 0.4532,
"教育质量评估力": 0.5252,
"教育条件保障力": 0.6626,
"数字化赋能力": 0.0931
},
"教师发展支持力": {
"课程领导力": 0.3326,
"教学变革力": -0.0989,
"学生发展指导力": 0.4532,
"教师发展支持力": 1.0,
"教育质量评估力": 0.3441,
"教育条件保障力": 0.4972,
"数字化赋能力": 0.266
},
"教育质量评估力": {
"课程领导力": 0.7864,
"教学变革力": 0.6205,
"学生发展指导力": 0.5252,
"教师发展支持力": 0.3441,
"教育质量评估力": 1.0,
"教育条件保障力": 0.17,
"数字化赋能力": -0.4923
},
"教育条件保障力": {
"课程领导力": 0.0945,
"教学变革力": 0.2865,
"学生发展指导力": 0.6626,
"教师发展支持力": 0.4972,
"教育质量评估力": 0.17,
"教育条件保障力": 1.0,
"数字化赋能力": 0.3431
},
"数字化赋能力": {
"课程领导力": -0.26,
"教学变革力": -0.2133,
"学生发展指导力": 0.0931,
"教师发展支持力": 0.266,
"教育质量评估力": -0.4923,
"教育条件保障力": 0.3431,
"数字化赋能力": 1.0
}
},
"all_schools_dim_scores": {
"课程领导力": {
"上大嘉高": 55.8221,
"上师嘉高": 55.9541,
"中光高中": 50.8827,
"交附嘉分": 48.6695,
"华旭双语": 40.8664,
"嘉一实高": 53.2333,
"嘉定一中": 55.1274,
"嘉定二中": 52.5684,
"安亭高中": 55.9181,
"封浜高中": 51.445,
"远东学校": 53.8526
},
"教学变革力": {
"上大嘉高": 54.0884,
"上师嘉高": 50.8551,
"中光高中": 47.6303,
"交附嘉分": 50.3776,
"华旭双语": 47.0445,
"嘉一实高": 51.6315,
"嘉定一中": 55.2398,
"嘉定二中": 47.6333,
"安亭高中": 52.0058,
"封浜高中": 48.5253,
"远东学校": 53.1962
},
"学生发展指导力": {
"上大嘉高": 53.2948,
"上师嘉高": 50.4215,
"中光高中": 47.9962,
"交附嘉分": 53.7047,
"华旭双语": 44.5019,
"嘉一实高": 52.3558,
"嘉定一中": 58.2771,
"嘉定二中": 52.1477,
"安亭高中": 51.6517,
"封浜高中": 51.4117,
"远东学校": 49.9078
},
"教师发展支持力": {
"上大嘉高": 51.045,
"上师嘉高": 48.0635,
"中光高中": 49.5901,
"交附嘉分": 51.2765,
"华旭双语": 43.9048,
"嘉一实高": 46.4977,
"嘉定一中": 54.4372,
"嘉定二中": 66.1305,
"安亭高中": 55.5971,
"封浜高中": 54.3137,
"远东学校": 46.9749
},
"教育质量评估力": {
"上大嘉高": 58.0141,
"上师嘉高": 57.859,
"中光高中": 48.4617,
"交附嘉分": 46.0685,
"华旭双语": 45.3775,
"嘉一实高": 51.2123,
"嘉定一中": 60.9451,
"嘉定二中": 55.5835,
"安亭高中": 56.6083,
"封浜高中": 58.437,
"远东学校": 59.7467
},
"教育条件保障力": {
"上大嘉高": 51.6818,
"上师嘉高": 45.0946,
"中光高中": 33.5063,
"交附嘉分": 57.0443,
"华旭双语": 48.7779,
"嘉一实高": 53.7194,
"嘉定一中": 59.6613,
"嘉定二中": 57.439,
"安亭高中": 53.8666,
"封浜高中": 52.2734,
"远东学校": 41.8456
},
"数字化赋能力": {
"上大嘉高": 46.1352,
"上师嘉高": 46.0825,
"中光高中": 52.3909,
"交附嘉分": 54.6031,
"华旭双语": 51.9656,
"嘉一实高": 54.8404,
"嘉定一中": 52.275,
"嘉定二中": 54.0275,
"安亭高中": 55.0284,
"封浜高中": 48.4166,
"远东学校": 49.1184
},
"总体得分": {
"上大嘉高": 52.8688,
"上师嘉高": 50.6186,
"中光高中": 47.2083,
"交附嘉分": 51.6777,
"华旭双语": 46.0627,
"嘉一实高": 51.9272,
"嘉定一中": 56.5661,
"嘉定二中": 55.0757,
"安亭高中": 54.3823,
"封浜高中": 52.1175,
"远东学校": 50.6632
}
},
"all_schools_sub_scores": {
"国家标准遵循": {
"上大嘉高": 58.6348,
"上师嘉高": 53.8003,
"中光高中": 52.8238,
"交附嘉分": 53.715,
"华旭双语": 29.7395,
"嘉一实高": 29.7395,
"嘉定一中": 52.402,
"嘉定二中": 43.6837,
"安亭高中": 55.8095,
"封浜高中": 45.2298,
"远东学校": 55.1366
},
"课程结构建设": {
"上大嘉高": 50.4521,
"上师嘉高": 55.6826,
"中光高中": 49.6795,
"交附嘉分": 45.8565,
"华旭双语": 45.5415,
"嘉一实高": 71.5809,
"嘉定一中": 55.7386,
"嘉定二中": 55.6422,
"安亭高中": 53.5653,
"封浜高中": 50.7256,
"远东学校": 48.0416
},
"课程规范落实": {
"上大嘉高": 58.3794,
"上师嘉高": 58.3794,
"中光高中": 50.1448,
"交附嘉分": 46.437,
"华旭双语": 47.3182,
"嘉一实高": 58.3794,
"嘉定一中": 57.2416,
"嘉定二中": 58.3794,
"安亭高中": 58.3794,
"封浜高中": 58.3794,
"远东学校": 58.3794
},
"教学方式变革": {
"上大嘉高": 53.1306,
"上师嘉高": 51.3148,
"中光高中": 46.7735,
"交附嘉分": 51.3598,
"华旭双语": 47.5365,
"嘉一实高": 51.8384,
"嘉定一中": 49.421,
"嘉定二中": 47.9311,
"安亭高中": 50.8481,
"封浜高中": 47.8703,
"远东学校": 49.1541
},
"作业设计与管理变革": {
"上大嘉高": 55.0463,
"上师嘉高": 50.3954,
"中光高中": 48.4872,
"交附嘉分": 49.3953,
"华旭双语": 46.5525,
"嘉一实高": 51.4246,
"嘉定一中": 61.0586,
"嘉定二中": 47.3355,
"安亭高中": 53.1636,
"封浜高中": 49.1803,
"远东学校": 57.2383
},
"学科发展的个性化辅导": {
"上大嘉高": 53.8015,
"上师嘉高": 53.9687,
"中光高中": 53.004,
"交附嘉分": 51.6517,
"华旭双语": 42.6447,
"嘉一实高": 55.5789,
"嘉定一中": 55.9948,
"嘉定二中": 51.5074,
"安亭高中": 50.5154,
"封浜高中": 50.0354,
"远东学校": 49.0558
},
"学生生涯发展指导": {
"上大嘉高": 52.788,
"上师嘉高": 46.8742,
"中光高中": 42.9885,
"交附嘉分": 55.7578,
"华旭双语": 46.3592,
"嘉一实高": 49.1327,
"嘉定一中": 60.5594,
"嘉定二中": 52.788,
"安亭高中": 52.788,
"封浜高中": 52.788,
"远东学校": 50.7599
},
"培训支持": {
"上大嘉高": 49.3282,
"上师嘉高": 49.2221,
"中光高中": 47.9852,
"交附嘉分": 50.4372,
"华旭双语": 48.0083,
"嘉一实高": 47.3989,
"嘉定一中": 54.6069,
"嘉定二中": 49.3731,
"安亭高中": 49.383,
"封浜高中": 50.4137,
"远东学校": 47.7772
},
"教研支持": {
"上大嘉高": 53.4863,
"上师嘉高": 49.1602,
"中光高中": 53.5168,
"交附嘉分": 55.4284,
"华旭双语": 42.2685,
"嘉一实高": 50.6564,
"嘉定一中": 53.8884,
"嘉定二中": 50.1347,
"安亭高中": 51.2671,
"封浜高中": 53.9753,
"远东学校": 50.9813
},
"项目支持": {
"上大嘉高": 50.3206,
"上师嘉高": 45.8082,
"中光高中": 47.2683,
"交附嘉分": 47.964,
"华旭双语": 41.4377,
"嘉一实高": 41.4377,
"嘉定一中": 54.8164,
"嘉定二中": 98.8836,
"安亭高中": 66.1413,
"封浜高中": 58.552,
"远东学校": 42.1661
},
"科学评价观": {
"上大嘉高": 56.5913,
"上师嘉高": 56.3756,
"中光高中": 50.8894,
"交附嘉分": 51.0055,
"华旭双语": 46.09,
"嘉一实高": 59.388,
"嘉定一中": 60.1653,
"嘉定二中": 55.9226,
"安亭高中": 60.8951,
"封浜高中": 53.0,
"远东学校": 50.8434
},
"学业质量评估": {
"上大嘉高": 52.3459,
"上师嘉高": 56.2348,
"中光高中": 41.1032,
"交附嘉分": 48.9711,
"华旭双语": 42.8051,
"嘉一实高": 52.5669,
"嘉定一中": 64.6782,
"嘉定二中": 45.532,
"安亭高中": 63.9947,
"封浜高中": 56.516,
"远东学校": 64.8695
},
"综合素质评估": {
"上大嘉高": 61.0498,
"上师嘉高": 61.0498,
"中光高中": 54.4457,
"交附嘉分": 41.7541,
"华旭双语": 50.433,
"嘉一实高": 47.7947,
"嘉定一中": 58.6924,
"嘉定二中": 59.9228,
"安亭高中": 45.9556,
"封浜高中": 61.0498,
"远东学校": 58.4487
},
"实践活动评估": {
"上大嘉高": 62.0694,
"上师嘉高": 57.7758,
"中光高中": 47.4085,
"交附嘉分": 42.5434,
"华旭双语": 42.1819,
"嘉一实高": 45.0998,
"嘉定一中": 60.2447,
"嘉定二中": 60.9566,
"安亭高中": 55.5878,
"封浜高中": 63.1821,
"远东学校": 64.8251
},
"区域推进": {
"上大嘉高": 53.1324,
"上师嘉高": 53.1324,
"中光高中": 38.3577,
"交附嘉分": 53.1324,
"华旭双语": 38.3577,
"嘉一实高": 55.2336,
"嘉定一中": 70.0083,
"嘉定二中": 48.3801,
"安亭高中": 61.5704,
"封浜高中": 48.8968,
"远东学校": 38.3577
},
"环境支持": {
"上大嘉高": 48.3723,
"上师嘉高": 36.2255,
"中光高中": 22.4955,
"交附嘉分": 64.6605,
"华旭双语": 57.43,
"嘉一实高": 52.1748,
"嘉定一中": 45.1475,
"嘉定二中": 64.1637,
"安亭高中": 46.064,
"封浜高中": 51.2805,
"远东学校": 44.199
},
"资源支持": {
"上大嘉高": 53.5406,
"上师嘉高": 45.9257,
"中光高中": 39.6658,
"交附嘉分": 53.3398,
"华旭双语": 50.546,
"嘉一实高": 53.7499,
"嘉定一中": 63.8282,
"嘉定二中": 59.7733,
"安亭高中": 53.9656,
"封浜高中": 56.6429,
"远东学校": 42.98
},
"教学方式创新": {
"上大嘉高": 54.1414,
"上师嘉高": 52.5568,
"中光高中": 49.1384,
"交附嘉分": 51.6319,
"华旭双语": 44.6587,
"嘉一实高": 51.4943,
"嘉定一中": 52.6504,
"嘉定二中": 50.0355,
"安亭高中": 51.2091,
"封浜高中": 50.1269,
"远东学校": 50.1797
},
"评价精准化与个性化": {
"上大嘉高": 50.7195,
"上师嘉高": 49.1516,
"中光高中": 50.7195,
"交附嘉分": 50.8502,
"华旭双语": 56.795,
"嘉一实高": 51.6994,
"嘉定一中": 44.1632,
"嘉定二中": 50.7195,
"安亭高中": 52.5487,
"封浜高中": 51.6994,
"远东学校": 49.7396
},
"课程迭代优化": {
"上大嘉高": 33.5447,
"上师嘉高": 36.5391,
"中光高中": 57.3147,
"交附嘉分": 61.3274,
"华旭双语": 54.4431,
"嘉一实高": 61.3274,
"嘉定一中": 60.0114,
"嘉定二中": 61.3274,
"安亭高中": 61.3274,
"封浜高中": 43.4234,
"远东学校": 47.436
}
},
"district": "嘉定区",
"total_schools_in_district": 11
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,886 @@
{
"school": "嘉定一中",
"school_info": {
"type": "市实验性示范性高中",
"code": "A类学校",
"nature": "公办",
"feature": "城市郊区学校"
},
"overall": {
"score": 56.57,
"district_avg": 51.74,
"same_type_avg": 52.47,
"rank_in_district": 1,
"total_schools": 11,
"rank_in_city": 7,
"total_schools_in_city": 266,
"cluster": "较好",
"school_type": "市实验性示范性高中",
"same_type_count": 74,
"rank_in_same_type": 5
},
"dimensions": {
"课程领导力": {
"score": 55.13,
"district_avg": 52.21,
"same_type_avg": 51.95,
"diff_district": 2.91,
"rank_in_district": 4,
"rank_in_city": 31,
"t_test_vs_district": {
"t": 2.038,
"p": 0.1784,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"教学变革力": {
"score": 55.24,
"district_avg": 50.75,
"same_type_avg": 51.72,
"diff_district": 4.49,
"rank_in_district": 1,
"rank_in_city": 22,
"t_test_vs_district": {
"t": 0.772,
"p": 0.5815,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"学生发展指导力": {
"score": 58.28,
"district_avg": 51.42,
"same_type_avg": 51.45,
"diff_district": 6.85,
"rank_in_district": 1,
"rank_in_city": 4,
"t_test_vs_district": {
"t": 3.002,
"p": 0.2047,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"教师发展支持力": {
"score": 54.44,
"district_avg": 51.62,
"same_type_avg": 53.88,
"diff_district": 2.82,
"rank_in_district": 3,
"rank_in_city": 40,
"t_test_vs_district": {
"t": 10.022,
"p": 0.0098,
"significant": true,
"n": 3
},
"cluster": "中等"
},
"教育质量评估力": {
"score": 60.95,
"district_avg": 54.39,
"same_type_avg": 53.42,
"diff_district": 6.55,
"rank_in_district": 1,
"rank_in_city": 25,
"t_test_vs_district": {
"t": 5.062,
"p": 0.0149,
"significant": true,
"n": 4
},
"cluster": "较好"
},
"教育条件保障力": {
"score": 59.66,
"district_avg": 50.45,
"same_type_avg": 53.07,
"diff_district": 9.21,
"rank_in_district": 1,
"rank_in_city": 10,
"t_test_vs_district": {
"t": 1.233,
"p": 0.3428,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"数字化赋能力": {
"score": 52.28,
"district_avg": 51.35,
"same_type_avg": 51.81,
"diff_district": 0.92,
"rank_in_district": 6,
"rank_in_city": 98,
"t_test_vs_district": {
"t": 0.201,
"p": 0.859,
"significant": false,
"n": 3
},
"cluster": "待提升"
}
},
"sub_dimensions": {
"国家标准遵循": {
"parent_dimension": "课程领导力",
"score": 52.4,
"district_avg": 48.25,
"diff_district": 4.16,
"rank_in_district": 7,
"rank_in_city": 152,
"level": 2,
"level_description": "必修未能开齐开足,选必和选修有一个满足要求",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 4,
"水平4": 1
}
},
"课程结构建设": {
"parent_dimension": "课程领导力",
"score": 55.74,
"district_avg": 52.96,
"diff_district": 2.78,
"rank_in_district": 2,
"rank_in_city": 31,
"level": 4,
"level_description": "学科类必修课程离差总和在30%以内,总体三类课程在150%以下,校本课程和综合实践较好",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 4,
"水平4": 4
}
},
"课程规范落实": {
"parent_dimension": "课程领导力",
"score": 57.24,
"district_avg": 55.44,
"diff_district": 1.81,
"rank_in_district": 8,
"rank_in_city": 74,
"level": 3,
"level_description": "都有建设规范文本,但过程性档案记录已经全部建成,部分有尚未使用",
"level_distribution": {
"水平1": 0,
"水平2": 2,
"水平3": 2,
"水平4": 7
}
},
"教学方式变革": {
"parent_dimension": "教学变革力",
"score": 49.42,
"district_avg": 49.74,
"diff_district": -0.32,
"rank_in_district": 6,
"rank_in_city": 153,
"level": 3,
"level_description": "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 7,
"水平4": 0
}
},
"作业设计与管理变革": {
"parent_dimension": "教学变革力",
"score": 61.06,
"district_avg": 51.75,
"diff_district": 9.31,
"rank_in_district": 1,
"rank_in_city": 4,
"level": 4,
"level_description": "在每类创新性作业中至少掌握3种类型,作业时长有统一控制管理,定期批改评价,有多元化属性标注",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 5,
"水平4": 3
}
},
"学科发展的个性化辅导": {
"parent_dimension": "学生发展指导力",
"score": 55.99,
"district_avg": 51.61,
"diff_district": 4.38,
"rank_in_district": 1,
"rank_in_city": 16,
"level": 3,
"level_description": "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 9,
"水平4": 0
}
},
"学生生涯发展指导": {
"parent_dimension": "学生发展指导力",
"score": 60.56,
"district_avg": 51.23,
"diff_district": 9.32,
"rank_in_district": 1,
"rank_in_city": 28,
"level": 4,
"level_description": "专设生涯指导课程,三年覆盖90%+学生,本校+外聘教师队伍,校内外资源足够支持",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 6,
"水平4": 1
}
},
"培训支持": {
"parent_dimension": "教师发展支持力",
"score": 54.61,
"district_avg": 49.45,
"diff_district": 5.16,
"rank_in_district": 1,
"rank_in_city": 11,
"level": 4,
"level_description": "各学科教师平均外出培训人数≥2.5人",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 6,
"水平4": 1
}
},
"教研支持": {
"parent_dimension": "教师发展支持力",
"score": 53.89,
"district_avg": 51.34,
"diff_district": 2.55,
"rank_in_district": 3,
"rank_in_city": 54,
"level": 3,
"level_description": "有教研活动,质量较好",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 8,
"水平4": 1
}
},
"项目支持": {
"parent_dimension": "教师发展支持力",
"score": 54.82,
"district_avg": 54.07,
"diff_district": 0.74,
"rank_in_district": 4,
"rank_in_city": 53,
"level": 3,
"level_description": "有部分学科负责校级以上项目至少一个",
"level_distribution": {
"水平1": 3,
"水平2": 3,
"水平3": 2,
"水平4": 3
}
},
"科学评价观": {
"parent_dimension": "教育质量评估力",
"score": 60.17,
"district_avg": 54.65,
"diff_district": 5.51,
"rank_in_district": 2,
"rank_in_city": 26,
"level": 4,
"level_description": "在所有方面均能至少关注两项素养发展",
"level_distribution": {
"水平1": 0,
"水平2": 1,
"水平3": 7,
"水平4": 3
}
},
"学业质量评估": {
"parent_dimension": "教育质量评估力",
"score": 64.68,
"district_avg": 53.6,
"diff_district": 11.08,
"rank_in_district": 2,
"rank_in_city": 27,
"level": 4,
"level_description": "校本化评价工具已研制并使用,学期考试质量分析并标注属性较为全面",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 3,
"水平4": 5
}
},
"综合素质评估": {
"parent_dimension": "教育质量评估力",
"score": 58.69,
"district_avg": 54.6,
"diff_district": 4.09,
"rank_in_district": 5,
"rank_in_city": 70,
"level": 4,
"level_description": "校本化综合素质评价体系均有建设和使用,有平台支持且评价结果表达科学",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 3,
"水平4": 5
}
},
"实践活动评估": {
"parent_dimension": "教育质量评估力",
"score": 60.24,
"district_avg": 54.72,
"diff_district": 5.53,
"rank_in_district": 5,
"rank_in_city": 55,
"level": 4,
"level_description": "研究性学习、社会考察和学科实践活动的校本化评价工具已研制并使用",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 1,
"水平4": 7
}
},
"区域推进": {
"parent_dimension": "教育条件保障力",
"score": 70.01,
"district_avg": 50.78,
"diff_district": 19.23,
"rank_in_district": 1,
"rank_in_city": 2,
"level": 4,
"level_description": "召开会议平均一个月2次及以上,区域管理文件数量和措施均为最大值",
"level_distribution": {
"水平1": 3,
"水平2": 2,
"水平3": 4,
"水平4": 2
}
},
"环境支持": {
"parent_dimension": "教育条件保障力",
"score": 45.15,
"district_avg": 48.38,
"diff_district": -3.24,
"rank_in_district": 8,
"rank_in_city": 186,
"level": 2,
"level_description": "信息化支持和硬件支持至少有一项建设较好",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 2,
"水平4": 3
}
},
"资源支持": {
"parent_dimension": "教育条件保障力",
"score": 63.83,
"district_avg": 52.18,
"diff_district": 11.65,
"rank_in_district": 1,
"rank_in_city": 6,
"level": 4,
"level_description": "校内外资源和师资水平均处于较高水平",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 6,
"水平4": 2
}
},
"教学方式创新": {
"parent_dimension": "数字化赋能力",
"score": 52.65,
"district_avg": 50.71,
"diff_district": 1.94,
"rank_in_district": 2,
"rank_in_city": 72,
"level": 3,
"level_description": "信息技术与教学融合程度较高,教师能使用信息技术",
"level_distribution": {
"水平1": 0,
"水平2": 1,
"水平3": 9,
"水平4": 1
}
},
"评价精准化与个性化": {
"parent_dimension": "数字化赋能力",
"score": 44.16,
"district_avg": 50.8,
"diff_district": -6.64,
"rank_in_district": 11,
"rank_in_city": 229,
"level": 2,
"level_description": "有信息技术平台支持学业评价",
"level_distribution": {
"水平1": 0,
"水平2": 3,
"水平3": 8,
"水平4": 0
}
},
"课程迭代优化": {
"parent_dimension": "数字化赋能力",
"score": 60.01,
"district_avg": 52.55,
"diff_district": 7.46,
"rank_in_district": 5,
"rank_in_city": 21,
"level": 4,
"level_description": "学校对促进教学数字化转型有专项研修计划并开展相关活动,业务绝大部分使用信息化系统",
"level_distribution": {
"水平1": 3,
"水平2": 1,
"水平3": 1,
"水平4": 6
}
}
},
"correlation": {
"课程领导力": {
"课程领导力": 1.0,
"教学变革力": 0.672,
"学生发展指导力": 0.6348,
"教师发展支持力": 0.3326,
"教育质量评估力": 0.7864,
"教育条件保障力": 0.0945,
"数字化赋能力": -0.26
},
"教学变革力": {
"课程领导力": 0.672,
"教学变革力": 1.0,
"学生发展指导力": 0.6986,
"教师发展支持力": -0.0989,
"教育质量评估力": 0.6205,
"教育条件保障力": 0.2865,
"数字化赋能力": -0.2133
},
"学生发展指导力": {
"课程领导力": 0.6348,
"教学变革力": 0.6986,
"学生发展指导力": 1.0,
"教师发展支持力": 0.4532,
"教育质量评估力": 0.5252,
"教育条件保障力": 0.6626,
"数字化赋能力": 0.0931
},
"教师发展支持力": {
"课程领导力": 0.3326,
"教学变革力": -0.0989,
"学生发展指导力": 0.4532,
"教师发展支持力": 1.0,
"教育质量评估力": 0.3441,
"教育条件保障力": 0.4972,
"数字化赋能力": 0.266
},
"教育质量评估力": {
"课程领导力": 0.7864,
"教学变革力": 0.6205,
"学生发展指导力": 0.5252,
"教师发展支持力": 0.3441,
"教育质量评估力": 1.0,
"教育条件保障力": 0.17,
"数字化赋能力": -0.4923
},
"教育条件保障力": {
"课程领导力": 0.0945,
"教学变革力": 0.2865,
"学生发展指导力": 0.6626,
"教师发展支持力": 0.4972,
"教育质量评估力": 0.17,
"教育条件保障力": 1.0,
"数字化赋能力": 0.3431
},
"数字化赋能力": {
"课程领导力": -0.26,
"教学变革力": -0.2133,
"学生发展指导力": 0.0931,
"教师发展支持力": 0.266,
"教育质量评估力": -0.4923,
"教育条件保障力": 0.3431,
"数字化赋能力": 1.0
}
},
"all_schools_dim_scores": {
"课程领导力": {
"上大嘉高": 55.8221,
"上师嘉高": 55.9541,
"中光高中": 50.8827,
"交附嘉分": 48.6695,
"华旭双语": 40.8664,
"嘉一实高": 53.2333,
"嘉定一中": 55.1274,
"嘉定二中": 52.5684,
"安亭高中": 55.9181,
"封浜高中": 51.445,
"远东学校": 53.8526
},
"教学变革力": {
"上大嘉高": 54.0884,
"上师嘉高": 50.8551,
"中光高中": 47.6303,
"交附嘉分": 50.3776,
"华旭双语": 47.0445,
"嘉一实高": 51.6315,
"嘉定一中": 55.2398,
"嘉定二中": 47.6333,
"安亭高中": 52.0058,
"封浜高中": 48.5253,
"远东学校": 53.1962
},
"学生发展指导力": {
"上大嘉高": 53.2948,
"上师嘉高": 50.4215,
"中光高中": 47.9962,
"交附嘉分": 53.7047,
"华旭双语": 44.5019,
"嘉一实高": 52.3558,
"嘉定一中": 58.2771,
"嘉定二中": 52.1477,
"安亭高中": 51.6517,
"封浜高中": 51.4117,
"远东学校": 49.9078
},
"教师发展支持力": {
"上大嘉高": 51.045,
"上师嘉高": 48.0635,
"中光高中": 49.5901,
"交附嘉分": 51.2765,
"华旭双语": 43.9048,
"嘉一实高": 46.4977,
"嘉定一中": 54.4372,
"嘉定二中": 66.1305,
"安亭高中": 55.5971,
"封浜高中": 54.3137,
"远东学校": 46.9749
},
"教育质量评估力": {
"上大嘉高": 58.0141,
"上师嘉高": 57.859,
"中光高中": 48.4617,
"交附嘉分": 46.0685,
"华旭双语": 45.3775,
"嘉一实高": 51.2123,
"嘉定一中": 60.9451,
"嘉定二中": 55.5835,
"安亭高中": 56.6083,
"封浜高中": 58.437,
"远东学校": 59.7467
},
"教育条件保障力": {
"上大嘉高": 51.6818,
"上师嘉高": 45.0946,
"中光高中": 33.5063,
"交附嘉分": 57.0443,
"华旭双语": 48.7779,
"嘉一实高": 53.7194,
"嘉定一中": 59.6613,
"嘉定二中": 57.439,
"安亭高中": 53.8666,
"封浜高中": 52.2734,
"远东学校": 41.8456
},
"数字化赋能力": {
"上大嘉高": 46.1352,
"上师嘉高": 46.0825,
"中光高中": 52.3909,
"交附嘉分": 54.6031,
"华旭双语": 51.9656,
"嘉一实高": 54.8404,
"嘉定一中": 52.275,
"嘉定二中": 54.0275,
"安亭高中": 55.0284,
"封浜高中": 48.4166,
"远东学校": 49.1184
},
"总体得分": {
"上大嘉高": 52.8688,
"上师嘉高": 50.6186,
"中光高中": 47.2083,
"交附嘉分": 51.6777,
"华旭双语": 46.0627,
"嘉一实高": 51.9272,
"嘉定一中": 56.5661,
"嘉定二中": 55.0757,
"安亭高中": 54.3823,
"封浜高中": 52.1175,
"远东学校": 50.6632
}
},
"all_schools_sub_scores": {
"国家标准遵循": {
"上大嘉高": 58.6348,
"上师嘉高": 53.8003,
"中光高中": 52.8238,
"交附嘉分": 53.715,
"华旭双语": 29.7395,
"嘉一实高": 29.7395,
"嘉定一中": 52.402,
"嘉定二中": 43.6837,
"安亭高中": 55.8095,
"封浜高中": 45.2298,
"远东学校": 55.1366
},
"课程结构建设": {
"上大嘉高": 50.4521,
"上师嘉高": 55.6826,
"中光高中": 49.6795,
"交附嘉分": 45.8565,
"华旭双语": 45.5415,
"嘉一实高": 71.5809,
"嘉定一中": 55.7386,
"嘉定二中": 55.6422,
"安亭高中": 53.5653,
"封浜高中": 50.7256,
"远东学校": 48.0416
},
"课程规范落实": {
"上大嘉高": 58.3794,
"上师嘉高": 58.3794,
"中光高中": 50.1448,
"交附嘉分": 46.437,
"华旭双语": 47.3182,
"嘉一实高": 58.3794,
"嘉定一中": 57.2416,
"嘉定二中": 58.3794,
"安亭高中": 58.3794,
"封浜高中": 58.3794,
"远东学校": 58.3794
},
"教学方式变革": {
"上大嘉高": 53.1306,
"上师嘉高": 51.3148,
"中光高中": 46.7735,
"交附嘉分": 51.3598,
"华旭双语": 47.5365,
"嘉一实高": 51.8384,
"嘉定一中": 49.421,
"嘉定二中": 47.9311,
"安亭高中": 50.8481,
"封浜高中": 47.8703,
"远东学校": 49.1541
},
"作业设计与管理变革": {
"上大嘉高": 55.0463,
"上师嘉高": 50.3954,
"中光高中": 48.4872,
"交附嘉分": 49.3953,
"华旭双语": 46.5525,
"嘉一实高": 51.4246,
"嘉定一中": 61.0586,
"嘉定二中": 47.3355,
"安亭高中": 53.1636,
"封浜高中": 49.1803,
"远东学校": 57.2383
},
"学科发展的个性化辅导": {
"上大嘉高": 53.8015,
"上师嘉高": 53.9687,
"中光高中": 53.004,
"交附嘉分": 51.6517,
"华旭双语": 42.6447,
"嘉一实高": 55.5789,
"嘉定一中": 55.9948,
"嘉定二中": 51.5074,
"安亭高中": 50.5154,
"封浜高中": 50.0354,
"远东学校": 49.0558
},
"学生生涯发展指导": {
"上大嘉高": 52.788,
"上师嘉高": 46.8742,
"中光高中": 42.9885,
"交附嘉分": 55.7578,
"华旭双语": 46.3592,
"嘉一实高": 49.1327,
"嘉定一中": 60.5594,
"嘉定二中": 52.788,
"安亭高中": 52.788,
"封浜高中": 52.788,
"远东学校": 50.7599
},
"培训支持": {
"上大嘉高": 49.3282,
"上师嘉高": 49.2221,
"中光高中": 47.9852,
"交附嘉分": 50.4372,
"华旭双语": 48.0083,
"嘉一实高": 47.3989,
"嘉定一中": 54.6069,
"嘉定二中": 49.3731,
"安亭高中": 49.383,
"封浜高中": 50.4137,
"远东学校": 47.7772
},
"教研支持": {
"上大嘉高": 53.4863,
"上师嘉高": 49.1602,
"中光高中": 53.5168,
"交附嘉分": 55.4284,
"华旭双语": 42.2685,
"嘉一实高": 50.6564,
"嘉定一中": 53.8884,
"嘉定二中": 50.1347,
"安亭高中": 51.2671,
"封浜高中": 53.9753,
"远东学校": 50.9813
},
"项目支持": {
"上大嘉高": 50.3206,
"上师嘉高": 45.8082,
"中光高中": 47.2683,
"交附嘉分": 47.964,
"华旭双语": 41.4377,
"嘉一实高": 41.4377,
"嘉定一中": 54.8164,
"嘉定二中": 98.8836,
"安亭高中": 66.1413,
"封浜高中": 58.552,
"远东学校": 42.1661
},
"科学评价观": {
"上大嘉高": 56.5913,
"上师嘉高": 56.3756,
"中光高中": 50.8894,
"交附嘉分": 51.0055,
"华旭双语": 46.09,
"嘉一实高": 59.388,
"嘉定一中": 60.1653,
"嘉定二中": 55.9226,
"安亭高中": 60.8951,
"封浜高中": 53.0,
"远东学校": 50.8434
},
"学业质量评估": {
"上大嘉高": 52.3459,
"上师嘉高": 56.2348,
"中光高中": 41.1032,
"交附嘉分": 48.9711,
"华旭双语": 42.8051,
"嘉一实高": 52.5669,
"嘉定一中": 64.6782,
"嘉定二中": 45.532,
"安亭高中": 63.9947,
"封浜高中": 56.516,
"远东学校": 64.8695
},
"综合素质评估": {
"上大嘉高": 61.0498,
"上师嘉高": 61.0498,
"中光高中": 54.4457,
"交附嘉分": 41.7541,
"华旭双语": 50.433,
"嘉一实高": 47.7947,
"嘉定一中": 58.6924,
"嘉定二中": 59.9228,
"安亭高中": 45.9556,
"封浜高中": 61.0498,
"远东学校": 58.4487
},
"实践活动评估": {
"上大嘉高": 62.0694,
"上师嘉高": 57.7758,
"中光高中": 47.4085,
"交附嘉分": 42.5434,
"华旭双语": 42.1819,
"嘉一实高": 45.0998,
"嘉定一中": 60.2447,
"嘉定二中": 60.9566,
"安亭高中": 55.5878,
"封浜高中": 63.1821,
"远东学校": 64.8251
},
"区域推进": {
"上大嘉高": 53.1324,
"上师嘉高": 53.1324,
"中光高中": 38.3577,
"交附嘉分": 53.1324,
"华旭双语": 38.3577,
"嘉一实高": 55.2336,
"嘉定一中": 70.0083,
"嘉定二中": 48.3801,
"安亭高中": 61.5704,
"封浜高中": 48.8968,
"远东学校": 38.3577
},
"环境支持": {
"上大嘉高": 48.3723,
"上师嘉高": 36.2255,
"中光高中": 22.4955,
"交附嘉分": 64.6605,
"华旭双语": 57.43,
"嘉一实高": 52.1748,
"嘉定一中": 45.1475,
"嘉定二中": 64.1637,
"安亭高中": 46.064,
"封浜高中": 51.2805,
"远东学校": 44.199
},
"资源支持": {
"上大嘉高": 53.5406,
"上师嘉高": 45.9257,
"中光高中": 39.6658,
"交附嘉分": 53.3398,
"华旭双语": 50.546,
"嘉一实高": 53.7499,
"嘉定一中": 63.8282,
"嘉定二中": 59.7733,
"安亭高中": 53.9656,
"封浜高中": 56.6429,
"远东学校": 42.98
},
"教学方式创新": {
"上大嘉高": 54.1414,
"上师嘉高": 52.5568,
"中光高中": 49.1384,
"交附嘉分": 51.6319,
"华旭双语": 44.6587,
"嘉一实高": 51.4943,
"嘉定一中": 52.6504,
"嘉定二中": 50.0355,
"安亭高中": 51.2091,
"封浜高中": 50.1269,
"远东学校": 50.1797
},
"评价精准化与个性化": {
"上大嘉高": 50.7195,
"上师嘉高": 49.1516,
"中光高中": 50.7195,
"交附嘉分": 50.8502,
"华旭双语": 56.795,
"嘉一实高": 51.6994,
"嘉定一中": 44.1632,
"嘉定二中": 50.7195,
"安亭高中": 52.5487,
"封浜高中": 51.6994,
"远东学校": 49.7396
},
"课程迭代优化": {
"上大嘉高": 33.5447,
"上师嘉高": 36.5391,
"中光高中": 57.3147,
"交附嘉分": 61.3274,
"华旭双语": 54.4431,
"嘉一实高": 61.3274,
"嘉定一中": 60.0114,
"嘉定二中": 61.3274,
"安亭高中": 61.3274,
"封浜高中": 43.4234,
"远东学校": 47.436
}
},
"district": "嘉定区",
"total_schools_in_district": 11
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,773 @@
{
"school": "奉贤中学",
"school_info": {
"type": "市实验性示范性高中",
"code": "A类学校",
"nature": "公办",
"feature": "城市郊区学校"
},
"overall": {
"score": 55.6,
"district_avg": 48.2,
"same_type_avg": 52.5,
"rank_in_district": 1,
"total_schools": 8,
"cluster": "较好",
"school_type": "市实验性示范性高中",
"same_type_count": 73,
"rank_in_same_type": 9
},
"dimensions": {
"课程领导力": {
"score": 55.24,
"district_avg": 49.03,
"same_type_avg": 52.03,
"diff_district": 6.21,
"rank_in_district": 1,
"t_test_vs_district": {
"t": 3.401,
"p": 0.0767,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"教学变革力": {
"score": 50.34,
"district_avg": 49.54,
"same_type_avg": 51.74,
"diff_district": 0.8,
"rank_in_district": 4,
"t_test_vs_district": {
"t": 2.735,
"p": 0.2232,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"学生发展指导力": {
"score": 54.53,
"district_avg": 47.37,
"same_type_avg": 51.41,
"diff_district": 7.16,
"rank_in_district": 1,
"t_test_vs_district": {
"t": 1.703,
"p": 0.338,
"significant": false,
"n": 2
},
"cluster": "较好"
},
"教师发展支持力": {
"score": 63.41,
"district_avg": 49.93,
"same_type_avg": 53.91,
"diff_district": 13.48,
"rank_in_district": 1,
"t_test_vs_district": {
"t": 1.461,
"p": 0.2814,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"教育质量评估力": {
"score": 58.97,
"district_avg": 48.6,
"same_type_avg": 53.49,
"diff_district": 10.37,
"rank_in_district": 1,
"t_test_vs_district": {
"t": 6.555,
"p": 0.0072,
"significant": true,
"n": 4
},
"cluster": "较好"
},
"教育条件保障力": {
"score": 55.48,
"district_avg": 47.7,
"same_type_avg": 53.09,
"diff_district": 7.78,
"rank_in_district": 2,
"t_test_vs_district": {
"t": 2.1,
"p": 0.1706,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"数字化赋能力": {
"score": 51.25,
"district_avg": 45.95,
"same_type_avg": 51.84,
"diff_district": 5.3,
"rank_in_district": 2,
"t_test_vs_district": {
"t": 1.312,
"p": 0.3199,
"significant": false,
"n": 3
},
"cluster": "较好"
}
},
"sub_dimensions": {
"国家标准遵循": {
"parent_dimension": "课程领导力",
"score": 54.33,
"district_avg": 46.06,
"diff_district": 8.27,
"rank_in_district": 2,
"level": 3,
"level_description": "考试类科目必修开齐,选必和选修满足要求",
"level_distribution": {
"水平1": 3,
"水平2": 2,
"水平3": 3,
"水平4": 0
}
},
"课程结构建设": {
"parent_dimension": "课程领导力",
"score": 58.75,
"district_avg": 53.35,
"diff_district": 5.4,
"rank_in_district": 3,
"level": 4,
"level_description": "学科类必修课程离差总和在30%以内,总体三类课程在150%以下,校本课程和综合实践较好",
"level_distribution": {
"水平1": 1,
"水平2": 1,
"水平3": 3,
"水平4": 3
}
},
"课程规范落实": {
"parent_dimension": "课程领导力",
"score": 52.63,
"district_avg": 47.68,
"diff_district": 4.95,
"rank_in_district": 2,
"level": 3,
"level_description": "都有建设规范文本,但过程性档案记录已经全部建成,部分有尚未使用",
"level_distribution": {
"水平1": 2,
"水平2": 2,
"水平3": 4,
"水平4": 0
}
},
"教学方式变革": {
"parent_dimension": "教学变革力",
"score": 50.05,
"district_avg": 50.19,
"diff_district": -0.14,
"rank_in_district": 5,
"level": 3,
"level_description": "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 4,
"水平4": 1
}
},
"作业设计与管理变革": {
"parent_dimension": "教学变革力",
"score": 50.64,
"district_avg": 48.89,
"diff_district": 1.74,
"rank_in_district": 4,
"level": 3,
"level_description": "在每类创新性作业中至少掌握2种类型,偶有时长控制管理,面批为主,有至少两种属性标注",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 4,
"水平4": 0
}
},
"学科发展的个性化辅导": {
"parent_dimension": "学生发展指导力",
"score": 50.33,
"district_avg": 48.12,
"diff_district": 2.21,
"rank_in_district": 2,
"level": 3,
"level_description": "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导",
"level_distribution": {
"水平1": 3,
"水平2": 3,
"水平3": 2,
"水平4": 0
}
},
"学生生涯发展指导": {
"parent_dimension": "学生发展指导力",
"score": 58.73,
"district_avg": 47.27,
"diff_district": 11.46,
"rank_in_district": 1,
"level": 4,
"level_description": "专设生涯指导课程,三年覆盖90%+学生,本校+外聘教师队伍,校内外资源足够支持",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 3,
"水平4": 1
}
},
"培训支持": {
"parent_dimension": "教师发展支持力",
"score": 51.81,
"district_avg": 49.12,
"diff_district": 2.69,
"rank_in_district": 1,
"level": 4,
"level_description": "各学科教师平均外出培训人数≥2.5人",
"level_distribution": {
"水平1": 2,
"水平2": 2,
"水平3": 3,
"水平4": 1
}
},
"教研支持": {
"parent_dimension": "教师发展支持力",
"score": 56.79,
"district_avg": 51.41,
"diff_district": 5.39,
"rank_in_district": 1,
"level": 4,
"level_description": "教研的数量和质量均较高",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 3,
"水平4": 2
}
},
"项目支持": {
"parent_dimension": "教师发展支持力",
"score": 81.63,
"district_avg": 50.62,
"diff_district": 31.02,
"rank_in_district": 1,
"level": 4,
"level_description": "各学科均有负责的校级以上项目至少一个",
"level_distribution": {
"水平1": 2,
"水平2": 5,
"水平3": 0,
"水平4": 1
}
},
"科学评价观": {
"parent_dimension": "教育质量评估力",
"score": 58.46,
"district_avg": 49.64,
"diff_district": 8.82,
"rank_in_district": 1,
"level": 4,
"level_description": "在所有方面均能至少关注两项素养发展",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 3,
"水平4": 1
}
},
"学业质量评估": {
"parent_dimension": "教育质量评估力",
"score": 54.71,
"district_avg": 46.8,
"diff_district": 7.91,
"rank_in_district": 1,
"level": 4,
"level_description": "校本化评价工具已研制并使用,学期考试质量分析并标注属性较为全面",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 3,
"水平4": 1
}
},
"综合素质评估": {
"parent_dimension": "教育质量评估力",
"score": 61.05,
"district_avg": 49.77,
"diff_district": 11.28,
"rank_in_district": 1,
"level": 4,
"level_description": "校本化综合素质评价体系均有建设和使用,有平台支持且评价结果表达科学",
"level_distribution": {
"水平1": 1,
"水平2": 4,
"水平3": 2,
"水平4": 1
}
},
"实践活动评估": {
"parent_dimension": "教育质量评估力",
"score": 61.67,
"district_avg": 48.2,
"diff_district": 13.46,
"rank_in_district": 1,
"level": 4,
"level_description": "研究性学习、社会考察和学科实践活动的校本化评价工具已研制并使用",
"level_distribution": {
"水平1": 3,
"水平2": 1,
"水平3": 1,
"水平4": 3
}
},
"区域推进": {
"parent_dimension": "教育条件保障力",
"score": 55.23,
"district_avg": 49.63,
"diff_district": 5.61,
"rank_in_district": 4,
"level": 3,
"level_description": "召开会议平均一个月1次及以上,区域管理文件3个以上,措施达3项",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 3,
"水平4": 2
}
},
"环境支持": {
"parent_dimension": "教育条件保障力",
"score": 49.19,
"district_avg": 43.92,
"diff_district": 5.27,
"rank_in_district": 3,
"level": 2,
"level_description": "信息化支持和硬件支持至少有一项建设较好",
"level_distribution": {
"水平1": 3,
"水平2": 3,
"水平3": 2,
"水平4": 0
}
},
"资源支持": {
"parent_dimension": "教育条件保障力",
"score": 62.03,
"district_avg": 49.56,
"diff_district": 12.47,
"rank_in_district": 1,
"level": 4,
"level_description": "校内外资源和师资水平均处于较高水平",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 3,
"水平4": 1
}
},
"教学方式创新": {
"parent_dimension": "数字化赋能力",
"score": 47.85,
"district_avg": 51.4,
"diff_district": -3.56,
"rank_in_district": 6,
"level": 3,
"level_description": "信息技术与教学融合程度较高,教师能使用信息技术",
"level_distribution": {
"水平1": 1,
"水平2": 0,
"水平3": 5,
"水平4": 2
}
},
"评价精准化与个性化": {
"parent_dimension": "数字化赋能力",
"score": 46.6,
"district_avg": 47.94,
"diff_district": -1.34,
"rank_in_district": 6,
"level": 2,
"level_description": "有信息技术平台支持学业评价",
"level_distribution": {
"水平1": 2,
"水平2": 4,
"水平3": 2,
"水平4": 0
}
},
"课程迭代优化": {
"parent_dimension": "数字化赋能力",
"score": 59.29,
"district_avg": 44.05,
"diff_district": 15.23,
"rank_in_district": 1,
"level": 4,
"level_description": "学校对促进教学数字化转型有专项研修计划并开展相关活动,业务绝大部分使用信息化系统",
"level_distribution": {
"水平1": 4,
"水平2": 1,
"水平3": 1,
"水平4": 2
}
}
},
"correlation": {
"课程领导力": {
"课程领导力": 1.0,
"教学变革力": -0.6105,
"学生发展指导力": -0.0892,
"教师发展支持力": 0.188,
"教育质量评估力": 0.5997,
"教育条件保障力": 0.0335,
"数字化赋能力": -0.3897
},
"教学变革力": {
"课程领导力": -0.6105,
"教学变革力": 1.0,
"学生发展指导力": 0.5319,
"教师发展支持力": 0.2804,
"教育质量评估力": 0.0889,
"教育条件保障力": 0.4846,
"数字化赋能力": 0.3124
},
"学生发展指导力": {
"课程领导力": -0.0892,
"教学变革力": 0.5319,
"学生发展指导力": 1.0,
"教师发展支持力": 0.6624,
"教育质量评估力": 0.4703,
"教育条件保障力": 0.6939,
"数字化赋能力": 0.3579
},
"教师发展支持力": {
"课程领导力": 0.188,
"教学变革力": 0.2804,
"学生发展指导力": 0.6624,
"教师发展支持力": 1.0,
"教育质量评估力": 0.708,
"教育条件保障力": 0.5715,
"数字化赋能力": 0.4776
},
"教育质量评估力": {
"课程领导力": 0.5997,
"教学变革力": 0.0889,
"学生发展指导力": 0.4703,
"教师发展支持力": 0.708,
"教育质量评估力": 1.0,
"教育条件保障力": 0.2641,
"数字化赋能力": 0.0097
},
"教育条件保障力": {
"课程领导力": 0.0335,
"教学变革力": 0.4846,
"学生发展指导力": 0.6939,
"教师发展支持力": 0.5715,
"教育质量评估力": 0.2641,
"教育条件保障力": 1.0,
"数字化赋能力": 0.0924
},
"数字化赋能力": {
"课程领导力": -0.3897,
"教学变革力": 0.3124,
"学生发展指导力": 0.3579,
"教师发展支持力": 0.4776,
"教育质量评估力": 0.0097,
"教育条件保障力": 0.0924,
"数字化赋能力": 1.0
}
},
"all_schools_dim_scores": {
"课程领导力": {
"上师大四附中": 51.8551,
"东华致远": 49.5367,
"华二临港": 39.4841,
"华理曙光": 42.3489,
"奉城高中": 52.3584,
"奉贤中学": 55.2378,
"景秀高中": 51.5565,
"美达菲": 49.868
},
"教学变革力": {
"上师大四附中": 50.6453,
"东华致远": 47.6026,
"华二临港": 53.5105,
"华理曙光": 51.8693,
"奉城高中": null,
"奉贤中学": 50.342,
"景秀高中": 46.2826,
"美达菲": 46.5209
},
"学生发展指导力": {
"上师大四附中": 48.646,
"东华致远": 45.2678,
"华二临港": 51.2961,
"华理曙光": 49.6106,
"奉城高中": 42.9885,
"奉贤中学": 54.5307,
"景秀高中": 51.2792,
"美达菲": 35.3781
},
"教师发展支持力": {
"上师大四附中": 46.3688,
"东华致远": 50.6232,
"华二临港": 50.5657,
"华理曙光": 50.1272,
"奉城高中": 44.8985,
"奉贤中学": 63.4129,
"景秀高中": 48.3054,
"美达菲": 45.1706
},
"教育质量评估力": {
"上师大四附中": 52.0626,
"东华致远": 47.4947,
"华二临港": 41.9598,
"华理曙光": 49.7758,
"奉城高中": 47.0476,
"奉贤中学": 58.9705,
"景秀高中": 46.7398,
"美达菲": 44.7888
},
"教育条件保障力": {
"上师大四附中": 48.6463,
"东华致远": 50.3368,
"华二临港": 55.9273,
"华理曙光": 40.7999,
"奉城高中": 48.5022,
"奉贤中学": 55.4843,
"景秀高中": 46.1201,
"美达菲": 35.8036
},
"数字化赋能力": {
"上师大四附中": 42.744,
"东华致远": 46.2288,
"华二临港": 54.1561,
"华理曙光": 47.9278,
"奉城高中": 27.4908,
"奉贤中学": 51.2455,
"景秀高中": 48.7126,
"美达菲": 49.0936
},
"总体得分": {
"上师大四附中": 48.7097,
"东华致远": 48.1558,
"华二临港": 49.5571,
"华理曙光": 47.4942,
"奉城高中": 43.881,
"奉贤中学": 55.6034,
"景秀高中": 48.428,
"美达菲": 43.8034
}
},
"all_schools_sub_scores": {
"国家标准遵循": {
"上师大四附中": 56.9335,
"东华致远": 52.4679,
"华二临港": 29.7395,
"华理曙光": 36.265,
"奉城高中": 46.114,
"奉贤中学": 54.3284,
"景秀高中": 54.2999,
"美达菲": 38.334
},
"课程结构建设": {
"上师大四附中": 48.4872,
"东华致远": 41.1641,
"华二临港": 50.5903,
"华理曙光": 52.9766,
"奉城高中": 59.783,
"奉贤中学": 58.7544,
"景秀高中": 52.9484,
"美达菲": 62.133
},
"课程规范落实": {
"上师大四附中": 50.1448,
"东华致远": 54.9782,
"华二临港": 38.1224,
"华理曙光": 37.8051,
"奉城高中": 51.1781,
"奉贤中学": 52.6305,
"景秀高中": 47.4214,
"美达菲": 49.137
},
"教学方式变革": {
"上师大四附中": 50.5637,
"东华致远": 50.2051,
"华二临港": 54.9287,
"华理曙光": 52.14,
"奉城高中": null,
"奉贤中学": 50.0483,
"景秀高中": 47.0802,
"美达菲": 46.3445
},
"作业设计与管理变革": {
"上师大四附中": 50.727,
"东华致远": 45.0001,
"华二临港": 52.0924,
"华理曙光": 51.5985,
"奉城高中": null,
"奉贤中学": 50.6356,
"景秀高中": 45.4849,
"美达菲": 46.6973
},
"学科发展的个性化辅导": {
"上师大四附中": 43.3619,
"东华致远": 44.5731,
"华二临港": 49.8042,
"华理曙光": 49.2212,
"奉城高中": null,
"奉贤中学": 50.3296,
"景秀高中": 47.7122,
"美达菲": 51.8095
},
"学生生涯发展指导": {
"上师大四附中": 53.9301,
"东华致远": 45.9625,
"华二临港": 52.788,
"华理曙光": 50.0,
"奉城高中": 42.9885,
"奉贤中学": 58.7318,
"景秀高中": 54.8461,
"美达菲": 18.9467
},
"培训支持": {
"上师大四附中": 47.4489,
"东华致远": 49.7922,
"华二临港": 49.003,
"华理曙光": 49.751,
"奉城高中": null,
"奉贤中学": 51.8118,
"景秀高中": 48.4051,
"美达菲": 47.6228
},
"教研支持": {
"上师大四附中": 48.2176,
"东华致远": 52.3596,
"华二临港": 55.0533,
"华理曙光": 50.6305,
"奉城高中": null,
"奉贤中学": 56.7922,
"景秀高中": 50.3305,
"美达菲": 46.4514
},
"项目支持": {
"上师大四附中": 43.44,
"东华致远": 49.7178,
"华二临港": 47.6409,
"华理曙光": 50.0,
"奉城高中": 44.8985,
"奉贤中学": 81.6346,
"景秀高中": 46.1805,
"美达菲": 41.4377
},
"科学评价观": {
"上师大四附中": 51.6848,
"东华致远": 40.6649,
"华二临港": 53.8405,
"华理曙光": 50.6772,
"奉城高中": 50.0,
"奉贤中学": 58.4561,
"景秀高中": 43.4193,
"美达菲": 48.3713
},
"学业质量评估": {
"上师大四附中": 42.9192,
"东华致远": 42.8051,
"华二临港": 40.7427,
"华理曙光": 50.446,
"奉城高中": 50.0,
"奉贤中学": 54.7098,
"景秀高中": 48.3199,
"美达菲": 44.4689
},
"综合素质评估": {
"上师大四附中": 57.3171,
"东华致远": 56.8277,
"华二临港": 37.6972,
"华理曙光": 46.573,
"奉城高中": 46.0108,
"奉贤中学": 61.0498,
"景秀高中": 49.0487,
"美达菲": 43.6698
},
"实践活动评估": {
"上师大四附中": 56.3291,
"东华致远": 49.681,
"华二临港": 35.5589,
"华理曙光": 51.407,
"奉城高中": 42.1796,
"奉贤中学": 61.6664,
"景秀高中": 46.1711,
"美达菲": 42.6452
},
"区域推进": {
"上师大四附中": 53.1324,
"东华致远": 61.5704,
"华二临港": 61.5704,
"华理曙光": 23.033,
"奉城高中": 55.2336,
"奉贤中学": 55.2336,
"景秀高中": 46.7957,
"美达菲": 40.4589
},
"环境支持": {
"上师大四附中": 46.5856,
"东华致远": 39.2041,
"华二临港": 50.0633,
"华理曙光": 50.5886,
"奉城高中": 44.9146,
"奉贤中学": 49.1939,
"景秀高中": 37.4653,
"美达菲": 33.3547
},
"资源支持": {
"上师大四附中": 46.2209,
"东华致远": 50.236,
"华二临港": 56.1482,
"华理曙光": 48.7781,
"奉城高中": 45.3585,
"奉贤中学": 62.0255,
"景秀高中": 54.0994,
"美达菲": 33.5971
},
"教学方式创新": {
"上师大四附中": 51.1648,
"东华致远": 49.868,
"华二临港": 55.4954,
"华理曙光": 54.8231,
"奉城高中": null,
"奉贤中学": 47.8467,
"景秀高中": 53.0563,
"美达菲": 47.5593
},
"评价精准化与个性化": {
"上师大四附中": 49.5763,
"东华致远": 49.5825,
"华二临港": 51.6994,
"华理曙光": 38.9604,
"奉城高中": null,
"奉贤中学": 46.6038,
"景秀高中": 51.6994,
"美达菲": 47.4531
},
"课程迭代优化": {
"上师大四附中": 27.4908,
"东华致远": 39.2358,
"华二临港": 55.2734,
"华理曙光": 50.0,
"奉城高中": 27.4908,
"奉贤中学": 59.2861,
"景秀高中": 41.3821,
"美达菲": 52.2682
}
},
"district": "奉贤区",
"total_schools_in_district": 8
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+799
View File
@@ -0,0 +1,799 @@
{
"school": "延安中学",
"school_info": {
"type": "市实验性示范性高中",
"code": "A类学校",
"nature": "公办",
"feature": "城区学校"
},
"overall": {
"score": 55.35,
"district_avg": 50.02,
"same_type_avg": 51.67,
"rank_in_district": 1,
"total_schools": 9,
"cluster": "较好",
"school_type": "市实验性示范性高中",
"same_type_count": 3,
"rank_in_same_type": 1
},
"dimensions": {
"课程领导力": {
"score": 49.48,
"district_avg": 50.0,
"same_type_avg": 49.26,
"diff_district": -0.52,
"rank_in_district": 6,
"t_test_vs_district": {
"t": -0.154,
"p": 0.8917,
"significant": false,
"n": 3
},
"cluster": "待提升"
},
"教学变革力": {
"score": 51.26,
"district_avg": 49.97,
"same_type_avg": 50.7,
"diff_district": 1.28,
"rank_in_district": 4,
"t_test_vs_district": {
"t": 2.141,
"p": 0.2782,
"significant": false,
"n": 2
},
"cluster": "待提升"
},
"学生发展指导力": {
"score": 54.22,
"district_avg": 49.92,
"same_type_avg": 52.49,
"diff_district": 4.3,
"rank_in_district": 2,
"t_test_vs_district": {
"t": 1.773,
"p": 0.3269,
"significant": false,
"n": 2
},
"cluster": "中等"
},
"教师发展支持力": {
"score": 60.65,
"district_avg": 49.99,
"same_type_avg": 54.22,
"diff_district": 10.66,
"rank_in_district": 1,
"t_test_vs_district": {
"t": 2.609,
"p": 0.1209,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"教育质量评估力": {
"score": 60.95,
"district_avg": 50.0,
"same_type_avg": 48.8,
"diff_district": 10.95,
"rank_in_district": 1,
"t_test_vs_district": {
"t": 2.396,
"p": 0.0963,
"significant": false,
"n": 4
},
"cluster": "较好"
},
"教育条件保障力": {
"score": 56.26,
"district_avg": 50.0,
"same_type_avg": 54.49,
"diff_district": 6.26,
"rank_in_district": 2,
"t_test_vs_district": {
"t": 2.011,
"p": 0.182,
"significant": false,
"n": 3
},
"cluster": "较好"
},
"数字化赋能力": {
"score": 54.66,
"district_avg": 50.28,
"same_type_avg": 51.76,
"diff_district": 4.38,
"rank_in_district": 3,
"t_test_vs_district": {
"t": 1.056,
"p": 0.4018,
"significant": false,
"n": 3
},
"cluster": "较好"
}
},
"sub_dimensions": {
"国家标准遵循": {
"parent_dimension": "课程领导力",
"score": 56.14,
"district_avg": 50.0,
"diff_district": 6.14,
"rank_in_district": 1,
"level": 3,
"level_description": "考试类科目必修开齐,选必和选修满足要求",
"level_distribution": {
"水平1": 2,
"水平2": 3,
"水平3": 4,
"水平4": 0
}
},
"课程结构建设": {
"parent_dimension": "课程领导力",
"score": 45.01,
"district_avg": 50.0,
"diff_district": -4.99,
"rank_in_district": 8,
"level": 2,
"level_description": "总体三类课程离差总和在300%以下,校本课程和综合实践较差",
"level_distribution": {
"水平1": 1,
"水平2": 4,
"水平3": 2,
"水平4": 2
}
},
"课程规范落实": {
"parent_dimension": "课程领导力",
"score": 47.29,
"district_avg": 50.0,
"diff_district": -2.71,
"rank_in_district": 7,
"level": 2,
"level_description": "部分有建设规范文本,档案大部分已经建成但未使用",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 6,
"水平4": 0
}
},
"教学方式变革": {
"parent_dimension": "教学变革力",
"score": 50.66,
"district_avg": 49.91,
"diff_district": 0.75,
"rank_in_district": 4,
"level": 3,
"level_description": "大部分老师有共识和研究,并能够落实教材中的相关要求,至少有2种常态化落实形式",
"level_distribution": {
"水平1": 0,
"水平2": 2,
"水平3": 7,
"水平4": 0
}
},
"作业设计与管理变革": {
"parent_dimension": "教学变革力",
"score": 51.86,
"district_avg": 50.04,
"diff_district": 1.82,
"rank_in_district": 3,
"level": 3,
"level_description": "在每类创新性作业中至少掌握2种类型,偶有时长控制管理,面批为主,有至少两种属性标注",
"level_distribution": {
"水平1": 1,
"水平2": 4,
"水平3": 2,
"水平4": 2
}
},
"学科发展的个性化辅导": {
"parent_dimension": "学生发展指导力",
"score": 56.65,
"district_avg": 49.84,
"diff_district": 6.81,
"rank_in_district": 1,
"level": 3,
"level_description": "各学科平均辅导时长每周1-2小时,教师根据学情确定内容,主要个别分散辅导",
"level_distribution": {
"水平1": 1,
"水平2": 5,
"水平3": 3,
"水平4": 0
}
},
"学生生涯发展指导": {
"parent_dimension": "学生发展指导力",
"score": 51.8,
"district_avg": 50.0,
"diff_district": 1.8,
"rank_in_district": 5,
"level": 3,
"level_description": "外请讲座实施,三年覆盖70-90%学生,外聘教师队伍,校内外资源有一些支持",
"level_distribution": {
"水平1": 2,
"水平2": 2,
"水平3": 3,
"水平4": 2
}
},
"培训支持": {
"parent_dimension": "教师发展支持力",
"score": 60.18,
"district_avg": 49.94,
"diff_district": 10.24,
"rank_in_district": 1,
"level": 4,
"level_description": "各学科教师平均外出培训人数≥2.5人",
"level_distribution": {
"水平1": 2,
"水平2": 3,
"水平3": 2,
"水平4": 2
}
},
"教研支持": {
"parent_dimension": "教师发展支持力",
"score": 53.82,
"district_avg": 50.03,
"diff_district": 3.79,
"rank_in_district": 2,
"level": 3,
"level_description": "有教研活动,质量较好",
"level_distribution": {
"水平1": 1,
"水平2": 3,
"水平3": 5,
"水平4": 0
}
},
"项目支持": {
"parent_dimension": "教师发展支持力",
"score": 67.95,
"district_avg": 50.0,
"diff_district": 17.95,
"rank_in_district": 1,
"level": 4,
"level_description": "各学科均有负责的校级以上项目至少一个",
"level_distribution": {
"水平1": 2,
"水平2": 2,
"水平3": 3,
"水平4": 2
}
},
"科学评价观": {
"parent_dimension": "教育质量评估力",
"score": 47.75,
"district_avg": 50.0,
"diff_district": -2.25,
"rank_in_district": 7,
"level": 2,
"level_description": "能较多关注学生发展",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 5,
"水平4": 1
}
},
"学业质量评估": {
"parent_dimension": "教育质量评估力",
"score": 68.48,
"district_avg": 50.0,
"diff_district": 18.48,
"rank_in_district": 1,
"level": 4,
"level_description": "校本化评价工具已研制并使用,学期考试质量分析并标注属性较为全面",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 4,
"水平4": 2
}
},
"综合素质评估": {
"parent_dimension": "教育质量评估力",
"score": 62.45,
"district_avg": 50.0,
"diff_district": 12.45,
"rank_in_district": 1,
"level": 4,
"level_description": "校本化综合素质评价体系均有建设和使用,有平台支持且评价结果表达科学",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 5,
"水平4": 1
}
},
"实践活动评估": {
"parent_dimension": "教育质量评估力",
"score": 65.12,
"district_avg": 50.0,
"diff_district": 15.12,
"rank_in_district": 1,
"level": 4,
"level_description": "研究性学习、社会考察和学科实践活动的校本化评价工具已研制并使用",
"level_distribution": {
"水平1": 2,
"水平2": 1,
"水平3": 0,
"水平4": 6
}
},
"区域推进": {
"parent_dimension": "教育条件保障力",
"score": 57.76,
"district_avg": 50.0,
"diff_district": 7.76,
"rank_in_district": 3,
"level": 3,
"level_description": "召开会议平均一个月1次及以上,区域管理文件3个以上,措施达3项",
"level_distribution": {
"水平1": 2,
"水平2": 2,
"水平3": 4,
"水平4": 1
}
},
"环境支持": {
"parent_dimension": "教育条件保障力",
"score": 50.28,
"district_avg": 50.0,
"diff_district": 0.28,
"rank_in_district": 5,
"level": 3,
"level_description": "信息化支持和硬件在平均水平附近",
"level_distribution": {
"水平1": 2,
"水平2": 2,
"水平3": 2,
"水平4": 3
}
},
"资源支持": {
"parent_dimension": "教育条件保障力",
"score": 60.75,
"district_avg": 50.0,
"diff_district": 10.75,
"rank_in_district": 1,
"level": 4,
"level_description": "校内外资源和师资水平均处于较高水平",
"level_distribution": {
"水平1": 1,
"水平2": 2,
"水平3": 4,
"水平4": 2
}
},
"教学方式创新": {
"parent_dimension": "数字化赋能力",
"score": 49.06,
"district_avg": 50.02,
"diff_district": -0.96,
"rank_in_district": 6,
"level": 3,
"level_description": "信息技术与教学融合程度较高,教师能使用信息技术",
"level_distribution": {
"水平1": 0,
"水平2": 0,
"水平3": 8,
"水平4": 1
}
},
"评价精准化与个性化": {
"parent_dimension": "数字化赋能力",
"score": 52.16,
"district_avg": 50.83,
"diff_district": 1.33,
"rank_in_district": 5,
"level": 3,
"level_description": "借助第三方平台支持学科诊断与综合素质评价",
"level_distribution": {
"水平1": 0,
"水平2": 4,
"水平3": 5,
"水平4": 0
}
},
"课程迭代优化": {
"parent_dimension": "数字化赋能力",
"score": 62.75,
"district_avg": 50.0,
"diff_district": 12.75,
"rank_in_district": 2,
"level": 4,
"level_description": "学校对促进教学数字化转型有专项研修计划并开展相关活动,业务绝大部分使用信息化系统",
"level_distribution": {
"水平1": 4,
"水平2": 0,
"水平3": 2,
"水平4": 3
}
}
},
"correlation": {
"课程领导力": {
"课程领导力": 1.0,
"教学变革力": -0.0461,
"学生发展指导力": -0.5591,
"教师发展支持力": -0.3338,
"教育质量评估力": 0.2474,
"教育条件保障力": -0.3225,
"数字化赋能力": 0.0415
},
"教学变革力": {
"课程领导力": -0.0461,
"教学变革力": 1.0,
"学生发展指导力": 0.7046,
"教师发展支持力": 0.1774,
"教育质量评估力": 0.6226,
"教育条件保障力": 0.7119,
"数字化赋能力": 0.9339
},
"学生发展指导力": {
"课程领导力": -0.5591,
"教学变革力": 0.7046,
"学生发展指导力": 1.0,
"教师发展支持力": 0.6181,
"教育质量评估力": 0.481,
"教育条件保障力": 0.6232,
"数字化赋能力": 0.5673
},
"教师发展支持力": {
"课程领导力": -0.3338,
"教学变革力": 0.1774,
"学生发展指导力": 0.6181,
"教师发展支持力": 1.0,
"教育质量评估力": 0.4044,
"教育条件保障力": 0.2729,
"数字化赋能力": 0.2597
},
"教育质量评估力": {
"课程领导力": 0.2474,
"教学变革力": 0.6226,
"学生发展指导力": 0.481,
"教师发展支持力": 0.4044,
"教育质量评估力": 1.0,
"教育条件保障力": 0.3448,
"数字化赋能力": 0.6486
},
"教育条件保障力": {
"课程领导力": -0.3225,
"教学变革力": 0.7119,
"学生发展指导力": 0.6232,
"教师发展支持力": 0.2729,
"教育质量评估力": 0.3448,
"教育条件保障力": 1.0,
"数字化赋能力": 0.6858
},
"数字化赋能力": {
"课程领导力": 0.0415,
"教学变革力": 0.9339,
"学生发展指导力": 0.5673,
"教师发展支持力": 0.2597,
"教育质量评估力": 0.6486,
"教育条件保障力": 0.6858,
"数字化赋能力": 1.0
}
},
"all_schools_dim_scores": {
"课程领导力": {
"仙霞高中": 48.4556,
"华政附中": 50.5107,
"复旦中学": 49.4807,
"天山学校": 50.7942,
"市三女中": 48.8126,
"延安中学": 49.4771,
"建青实验": 49.1025,
"民办新虹桥": 50.8982,
"西郊学校": 52.4684
},
"教学变革力": {
"仙霞高中": 47.515,
"华政附中": 48.2694,
"复旦中学": 49.5334,
"天山学校": 49.7866,
"市三女中": 51.3195,
"延安中学": 51.2594,
"建青实验": 53.5021,
"民办新虹桥": 46.9305,
"西郊学校": 51.6555
},
"学生发展指导力": {
"仙霞高中": 46.9318,
"华政附中": 49.5928,
"复旦中学": 50.7792,
"天山学校": 50.2304,
"市三女中": 52.4578,
"延安中学": 54.2221,
"建青实验": 55.5923,
"民办新虹桥": 43.8925,
"西郊学校": 45.5851
},
"教师发展支持力": {
"仙霞高中": 48.614,
"华政附中": 51.9092,
"复旦中学": 52.3288,
"天山学校": 52.3637,
"市三女中": 49.6822,
"延安中学": 60.6521,
"建青实验": 46.9914,
"民办新虹桥": 42.3748,
"西郊学校": 44.9994
},
"教育质量评估力": {
"仙霞高中": 42.793,
"华政附中": 52.9184,
"复旦中学": 39.2987,
"天山学校": 53.4378,
"市三女中": 46.1352,
"延安中学": 60.952,
"建青实验": 56.9331,
"民办新虹桥": 42.8134,
"西郊学校": 54.7184
},
"教育条件保障力": {
"仙霞高中": 45.7273,
"华政附中": 38.2073,
"复旦中学": 46.4728,
"天山学校": 54.2133,
"市三女中": 60.7468,
"延安中学": 56.2614,
"建青实验": 55.2529,
"民办新虹桥": 44.4481,
"西郊学校": 48.6701
},
"数字化赋能力": {
"仙霞高中": 47.1035,
"华政附中": 45.2708,
"复旦中学": 49.4865,
"天山学校": 50.249,
"市三女中": 51.1399,
"延安中学": 54.6578,
"建青实验": 55.2976,
"民办新虹桥": 43.962,
"西郊学校": 55.3689
},
"总体得分": {
"仙霞高中": 46.7343,
"华政附中": 48.0969,
"复旦中学": 48.1972,
"天山学校": 51.5821,
"市三女中": 51.4706,
"延安中学": 55.3546,
"建青实验": 53.2389,
"民办新虹桥": 45.0456,
"西郊学校": 50.4951
}
},
"all_schools_sub_scores": {
"国家标准遵循": {
"仙霞高中": 55.2261,
"华政附中": 48.8041,
"复旦中学": 50.3006,
"天山学校": 54.1258,
"市三女中": 41.0937,
"延安中学": 56.1359,
"建青实验": 54.9257,
"民办新虹桥": 40.7642,
"西郊学校": 48.6239
},
"课程结构建设": {
"仙霞高中": 45.7208,
"华政附中": 59.3194,
"复旦中学": 46.0837,
"天山学校": 46.318,
"市三女中": 54.2715,
"延安中学": 45.0087,
"建青实验": 40.4431,
"民办新虹桥": 59.9915,
"西郊学校": 52.8433
},
"课程规范落实": {
"仙霞高中": 44.42,
"华政附中": 43.4085,
"复旦中学": 52.0579,
"天山学校": 51.9388,
"市三女中": 51.0727,
"延安中学": 47.2866,
"建青实验": 51.9388,
"民办新虹桥": 51.9388,
"西郊学校": 55.9379
},
"教学方式变革": {
"仙霞高中": 50.062,
"华政附中": 49.477,
"复旦中学": 50.7955,
"天山学校": 50.9855,
"市三女中": 48.3053,
"延安中学": 50.6592,
"建青实验": 49.8711,
"民办新虹桥": 46.681,
"西郊学校": 52.3253
},
"作业设计与管理变革": {
"仙霞高中": 44.968,
"华政附中": 47.0617,
"复旦中学": 48.2712,
"天山学校": 48.5877,
"市三女中": 54.3336,
"延安中学": 51.8595,
"建青实验": 57.133,
"民办新虹桥": 47.18,
"西郊学校": 50.9857
},
"学科发展的个性化辅导": {
"仙霞高中": 53.5034,
"华政附中": 44.9556,
"复旦中学": 51.5584,
"天山学校": 48.6644,
"市三女中": 47.2845,
"延安中学": 56.6478,
"建青实验": 49.9196,
"民办新虹桥": 47.4249,
"西郊学校": 48.6094
},
"学生生涯发展指导": {
"仙霞高中": 40.3601,
"华政附中": 54.23,
"复旦中学": 50.0,
"天山学校": 51.7965,
"市三女中": 57.6311,
"延安中学": 51.7965,
"建青实验": 61.265,
"民办新虹桥": 40.3601,
"西郊学校": 42.5608
},
"培训支持": {
"仙霞高中": 49.1277,
"华政附中": 49.8011,
"复旦中学": 48.7739,
"天山学校": 51.3166,
"市三女中": 48.123,
"延安中学": 60.1806,
"建青实验": 48.2716,
"民办新虹桥": 46.4335,
"西郊学校": 47.4447
},
"教研支持": {
"仙霞高中": 50.3324,
"华政附中": 47.6997,
"复旦中学": 53.8667,
"天山学校": 52.79,
"市三女中": 50.124,
"延安中学": 53.8217,
"建青实验": 47.9023,
"民办新虹桥": 46.3074,
"西郊学校": 47.4299
},
"项目支持": {
"仙霞高中": 46.3819,
"华政附中": 58.2267,
"复旦中学": 54.3458,
"天山学校": 52.9845,
"市三女中": 50.7995,
"延安中学": 67.954,
"建青实验": 44.8004,
"民办新虹桥": 34.3835,
"西郊学校": 40.1237
},
"科学评价观": {
"仙霞高中": 55.2838,
"华政附中": 51.1834,
"复旦中学": 39.0478,
"天山学校": 53.5481,
"市三女中": 53.9277,
"延安中学": 47.7458,
"建青实验": 51.5574,
"民办新虹桥": 31.352,
"西郊学校": 66.3541
},
"学业质量评估": {
"仙霞高中": 41.6331,
"华政附中": 47.7924,
"复旦中学": 44.7948,
"天山学校": 49.9498,
"市三女中": 48.0109,
"延安中学": 68.484,
"建青实验": 63.6725,
"民办新虹桥": 37.2325,
"西郊学校": 48.4301
},
"综合素质评估": {
"仙霞高中": 39.3144,
"华政附中": 55.5523,
"复旦中学": 38.1422,
"天山学校": 55.0228,
"市三女中": 36.4468,
"延安中学": 62.4542,
"建青实验": 56.8134,
"民办新虹桥": 52.256,
"西郊学校": 53.998
},
"实践活动评估": {
"仙霞高中": 34.9408,
"华政附中": 57.1456,
"复旦中学": 35.2102,
"天山学校": 55.2305,
"市三女中": 46.1554,
"延安中学": 65.1241,
"建青实验": 55.689,
"民办新虹桥": 50.4131,
"西郊学校": 50.0913
},
"区域推进": {
"仙霞高中": 38.9607,
"华政附中": 30.891,
"复旦中学": 49.6889,
"天山学校": 47.0304,
"市三女中": 63.1227,
"延安中学": 57.7586,
"建青实验": 52.3945,
"民办新虹桥": 57.7586,
"西郊学校": 52.3945
},
"环境支持": {
"仙霞高中": 53.6302,
"华政附中": 34.3936,
"复旦中学": 40.344,
"天山学校": 61.4189,
"市三女中": 61.4189,
"延安中学": 50.2782,
"建青实验": 60.6526,
"民办新虹桥": 42.9222,
"西郊学校": 44.9415
},
"资源支持": {
"仙霞高中": 44.5911,
"华政附中": 49.3374,
"复旦中学": 49.3856,
"天山学校": 54.1908,
"市三女中": 57.6986,
"延安中学": 60.7473,
"建青实验": 52.7116,
"民办新虹桥": 32.6634,
"西郊学校": 48.6742
},
"教学方式创新": {
"仙霞高中": 48.7259,
"华政附中": 45.9517,
"复旦中学": 49.7523,
"天山学校": 49.3392,
"市三女中": 51.5614,
"延安中学": 49.06,
"建青实验": 50.9795,
"民办新虹桥": 48.3275,
"西郊学校": 56.4383
},
"评价精准化与个性化": {
"仙霞高中": 49.6399,
"华政附中": 52.1607,
"复旦中学": 45.8586,
"天山学校": 58.4629,
"市三女中": 49.0097,
"延安中学": 52.1607,
"建青实验": 52.1607,
"民办新虹桥": 45.8586,
"西郊学校": 52.1607
},
"课程迭代优化": {
"仙霞高中": 42.9448,
"华政附中": 37.6999,
"复旦中学": 52.8487,
"天山学校": 42.9448,
"市三女中": 52.8487,
"延安中学": 62.7527,
"建青实验": 62.7527,
"民办新虹桥": 37.6999,
"西郊学校": 57.5078
}
}
}

Some files were not shown because too many files have changed in this diff Show More