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
+147
View File
@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ school }}课程实施监测数据分析报告</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@2/dist/echarts-gl.min.js"></script>
{% include 'components/styles.html' %}
</head>
<body>
{# ===== 右侧目录导航(JS自动从h1/h2生成) ===== #}
<nav class="toc-nav" id="toc-nav"><div class="toc-track" id="toc-track"></div></nav>
<button class="toc-toggle" id="toc-toggle" onclick="document.getElementById('toc-nav').classList.toggle('show')"></button>
<div class="report-container">
{# ===== 封面 ===== #}
{% include 'sections/cover.html' %}
{# ===== 第一部分:测评背景与实施 ===== #}
{% include 'sections/part0_background.html' %}
{# ===== 第二部分:总体表现 ===== #}
{% include 'sections/part1_overview.html' %}
{# ===== 第三~九部分:各维度详细分析 ===== #}
{% set part_names = {
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
"教师发展支持力": "part6", "教育质量评估力": "part7",
"教育条件保障力": "part8", "数字化赋能力": "part9"
} %}
{% set part_numbers = {
"课程领导力": "三", "教学变革力": "四", "学生发展指导力": "五",
"教师发展支持力": "六", "教育质量评估力": "七",
"教育条件保障力": "八", "数字化赋能力": "九"
} %}
{% for dim_name, dim_data in dimensions.items() %}
{% set part_id = part_names[dim_name] %}
{% set part_number = part_numbers[dim_name] %}
{% include 'sections/dimension_detail.html' %}
{% endfor %}
{# ===== 总结与建议 ===== #}
{% include 'sections/conclusion.html' %}
</div>
{# ===== ECharts 图表渲染 ===== #}
{% include 'components/charts_common.js.html' %}
{% include 'components/charts_overview.js.html' %}
{% include 'components/charts_dimension.js.html' %}
{% include 'components/charts_innovative.js.html' %}
{# ===== 自动生成目录 + 滚动高亮 ===== #}
<script>
(function() {
/* ---- 1. 自动扫描 h1/h2 生成 TOC ---- */
var track = document.getElementById('toc-track');
var container = document.querySelector('.report-container');
// 取 .section 内的 h1(部分标题)和 h2[id](子维度标题)
var headings = container.querySelectorAll('.section > h1, h2[id]');
var currentGroup = null;
headings.forEach(function(h) {
// 确保有 id 可跳转;h1 取父 .section 的 idh2 自身有 id
var id = '';
if (h.tagName === 'H1') {
var sec = h.closest('.section');
id = sec ? sec.id : '';
} else {
id = h.id;
}
if (!id) return;
var a = document.createElement('a');
a.href = '#' + id;
a.className = 'toc-item';
// 去掉"第X部分 "前缀,只保留核心文字
var text = h.textContent.replace(/^第[一二三四五六七八九十]+部分\s*/, '');
// h2 也去掉"二、""三、"等编号前缀
text = text.replace(/^[一二三四五六七八九十]+、\s*/, '');
a.textContent = text;
if (h.tagName === 'H1') {
a.classList.add('level-1');
a.setAttribute('data-group', id);
currentGroup = id;
} else {
a.classList.add('level-2');
if (currentGroup) a.setAttribute('data-parent', currentGroup);
}
track.appendChild(a);
});
/* ---- 2. 事件绑定 ---- */
var tocItems = track.querySelectorAll('.toc-item');
var level2Items = track.querySelectorAll('.toc-item.level-2');
var sections = [];
tocItems.forEach(function(item) {
var target = document.getElementById(item.getAttribute('href').slice(1));
if (target) sections.push({ el: target, link: item });
// 点击平滑滚动
item.addEventListener('click', function(e) {
e.preventDefault();
target && target.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (window.innerWidth <= 1280) {
document.getElementById('toc-nav').classList.remove('show');
}
});
});
function expandGroup(gid) {
level2Items.forEach(function(item) {
item.classList.toggle('visible', item.getAttribute('data-parent') === gid);
});
}
/* ---- 3. 滚动高亮 ---- */
var ticking = false;
window.addEventListener('scroll', function() {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(function() {
var scrollY = window.scrollY + 150;
var current = null;
for (var i = sections.length - 1; i >= 0; i--) {
if (sections[i].el.offsetTop <= scrollY) { current = sections[i]; break; }
}
tocItems.forEach(function(item) { item.classList.remove('active'); });
if (current) {
current.link.classList.add('active');
expandGroup(current.link.getAttribute('data-group') || current.link.getAttribute('data-parent') || '');
}
ticking = false;
});
});
window.dispatchEvent(new Event('scroll'));
})();
</script>
</body>
</html>
@@ -0,0 +1,18 @@
{# 图表通用常量 #}
<script>
const COLORS = {
primary: '#2563eb',
primaryLight: '#93bbfd',
success: '#059669',
warning: '#d97706',
danger: '#dc2626',
gray: '#d1d5db',
grayDark: '#6b7280',
cluster_good: '#2563eb',
cluster_weak: '#f59e0b',
level1: '#ef4444',
level2: '#f59e0b',
level3: '#3b82f6',
level4: '#10b981',
};
</script>
@@ -0,0 +1,202 @@
{# 各维度详细图表:得分柱状图、子维度雷达图、对比柱状图、水平分布、散点图 #}
<script>
// ===== 8. 各维度独立得分柱状图 =====
const dimScoreBars = {{ dim_score_bar_data | tojson }};
Object.keys(dimScoreBars).forEach(partId => {
const el = document.getElementById('dim-score-' + partId);
if (!el) return;
const chart = echarts.init(el);
const d = dimScoreBars[partId];
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '8%', right: '8%', bottom: '12%', top: '8%', containLabel: true },
xAxis: { type: 'category', data: [d.school_name, '区均值', '同类学校'], axisLabel: { fontSize: 12 } },
yAxis: { type: 'value', min: 30, max: 80 },
series: [{
type: 'bar', barWidth: '40%',
data: [
{ value: d.school_score, itemStyle: { color: COLORS.primary } },
{ value: d.district_avg, itemStyle: { color: COLORS.gray } },
{ value: d.same_type_avg, itemStyle: { color: COLORS.warning } },
],
label: { show: true, position: 'top', fontSize: 13, fontWeight: 'bold', formatter: p => p.value.toFixed(2) },
markLine: { silent: true, data: [{ yAxis: 50, label: { formatter: '均值基线(50)', fontSize: 10 }, lineStyle: { color: '#ef4444', type: 'dashed' } }] },
}],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
});
// ===== 9. 各维度子维度雷达图 =====
const dimSubRadar = {{ dim_sub_radar_data | tojson }};
Object.keys(dimSubRadar).forEach(partId => {
const el = document.getElementById('sub-radar-' + partId);
if (!el) return;
const chart = echarts.init(el);
const d = dimSubRadar[partId];
if (d.type === 'radar' && d.sub_dims && d.sub_dims.length >= 3) {
const option = {
tooltip: {},
legend: { data: [d.school_name, '区均值', '同类学校'], bottom: 5, textStyle: { fontSize: 11 } },
radar: {
indicator: d.sub_dims.map(sd => ({ name: sd, max: 80 })),
shape: 'polygon', splitNumber: 4, axisName: { color: '#333', fontSize: 10 }, radius: '60%',
},
series: [{
type: 'radar',
data: [
{ value: d.school_values, name: d.school_name,
areaStyle: { opacity: 0.3, color: COLORS.primary },
lineStyle: { width: 3, color: COLORS.primary }, itemStyle: { color: COLORS.primary } },
{ value: d.district_avg, name: '区均值',
lineStyle: { width: 1, type: 'dashed', color: COLORS.grayDark }, itemStyle: { color: COLORS.grayDark } },
{ value: d.same_type_avg, name: '同类学校',
lineStyle: { width: 1, type: 'dotted', color: COLORS.warning }, itemStyle: { color: COLORS.warning } },
],
}],
};
chart.setOption(option);
} else {
const option = {
tooltip: { trigger: 'axis' },
legend: { data: [d.school_name, '区均值', '同类学校'], bottom: 5, textStyle: { fontSize: 11 } },
grid: { left: '3%', right: '4%', bottom: '18%', top: '5%', containLabel: true },
xAxis: { type: 'category', data: d.sub_dims, axisLabel: { fontSize: 11 } },
yAxis: { type: 'value', min: 20, max: 80 },
series: [
{ name: d.school_name, type: 'bar', data: d.school_values,
itemStyle: { color: COLORS.primary }, barWidth: '22%',
label: { show: true, position: 'top', fontSize: 10 } },
{ name: '区均值', type: 'bar', data: d.district_avg, itemStyle: { color: COLORS.gray }, barWidth: '22%' },
{ name: '同类学校', type: 'bar', data: d.same_type_avg, itemStyle: { color: COLORS.warning }, barWidth: '22%' },
],
};
chart.setOption(option);
}
window.addEventListener('resize', () => chart.resize());
});
// ===== 10. 各维度子维度柱状图 =====
const subDimCharts = {{ sub_dim_chart_data | tojson }};
Object.keys(subDimCharts).forEach(chartId => {
const el = document.getElementById('chart-' + chartId);
if (!el) return;
const chart = echarts.init(el);
const d = subDimCharts[chartId];
const option = {
tooltip: { trigger: 'axis' },
legend: { data: ['{{ school }}', '区均值'], bottom: 5 },
grid: { left: '3%', right: '4%', bottom: '18%', containLabel: true },
xAxis: { type: 'category', data: d.categories, axisLabel: { rotate: 15, fontSize: 11 } },
yAxis: { type: 'value', min: 20, max: 80 },
series: [
{ name: '{{ school }}', type: 'bar', data: d.school_values,
itemStyle: { color: COLORS.primary }, barWidth: '28%',
label: { show: true, position: 'top', fontSize: 10 } },
{ name: '区均值', type: 'bar', data: d.avg_values, itemStyle: { color: COLORS.gray }, barWidth: '28%' },
{ name: '均值基线(50)', type: 'line', data: d.categories.map(() => 50),
lineStyle: { color: '#ef4444', type: 'dashed', width: 1 }, symbol: 'none' },
],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
});
// ===== 11. 水平分布堆叠条形图 =====
const levelDistData = {{ level_dist_data | tojson }};
Object.keys(levelDistData).forEach(chartId => {
const el = document.getElementById('level-' + chartId);
if (!el) return;
const chart = echarts.init(el);
const d = levelDistData[chartId];
if (!d.categories || d.categories.length === 0) return;
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' },
formatter: function(params) {
let s = params[0].axisValue + '<br/>';
params.forEach(p => { s += p.marker + ' ' + p.seriesName + ': ' + p.value + '%<br/>'; });
const idx = d.categories.indexOf(params[0].axisValue);
if (idx >= 0) s += '<strong>{{ school }}:水平' + d.school_levels[idx] + '</strong>';
return s;
}
},
legend: { data: ['水平一', '水平二', '水平三', '水平四'], bottom: 5, textStyle: { fontSize: 11 } },
grid: { left: '3%', right: '4%', bottom: '18%', top: '5%', containLabel: true },
xAxis: { type: 'category', data: d.categories, axisLabel: { fontSize: 11, rotate: 15 } },
yAxis: { type: 'value', max: 100, axisLabel: { formatter: '{value}%' } },
series: [
{ name: '水平一', type: 'bar', stack: 'total', data: d.level1, itemStyle: { color: COLORS.level1 }, barWidth: '50%' },
{ name: '水平二', type: 'bar', stack: 'total', data: d.level2, itemStyle: { color: COLORS.level2 } },
{ name: '水平三', type: 'bar', stack: 'total', data: d.level3, itemStyle: { color: COLORS.level3 } },
{ name: '水平四', type: 'bar', stack: 'total', data: d.level4, itemStyle: { color: COLORS.level4 } },
],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
});
// ===== 12. 各维度聚类散点图(2D / 3D =====
const dimScatterData = {{ dim_scatter_data | tojson }};
Object.keys(dimScatterData).forEach(partId => {
const el = document.getElementById('scatter-' + partId);
if (!el) return;
const d = dimScatterData[partId];
if (!d || !d.axes) return;
const chart = echarts.init(el);
if (d.type === '3d') {
const axes = d.axes;
const option = {
tooltip: {
formatter: function(params) {
const v = params.value || (params.data && params.data.value);
if (!v) return '';
return params.seriesName + '<br/>'
+ axes[0] + ': ' + (typeof v[0] === 'number' ? v[0].toFixed(2) : v[0]) + '<br/>'
+ axes[1] + ': ' + (typeof v[1] === 'number' ? v[1].toFixed(2) : v[1]) + '<br/>'
+ axes[2] + ': ' + (typeof v[2] === 'number' ? v[2].toFixed(2) : v[2]);
}
},
legend: { data: ['较好类', '待提升类', d.school_name], bottom: 5, textStyle: { fontSize: 11 } },
xAxis3D: { name: d.axes[0], type: 'value', nameTextStyle: { fontSize: 10 } },
yAxis3D: { name: d.axes[1], type: 'value', nameTextStyle: { fontSize: 10 } },
zAxis3D: { name: d.axes[2], type: 'value', nameTextStyle: { fontSize: 10 } },
grid3D: {
viewControl: { autoRotate: true, autoRotateSpeed: 5, distance: 200, alpha: 20, beta: 30 },
boxWidth: 100, boxHeight: 100, boxDepth: 100,
light: { main: { intensity: 1.2, shadow: true }, ambient: { intensity: 0.3 } },
},
series: [
{ name: '较好类', type: 'scatter3D', data: d.good_data.map(p => ({ value: p })),
symbolSize: 8, itemStyle: { color: COLORS.success, opacity: 0.7 } },
{ name: '待提升类', type: 'scatter3D', data: d.weak_data.map(p => ({ value: p })),
symbolSize: 8, itemStyle: { color: COLORS.danger, opacity: 0.7 } },
{ name: d.school_name, type: 'scatter3D',
data: d.school_point ? [{ value: d.school_point }] : [],
symbolSize: 18, itemStyle: { color: COLORS.primary, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: d.school_name, textStyle: { fontSize: 12, fontWeight: 'bold', color: COLORS.primary } } },
],
};
chart.setOption(option);
} else {
const option = {
tooltip: { formatter: p => (p.data && p.data.name) ? p.data.name : d.axes[0] + ': ' + p.value[0] + '<br/>' + d.axes[1] + ': ' + p.value[1] },
legend: { data: ['较好类', '待提升类', d.school_name], bottom: 5, textStyle: { fontSize: 11 } },
grid: { left: '10%', right: '8%', bottom: '18%', top: '5%' },
xAxis: { name: d.axes[0], nameLocation: 'center', nameGap: 30, nameTextStyle: { fontSize: 11 }, splitLine: { show: true, lineStyle: { type: 'dashed' } } },
yAxis: { name: d.axes[1], nameLocation: 'center', nameGap: 40, nameTextStyle: { fontSize: 11 }, splitLine: { show: true, lineStyle: { type: 'dashed' } } },
series: [
{ name: '较好类', type: 'scatter', data: d.good_data, symbolSize: 10, itemStyle: { color: COLORS.success, opacity: 0.6 } },
{ name: '待提升类', type: 'scatter', data: d.weak_data, symbolSize: 10, itemStyle: { color: COLORS.danger, opacity: 0.6 } },
{ name: d.school_name, type: 'scatter',
data: d.school_point ? [{ value: d.school_point, name: d.school_name, symbolSize: 22 }] : [],
symbolSize: 22, itemStyle: { color: COLORS.primary, borderColor: '#fff', borderWidth: 3 },
label: { show: true, formatter: d.school_name, position: 'top', textStyle: { fontSize: 13, fontWeight: 'bold', color: COLORS.primary } } },
],
};
chart.setOption(option);
}
window.addEventListener('resize', () => chart.resize());
});
</script>
@@ -0,0 +1,384 @@
<script>
// ===== A. 学校画像卡(仪表盘 + 红绿灯) =====
const profileData = {{ profile_card_data | tojson }};
if (profileData && profileData.total_score) {
(function() {
const chart = echarts.init(document.getElementById('profile-card-chart'));
const score = profileData.total_score;
const districtAvg = profileData.district_avg;
const signals = profileData.dim_signals;
const levelCounts = profileData.level_counts;
// 信号灯颜色映射
const signalColor = function(minLevel) {
if (minLevel >= 4) return '#10b981';
if (minLevel >= 3) return '#3b82f6';
if (minLevel >= 2) return '#f59e0b';
return '#ef4444';
};
const option = {
title: [
{ text: profileData.school_name, left: 'center', top: 6, textStyle: { fontSize: 16, fontWeight: 'bold', color: '#1e3a5f' } },
{ text: profileData.school_type + ' · 区内第' + profileData.rank + '名', left: 'center', top: 28, textStyle: { fontSize: 11, color: '#6b7280' } },
],
series: [
// 中央仪表盘
{
type: 'gauge',
center: ['50%', '60%'],
radius: '55%',
startAngle: 210,
endAngle: -30,
min: 30,
max: 70,
splitNumber: 4,
itemStyle: { color: score >= districtAvg ? '#2563eb' : '#f59e0b' },
progress: { show: true, width: 18, roundCap: true },
pointer: { show: false },
axisLine: { lineStyle: { width: 18, color: [[0.25, '#fee2e2'], [0.5, '#fef3c7'], [0.75, '#dbeafe'], [1, '#d1fae5']] } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: true, distance: 25, fontSize: 10, color: '#6b7280',
formatter: function(v) { return v.toFixed(0); } },
detail: {
valueAnimation: true, offsetCenter: [0, '-5%'],
fontSize: 36, fontWeight: 'bold', color: '#1e3a5f',
formatter: function(v) { return v.toFixed(1); },
},
data: [{ value: score }],
},
// 底部饼图:20个三级维度的水平分布
{
type: 'pie',
center: ['50%', '92%'],
radius: ['0%', '0%'], // 隐藏饼图,只用graphic展示
silent: true,
data: [],
},
],
graphic: (function() {
var items = [];
// 7个维度信号灯(底部横排)
var startX = 50 - (signals.length - 1) * 6;
signals.forEach(function(sig, i) {
var x = startX + i * 12;
// 圆点
items.push({
type: 'circle', shape: { cx: 0, cy: 0, r: 8 },
left: x + '%', top: '78%',
style: { fill: signalColor(sig.min_level), shadowBlur: 6, shadowColor: signalColor(sig.min_level) },
});
// 标签
items.push({
type: 'text',
left: x + '%', top: '84%',
style: {
text: sig.name.replace('力', ''),
textAlign: 'center', fontSize: 9, fill: '#374151',
},
});
});
// 水平分布摘要文字
var summaryText = '水平分布: ';
summaryText += 'Lv4=' + levelCounts[4] + ' Lv3=' + levelCounts[3] + ' Lv2=' + levelCounts[2] + ' Lv1=' + levelCounts[1];
items.push({
type: 'text',
left: 'center', top: '93%',
style: { text: summaryText, textAlign: 'center', fontSize: 10, fill: '#6b7280' },
});
return items;
})(),
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
// ===== B. 优势-短板象限图 =====
const quadrantData = {{ quadrant_data | tojson }};
if (quadrantData && quadrantData.items && quadrantData.items.length > 0) {
(function() {
const chart = echarts.init(document.getElementById('quadrant-chart'));
const items = quadrantData.items;
// 颜色映射(按父维度)
const dimColors = {
'课程领导力': '#2563eb', '教学变革力': '#7c3aed', '学生发展指导力': '#059669',
'教师发展支持力': '#d97706', '教育质量评估力': '#dc2626',
'教育条件保障力': '#0891b2', '数字化赋能力': '#be185d',
};
const scatterData = items.map(function(item) {
return {
value: [item.score, item.diff],
name: item.name,
parent: item.parent,
level: item.level,
itemStyle: { color: dimColors[item.parent] || '#6b7280' },
};
});
const option = {
tooltip: {
formatter: function(p) {
var d = p.data;
return '<strong>' + d.name + '</strong> (' + d.parent + ')<br/>' +
'得分: ' + d.value[0] + '分<br/>' +
'差异: ' + (d.value[1] > 0 ? '+' : '') + d.value[1] + '分<br/>' +
'水平: ' + d.level;
}
},
grid: { left: '12%', right: '5%', bottom: '12%', top: '8%' },
xAxis: {
name: '得分', nameLocation: 'center', nameGap: 28,
nameTextStyle: { fontSize: 12 },
min: 20, max: 80,
splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e5e7eb' } },
},
yAxis: {
name: '与区均值差异', nameLocation: 'center', nameGap: 40,
nameTextStyle: { fontSize: 12 },
splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e5e7eb' } },
},
series: [{
type: 'scatter',
data: scatterData,
symbolSize: function(val, params) { return Math.abs(val[1]) * 1.5 + 10; },
label: {
show: true, position: 'right', fontSize: 9,
formatter: function(p) {
var n = p.data.name;
return n.length > 5 ? n.substring(0, 5) + '…' : n;
},
color: '#374151',
},
}],
// 象限标注
graphic: [
// 均值十字线
{ type: 'line', shape: { x1: 0, y1: 0, x2: 0, y2: 0 }, silent: true },
// 象限标签
{ type: 'text', right: '8%', top: '10%',
style: { text: '✦ 核心优势', fill: '#059669', fontSize: 11, fontWeight: 'bold' } },
{ type: 'text', left: '14%', bottom: '14%',
style: { text: '⚠ 急需改进', fill: '#dc2626', fontSize: 11, fontWeight: 'bold' } },
{ type: 'text', right: '8%', bottom: '14%',
style: { text: '⊙ 隐性风险', fill: '#d97706', fontSize: 11, fontWeight: 'bold' } },
{ type: 'text', left: '14%', top: '10%',
style: { text: '↗ 潜力项', fill: '#2563eb', fontSize: 11, fontWeight: 'bold' } },
],
// 均值参考线
markLine: {},
};
// 加十字参考线
option.series[0].markLine = {
silent: true, symbol: 'none',
lineStyle: { color: '#9ca3af', type: 'dashed', width: 1 },
data: [
{ xAxis: 50, label: { formatter: '区均值', fontSize: 10, position: 'start' } },
{ yAxis: 0, label: { show: false } },
],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
// ===== C. 温度计条形图 =====
const thermoData = {{ thermometer_data | tojson }};
if (thermoData && thermoData.data) {
(function() {
// 为每个 thermo-{partId}-{subIndex} 容器渲染
Object.keys(thermoData.data).forEach(function(sdName) {
var d = thermoData.data[sdName];
// 查找匹配的容器(遍历所有 thermo- 开头的元素)
var allThermos = document.querySelectorAll('[id^="thermo-"]');
allThermos.forEach(function(el) {
if (el.getAttribute('data-rendered')) return;
// 找到未渲染的容器,按顺序匹配
});
});
// 用更简单的方式:直接在维度渲染时按 partId 和 subIndex 匹配
var partSubMap = {};
{% for dim_name, dim_data in dimensions.items() %}
{% set part_names_map = {"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5", "教师发展支持力": "part6", "教育质量评估力": "part7", "教育条件保障力": "part8", "数字化赋能力": "part9"} %}
{% set pid = part_names_map[dim_name] %}
{% for sub_dim in framework[dim_name].sub_dimensions %}
{% set sd = sub_dimensions.get(sub_dim, {}) %}
{% if sd %}
partSubMap['{{ pid }}-{{ loop.index }}'] = '{{ sub_dim }}';
{% endif %}
{% endfor %}
{% endfor %}
Object.keys(partSubMap).forEach(function(key) {
var sdName = partSubMap[key];
var d = thermoData.data[sdName];
if (!d) return;
var el = document.getElementById('thermo-' + key);
if (!el) return;
var chart = echarts.init(el);
var score = d.score;
var distAvg = d.district_avg;
var sameTypeAvg = d.same_type_avg;
var th = d.thresholds;
var option = {
grid: { left: '3%', right: '3%', top: '25%', bottom: '25%' },
xAxis: {
type: 'value', min: 20, max: 80,
axisLabel: { show: false },
axisTick: { show: false },
axisLine: { show: false },
splitLine: { show: false },
},
yAxis: {
type: 'category', data: [''],
axisLabel: { show: false },
axisTick: { show: false },
axisLine: { show: false },
},
series: [
// 背景条(水平区间着色:低分端=水平四红色 → 高分端=水平一绿色)
{ type: 'bar', data: [th.level2 - 20], stack: 'bg', barWidth: '60%',
itemStyle: { color: '#fee2e2', borderRadius: [4, 0, 0, 4] }, silent: true },
{ type: 'bar', data: [th.level3 - th.level2], stack: 'bg', barWidth: '60%',
itemStyle: { color: '#fef3c7' }, silent: true },
{ type: 'bar', data: [th.level4 - th.level3], stack: 'bg', barWidth: '60%',
itemStyle: { color: '#dbeafe' }, silent: true },
{ type: 'bar', data: [80 - th.level4], stack: 'bg', barWidth: '60%',
itemStyle: { color: '#d1fae5', borderRadius: [0, 4, 4, 0] }, silent: true },
],
// 指针标注
graphic: (function() {
var items = [];
var toPercent = function(val) { return ((val - 20) / 60 * 94 + 3) + '%'; };
// 本校得分指针(三角形 + 标签)
items.push({
type: 'text', left: toPercent(score), top: '2%',
style: { text: '▼ ' + score, textAlign: 'center', fontSize: 11, fontWeight: 'bold',
fill: '#1e3a5f' },
});
// 区均值指针
items.push({
type: 'text', left: toPercent(distAvg), bottom: '0%',
style: { text: '▲区均', textAlign: 'center', fontSize: 9, fill: '#6b7280' },
});
// 同类学校均值指针
if (Math.abs(sameTypeAvg - distAvg) > 1) {
items.push({
type: 'text', left: toPercent(sameTypeAvg), bottom: '0%',
style: { text: '▲同类', textAlign: 'center', fontSize: 9, fill: '#d97706' },
});
}
return items;
})(),
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
});
})();
}
// ===== D. 进步空间瀑布图 =====
const waterfallData = {{ waterfall_data | tojson }};
if (waterfallData && waterfallData.steps && waterfallData.steps.length > 1) {
(function() {
var el = document.getElementById('waterfall-chart');
if (!el) return;
var chart = echarts.init(el);
var steps = waterfallData.steps;
var categories = [];
var baseData = []; // 透明基底
var gainData = []; // 增长部分
var totalData = []; // 起止柱
var runningBase = 0;
steps.forEach(function(step, i) {
var shortName = step.name.length > 6 ? step.name.substring(0, 6) + '…' : step.name;
categories.push(shortName);
if (step.type === 'current') {
baseData.push(0);
gainData.push(0);
totalData.push(step.value);
runningBase = step.value;
} else if (step.type === 'gain') {
baseData.push(runningBase);
gainData.push(step.value);
totalData.push(0);
runningBase += step.value;
} else if (step.type === 'potential') {
baseData.push(0);
gainData.push(0);
totalData.push(step.value);
}
});
var option = {
tooltip: {
trigger: 'axis', axisPointer: { type: 'shadow' },
formatter: function(params) {
var idx = params[0].dataIndex;
var step = steps[idx];
if (step.type === 'current') return '当前总分: <strong>' + step.value + '</strong>分';
if (step.type === 'potential') return '潜在总分: <strong>' + step.value + '</strong>分 (+' + waterfallData.total_gain + ')';
return '<strong>' + step.name + '</strong><br/>' +
(step.detail || '') + '<br/>预估总分贡献: +' + step.value + '分';
},
},
grid: { left: '5%', right: '5%', bottom: '18%', top: '8%', containLabel: true },
xAxis: {
type: 'category', data: categories,
axisLabel: { fontSize: 10, rotate: 20 },
},
yAxis: {
type: 'value',
min: Math.floor(waterfallData.current_total - 2),
max: Math.ceil(waterfallData.potential_total + 2),
axisLabel: { fontSize: 11 },
},
series: [
// 透明基底
{
type: 'bar', stack: 'waterfall', data: baseData,
itemStyle: { color: 'transparent' }, barWidth: '45%',
emphasis: { itemStyle: { color: 'transparent' } },
},
// 增长部分(绿色)
{
type: 'bar', stack: 'waterfall', data: gainData,
itemStyle: { color: '#10b981', borderRadius: [4, 4, 0, 0] },
barWidth: '45%',
label: {
show: true, position: 'top', fontSize: 10, color: '#059669',
formatter: function(p) { return p.value > 0 ? '+' + p.value.toFixed(2) : ''; },
},
},
// 起止柱(蓝色)
{
type: 'bar', data: totalData,
itemStyle: {
color: function(p) {
return p.dataIndex === 0 ? '#2563eb' : '#0891b2';
},
borderRadius: [4, 4, 0, 0],
},
barWidth: '45%',
label: {
show: true, position: 'top', fontSize: 12, fontWeight: 'bold',
color: '#1e3a5f',
formatter: function(p) { return p.value > 0 ? p.value.toFixed(1) : ''; },
},
},
],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
</script>
@@ -0,0 +1,208 @@
{# 第一部分:总体概览的所有图表 #}
<script>
// ===== 1. 得分对比横向条形图 =====
const scoreCompare = {{ score_compare_data | tojson }};
(function() {
const chart = echarts.init(document.getElementById('score-compare-chart'));
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: [scoreCompare.school_name, '区均值', '同类学校均值'], bottom: 5, textStyle: { fontSize: 12 } },
grid: { left: '3%', right: '8%', bottom: '18%', top: '5%', containLabel: true },
xAxis: { type: 'value', min: 30, max: 80 },
yAxis: { type: 'category', data: scoreCompare.categories, axisLabel: { fontSize: 12 } },
series: [
{ name: scoreCompare.school_name, type: 'bar', data: scoreCompare.school_values,
itemStyle: { color: COLORS.primary }, barWidth: '22%',
label: { show: true, position: 'right', fontSize: 11 } },
{ name: '区均值', type: 'bar', data: scoreCompare.district_avg,
itemStyle: { color: COLORS.gray }, barWidth: '22%' },
{ name: '同类学校均值', type: 'bar', data: scoreCompare.same_type_avg,
itemStyle: { color: COLORS.warning }, barWidth: '22%' },
],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
// ===== 2. 七维度雷达图 =====
const radarData = {{ radar_data | tojson }};
(function() {
const chart = echarts.init(document.getElementById('radar-chart'));
const option = {
tooltip: {},
legend: { data: radarData.legend, bottom: 10 },
radar: {
indicator: radarData.dimensions.map(d => ({ name: d, max: 80 })),
shape: 'polygon', splitNumber: 4,
axisName: { color: '#333', fontSize: 12 }, radius: '65%',
},
series: [{
type: 'radar',
data: radarData.series.map((s, i) => ({
value: s.values, name: s.name,
areaStyle: { opacity: i === 0 ? 0.3 : 0.1 },
lineStyle: { width: i === 0 ? 3 : 1 },
})),
}],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
// ===== 3. 聚类类型分布饼图 =====
const clusterDist = {{ cluster_type_dist_data | tojson }};
if (clusterDist.total) {
(function() {
const chart = echarts.init(document.getElementById('cluster-type-dist-chart'));
const option = {
tooltip: { trigger: 'item' },
legend: { bottom: 5, textStyle: { fontSize: 11 } },
series: [{
type: 'pie', radius: ['35%', '60%'], center: ['50%', '45%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{c}所 ({d}%)', fontSize: 12 },
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
data: [
{ value: clusterDist.good_count, name: '课程实施较好类', itemStyle: { color: COLORS.primary } },
{ value: clusterDist.weak_count, name: '课程实施待提升类', itemStyle: { color: COLORS.warning } },
]
}],
graphic: [{
type: 'text', left: 'center', top: '40%',
style: {
text: clusterDist.school_name + '\n属于' + (clusterDist.school_cluster === '较好' ? '较好类' : '待提升类'),
textAlign: 'center', fill: COLORS.primary, fontSize: 12, fontWeight: 'bold',
}
}],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
// ===== 4. 两类学校折线对比图 =====
const clusterLine = {{ cluster_line_compare_data | tojson }};
if (clusterLine.dimensions) {
(function() {
const chart = echarts.init(document.getElementById('cluster-line-chart'));
const option = {
tooltip: { trigger: 'axis' },
legend: {
data: ['课程实施较好类(' + clusterLine.good_count + '所)', '课程实施待提升类(' + clusterLine.weak_count + '所)'],
bottom: 5, textStyle: { fontSize: 11 }
},
grid: { left: '3%', right: '4%', bottom: '18%', top: '8%', containLabel: true },
xAxis: {
type: 'category', data: clusterLine.dimensions.map(d => d.replace('力', '')),
axisLabel: { fontSize: 11, rotate: 15 }, boundaryGap: false,
},
yAxis: { type: 'value', min: 40, max: 60 },
series: [
{
name: '课程实施较好类(' + clusterLine.good_count + '所)',
type: 'line', data: clusterLine.good_values,
lineStyle: { width: 3, color: COLORS.primary }, itemStyle: { color: COLORS.primary },
symbol: 'circle', symbolSize: 8,
label: { show: true, position: 'top', fontSize: 10, color: COLORS.primary, formatter: p => p.value.toFixed(2) },
},
{
name: '课程实施待提升类(' + clusterLine.weak_count + '所)',
type: 'line', data: clusterLine.weak_values,
lineStyle: { width: 3, color: COLORS.warning }, itemStyle: { color: COLORS.warning },
symbol: 'circle', symbolSize: 8,
label: { show: true, position: 'bottom', fontSize: 10, color: COLORS.warning, formatter: p => p.value.toFixed(2) },
},
],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
// ===== 5. 聚类类型对比雷达图 =====
const clusterRadar = {{ cluster_radar_data | tojson }};
if (clusterRadar.dimensions) {
(function() {
const chart = echarts.init(document.getElementById('cluster-radar-chart'));
const colorMap = [COLORS.cluster_good, COLORS.cluster_weak, COLORS.danger];
const option = {
tooltip: {},
legend: {
data: clusterRadar.legend, bottom: 5, textStyle: { fontSize: 11 },
formatter: function(name) {
if (name === '课程实施较好类') return name + '(' + clusterRadar.good_count + '所)';
if (name === '课程实施待提升类') return name + '(' + clusterRadar.weak_count + '所)';
return name;
}
},
radar: {
indicator: clusterRadar.dimensions.map(d => ({ name: d, max: 80 })),
shape: 'polygon', splitNumber: 4,
axisName: { color: '#333', fontSize: 11 }, radius: '60%',
},
series: [{
type: 'radar',
data: clusterRadar.series.map((s, i) => ({
value: s.values, name: s.name,
areaStyle: { opacity: i === 2 ? 0.05 : 0.15 },
lineStyle: { width: i === 2 ? 3 : 2, type: i === 2 ? 'solid' : 'dashed', color: colorMap[i] },
itemStyle: { color: colorMap[i] },
symbol: i === 2 ? 'diamond' : 'circle', symbolSize: i === 2 ? 8 : 4,
})),
}],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
// ===== 6. 区内学校排名条形图 =====
const rankingData = {{ school_ranking_data | tojson }};
if (rankingData.schools) {
(function() {
const chart = echarts.init(document.getElementById('school-ranking-chart'));
const barColors = rankingData.schools.map(s => s === rankingData.current_school ? COLORS.primary : COLORS.gray);
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '3%', right: '12%', bottom: '5%', top: '5%', containLabel: true },
xAxis: { type: 'value', min: 35, max: 65 },
yAxis: { type: 'category', data: rankingData.schools.slice().reverse(),
axisLabel: { fontSize: 12, fontWeight: function(v) { return v === rankingData.current_school ? 'bold' : 'normal'; } } },
series: [{
type: 'bar', barWidth: '50%',
data: rankingData.total_scores.slice().reverse().map((v, i) => ({
value: v, itemStyle: { color: barColors.slice().reverse()[i] },
})),
label: { show: true, position: 'right', fontSize: 12, fontWeight: 'bold', formatter: p => p.value.toFixed(1) },
}],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
// ===== 7. 维度间相关性热力图 =====
const corrData = {{ correlation_data | tojson }};
if (corrData.dimensions) {
(function() {
const chart = echarts.init(document.getElementById('correlation-chart'));
const names = corrData.short_names || corrData.dimensions;
const option = {
tooltip: { position: 'top', formatter: p => corrData.dimensions[p.value[0]] + ' × ' + corrData.dimensions[p.value[1]] + '<br/>相关系数: ' + p.value[2] },
grid: { left: '15%', right: '12%', bottom: '18%', top: '3%' },
xAxis: { type: 'category', data: names, axisLabel: { rotate: 30, fontSize: 11 }, splitArea: { show: true } },
yAxis: { type: 'category', data: names, axisLabel: { fontSize: 11 }, splitArea: { show: true } },
visualMap: { min: -1, max: 1, calculable: true, orient: 'vertical', right: 0, top: 'center',
inRange: { color: ['#fee2e2', '#fef3c7', '#f3f4f6', '#dbeafe', '#2563eb'] }, textStyle: { fontSize: 11 } },
series: [{
type: 'heatmap', data: corrData.data,
label: { show: true, fontSize: 11, formatter: p => p.value[2].toFixed(2) },
itemStyle: { borderWidth: 2, borderColor: '#fff' },
}],
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
})();
}
</script>
+375
View File
@@ -0,0 +1,375 @@
<style>
:root {
--primary: #1a56db;
--primary-light: #e8effc;
--success: #059669;
--success-light: #d1fae5;
--warning: #d97706;
--warning-light: #fef3c7;
--danger: #dc2626;
--danger-light: #fee2e2;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-500: #6b7280;
--gray-700: #374151;
--gray-900: #111827;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: var(--gray-900);
background: var(--gray-50);
line-height: 1.8;
font-size: 15px;
}
.report-container {
max-width: 1100px;
margin: 0 auto;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* 封面 */
.cover {
text-align: center;
padding: 80px 60px;
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
color: white;
}
.cover h1 { font-size: 32px; margin-bottom: 16px; font-weight: 700; }
.cover .subtitle { font-size: 18px; opacity: 0.9; margin-bottom: 40px; }
.cover .school-name { font-size: 42px; font-weight: 700; margin: 30px 0; letter-spacing: 4px; }
.cover .date { font-size: 16px; opacity: 0.7; margin-top: 40px; }
/* 正文 */
.section { padding: 40px 60px; page-break-before: auto; }
.section h1 {
font-size: 26px; color: var(--primary); margin-bottom: 24px;
padding-bottom: 12px; border-bottom: 3px solid var(--primary);
}
.section h2 { font-size: 22px; color: var(--gray-900); margin: 28px 0 16px; }
.section h3 { font-size: 18px; color: var(--gray-700); margin: 20px 0 12px; }
.section p { margin: 10px 0; text-indent: 2em; text-align: justify; }
/* 得分卡片 */
.score-cards {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px; margin: 20px 0;
}
.score-card {
background: var(--gray-50); border-radius: 12px; padding: 20px;
text-align: center; border: 1px solid var(--gray-200);
transition: transform 0.2s, box-shadow 0.2s;
}
.score-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.score-card .label { font-size: 13px; color: var(--gray-500); margin-bottom: 8px; }
.score-card .value { font-size: 32px; font-weight: 700; color: var(--primary); }
.score-card .diff { font-size: 13px; margin-top: 4px; }
.score-card .diff.positive { color: var(--success); }
.score-card .diff.negative { color: var(--danger); }
/* 图表容器 */
.chart-box {
width: 100%; margin: 20px 0; background: white;
border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px;
}
.chart-box .chart { width: 100%; height: 400px; }
.chart-box .chart-title {
font-size: 14px; color: var(--gray-500); text-align: center;
margin-top: 8px; font-weight: 500;
}
/* 双图并排 */
.chart-row {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0;
}
.chart-row .chart-box { margin: 0; }
.chart-row .chart-box .chart { height: 350px; }
/* 三图并排 */
.chart-row-3 {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin: 20px 0;
}
.chart-row-3 .chart-box { margin: 0; }
.chart-row-3 .chart-box .chart { height: 300px; }
/* ====== 金字塔水平指示器 ====== */
.pyramid-container {
display: flex; flex-direction: column; align-items: center;
margin: 16px 0; padding: 20px;
}
.pyramid-row {
position: relative;
display: flex; align-items: center; justify-content: center;
margin: 3px 0;
width: 100%;
}
.pyramid-block {
height: 38px; display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 13px; color: white;
border-radius: 4px; transition: all 0.3s;
letter-spacing: 1px;
}
.pyramid-block.lv4 { width: 140px; background: linear-gradient(135deg, #059669, #10b981); }
.pyramid-block.lv3 { width: 200px; background: linear-gradient(135deg, #2563eb, #3b82f6); }
.pyramid-block.lv2 { width: 260px; background: linear-gradient(135deg, #d97706, #f59e0b); }
.pyramid-block.lv1 { width: 320px; background: linear-gradient(135deg, #dc2626, #ef4444); }
.pyramid-block.active {
box-shadow: 0 0 0 3px white, 0 0 0 6px var(--primary);
transform: scale(1.05); z-index: 2;
}
.pyramid-block.dimmed { opacity: 0.35; }
/* 标注用绝对定位,不影响色块居中 */
.pyramid-label {
position: absolute;
left: calc(50% + 175px); /* 最宽块320/2=160,再留15px间距 */
font-size: 13px; font-weight: 700;
color: var(--primary); white-space: nowrap;
animation: arrowPulse 1.5s infinite;
}
.pyramid-label .arrow { margin-right: 4px; }
@keyframes arrowPulse {
0%, 100% { opacity: 1; transform: translateX(0); }
50% { opacity: 0.6; transform: translateX(4px); }
}
/* 水平指示器(行内) */
.level-indicator {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 14px; border-radius: 20px; font-size: 14px; font-weight: 600;
}
.level-indicator.level-4 { background: #d1fae5; color: #065f46; }
.level-indicator.level-3 { background: #dbeafe; color: #1e40af; }
.level-indicator.level-2 { background: #fef3c7; color: #92400e; }
.level-indicator.level-1 { background: #fee2e2; color: #991b1b; }
/* 水平对比表 */
.level-table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; }
.level-table th {
background: var(--primary); color: white; padding: 10px 14px;
text-align: center; font-weight: 600;
}
.level-table td { padding: 10px 14px; text-align: center; border: 1px solid var(--gray-200); font-weight: 600; }
.level-table .lv4 { background: #d1fae5; color: #065f46; }
.level-table .lv3 { background: #dbeafe; color: #1e40af; }
.level-table .lv2 { background: #fef3c7; color: #92400e; }
.level-table .lv1 { background: #fee2e2; color: #991b1b; }
/* 表格 */
.data-table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; }
.data-table th {
background: var(--primary); color: white; padding: 10px 14px;
text-align: center; font-weight: 600;
}
.data-table td { padding: 10px 14px; text-align: center; border-bottom: 1px solid var(--gray-200); }
.data-table tr:nth-child(even) { background: var(--gray-50); }
.data-table tr:hover { background: var(--primary-light); }
/* LLM分析区域 */
.llm-analysis {
background: var(--gray-50); border-left: 4px solid var(--primary);
padding: 20px 24px; margin: 16px 0; border-radius: 0 8px 8px 0;
}
.llm-analysis p { text-indent: 2em; margin: 8px 0; }
.llm-analysis h3 { color: var(--primary); margin: 12px 0 8px; }
.llm-analysis h4 { color: var(--gray-700); margin: 10px 0 6px; }
.llm-analysis ul { margin: 8px 0 8px 2em; }
.llm-analysis li { margin: 4px 0; }
.llm-analysis strong { color: var(--primary); }
/* 图表解读说明(轻量版,紧贴图表下方) */
.chart-caption {
background: linear-gradient(135deg, #f0f4ff 0%, #f8fafc 100%);
border: 1px solid var(--gray-200);
border-top: none;
padding: 12px 18px;
margin: -4px 0 20px 0;
border-radius: 0 0 8px 8px;
font-size: 13.5px;
color: var(--gray-700);
line-height: 1.7;
}
.chart-caption::before {
content: '▎';
color: var(--primary);
font-weight: 700;
margin-right: 6px;
}
.chart-caption p {
display: inline;
margin: 0;
text-indent: 0;
}
/* 分隔线 */
.section-divider {
border: none; border-top: 2px dashed var(--gray-200);
margin: 30px 0;
}
/* 类型标签 */
.type-badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 16px; border-radius: 20px; font-size: 14px; font-weight: 600;
}
.type-badge.good {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #065f46; border: 1px solid #6ee7b7;
}
.type-badge.weak {
background: linear-gradient(135deg, #fef3c7, #fde68a);
color: #92400e; border: 1px solid #fcd34d;
}
/* ====== 右侧目录导航(极简现代风) ====== */
.toc-nav {
position: fixed;
top: 50%; right: 20px;
transform: translateY(-50%);
width: 180px;
max-height: 75vh;
overflow-y: auto;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 12px;
padding: 14px 0;
z-index: 1000;
opacity: 0.45;
transition: opacity 0.3s, box-shadow 0.3s;
}
.toc-nav:hover {
opacity: 1;
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
}
.toc-nav::-webkit-scrollbar { width: 0; }
/* 左侧细线轨道 */
.toc-track {
position: relative;
padding-left: 2px;
}
.toc-track::before {
content: '';
position: absolute; left: 14px; top: 0; bottom: 0;
width: 1.5px;
background: var(--gray-200);
}
.toc-item {
display: block;
position: relative;
padding: 4px 14px 4px 28px;
color: var(--gray-500);
text-decoration: none;
font-size: 12px;
line-height: 1.6;
transition: color 0.2s;
cursor: pointer;
}
/* 轨道上的圆点 */
.toc-item::before {
content: '';
position: absolute; left: 11px; top: 50%;
width: 5px; height: 5px;
border-radius: 50%;
background: var(--gray-300);
transform: translateY(-50%);
transition: all 0.25s;
}
.toc-item:hover { color: var(--gray-900); }
.toc-item:hover::before { background: var(--gray-500); }
.toc-item.active {
color: var(--gray-900);
font-weight: 600;
}
.toc-item.active::before {
background: var(--primary);
width: 7px; height: 7px;
box-shadow: 0 0 0 3px rgba(26,86,219,0.15);
}
.toc-item.level-1 {
font-size: 12px; font-weight: 500;
padding-top: 6px; padding-bottom: 2px;
color: var(--gray-700);
}
.toc-item.level-2 {
font-size: 11px; font-weight: 400;
padding-left: 36px;
color: var(--gray-400);
max-height: 0; overflow: hidden;
padding-top: 0; padding-bottom: 0;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s;
opacity: 0;
}
.toc-item.level-2.visible {
max-height: 30px;
padding-top: 3px; padding-bottom: 3px;
opacity: 1;
}
.toc-item.level-2::before { left: 19px; width: 4px; height: 4px; }
.toc-item.level-2.active::before { width: 6px; height: 6px; }
/* 折叠按钮 */
.toc-toggle {
position: fixed;
bottom: 24px; right: 24px;
width: 40px; height: 40px;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(8px);
color: var(--gray-700);
border: 1px solid var(--gray-200);
border-radius: 10px;
cursor: pointer; z-index: 1001;
font-size: 18px;
display: none;
align-items: center; justify-content: center;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
transition: background 0.2s, box-shadow 0.2s;
}
.toc-toggle:hover {
background: white;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
}
/* 打印样式 */
@media print {
body { background: white; }
.report-container { box-shadow: none; max-width: none; }
.section { page-break-inside: avoid; }
.chart-box { page-break-inside: avoid; }
.chart-row { grid-template-columns: 1fr 1fr; }
.toc-nav, .toc-toggle { display: none !important; }
}
@media (max-width: 1400px) {
.toc-nav { width: 160px; right: 12px; }
}
@media (max-width: 1280px) {
.toc-nav { display: none; }
.toc-toggle { display: flex; }
.toc-nav.show {
display: block; right: 12px; bottom: 72px; top: auto;
transform: none; max-height: 65vh;
opacity: 1; box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
}
@media (max-width: 768px) {
.chart-row { grid-template-columns: 1fr; }
.chart-row-3 { grid-template-columns: 1fr; }
.section { padding: 20px 24px; }
.cover { padding: 40px 24px; }
.cover .school-name { font-size: 28px; }
.toc-nav { display: none; }
.toc-toggle { display: flex; }
.toc-nav.show { display: block; width: 170px; }
}
</style>
+19
View File
@@ -0,0 +1,19 @@
{# 总结与建议 #}
<div class="section" id="conclusion">
<h1>第十部分 总结与改进建议</h1>
<!-- D. 进步空间瀑布图 -->
{% if waterfall_data and waterfall_data.improvements %}
<div class="chart-box">
<div id="waterfall-chart" class="chart" style="height:400px"></div>
<div class="chart-title">图10-1 进步空间分析:提升至水平3的潜在收益</div>
</div>
<div class="chart-caption">
{{ llm_sections.get('part10_caption_waterfall', '') | safe }}
</div>
{% endif %}
<div class="llm-analysis">
{{ llm_sections.get('part10_conclusion', '<p>分析加载中...</p>') | safe }}
</div>
</div>
+8
View File
@@ -0,0 +1,8 @@
{# 封面 #}
<div class="cover">
<h1>课程实施监测数据分析报告</h1>
<div class="subtitle">基于学校领导力视角的七维度分析</div>
<div class="school-name">{{ school }}</div>
<div class="subtitle">长宁区 · {{ school_info.get('type', '') }}</div>
<div class="date">{{ generation_date }}</div>
</div>
@@ -0,0 +1,121 @@
{# 单个维度详细分析(在 for 循环内 include #}
{# 需要外部传入: dim_name, dim_data, part_id, part_number #}
<div class="section" id="{{ part_id }}">
<h1>第{{ part_number }}部分 {{ dim_name }}表现</h1>
<h2>一、整体表现</h2>
<div class="score-cards">
<div class="score-card">
<div class="label">{{ dim_name }}得分</div>
<div class="value">{{ "%.1f"|format(dim_data.score) }}</div>
<div class="diff {{ 'positive' if dim_data.diff_district > 0 else 'negative' }}">
{{ "%+.1f"|format(dim_data.diff_district) }} vs 区均值
</div>
</div>
<div class="score-card">
<div class="label">同类学校均值</div>
<div class="value" style="font-size:24px">{{ "%.1f"|format(dim_data.same_type_avg) }}</div>
</div>
<div class="score-card">
<div class="label">区内排名</div>
<div class="value">{{ dim_data.rank_in_district }}/{{ overall.total_schools }}</div>
</div>
<div class="score-card">
<div class="label">维度类型</div>
<div class="value" style="font-size:14px">
<span class="type-badge {{ 'good' if '较好' in (dim_data.get('cluster', '') or '') else 'weak' }}">
{{ dim_data.get('cluster', '-') }}
</span>
</div>
</div>
</div>
{% set part_num = part_id | replace('part', '') %}
<!-- 维度独立得分柱状图 + 子维度雷达/柱状图 -->
<div class="chart-row">
<div class="chart-box">
<div id="dim-score-{{ part_id }}" class="chart"></div>
<div class="chart-title">图{{ part_num }}-1 {{ dim_name }}得分情况</div>
</div>
<div class="chart-box">
<div id="sub-radar-{{ part_id }}" class="chart"></div>
<div class="chart-title">图{{ part_num }}-2 {{ dim_name }}各子维度得分情况</div>
</div>
</div>
<div class="chart-caption">
{{ llm_sections.get(part_id + '_caption_dim_score', '') | safe }}
</div>
<!-- 子维度柱状图 + 水平分布堆叠图 -->
<div class="chart-row">
<div class="chart-box">
<div id="chart-{{ part_id }}" class="chart"></div>
<div class="chart-title">图{{ part_num }}-3 {{ dim_name }}各子维度对比详情</div>
</div>
<div class="chart-box">
<div id="level-{{ part_id }}" class="chart"></div>
<div class="chart-title">图{{ part_num }}-4 {{ dim_name }}各子维度水平分布</div>
</div>
</div>
<div class="chart-caption">
{{ llm_sections.get(part_id + '_caption_sub_detail', '') | safe }}
</div>
<!-- 聚类散点图(2D或3D -->
<div class="chart-box">
<div id="scatter-{{ part_id }}" class="chart" style="height:450px"></div>
<div class="chart-title">图{{ part_num }}-5 {{ dim_name }}聚类分析散点图</div>
</div>
<div class="chart-caption">
{{ llm_sections.get(part_id + '_caption_scatter', '') | safe }}
</div>
<!-- 维度整体LLM分析 -->
<div class="llm-analysis">
{{ llm_sections.get(part_id + '_overall', '<p>分析加载中...</p>') | safe }}
</div>
<!-- 水平对比表 -->
<h3>{{ dim_name }}各子维度水平对比(表{{ part_num }}-1</h3>
<table class="level-table">
<thead>
<tr>
<th>子维度</th>
<th>{{ school }}</th>
<th>得分</th>
<th>区均值</th>
<th>排名</th>
</tr>
</thead>
<tbody>
{% for sub_dim in framework[dim_name].sub_dimensions %}
{% set sd = sub_dimensions.get(sub_dim, {}) %}
{% if sd %}
<tr>
<td>{{ sub_dim }}</td>
<td class="lv{{ sd.level }}">水平{{ sd.level }}</td>
<td>{{ "%.1f"|format(sd.score) }}</td>
<td>{{ "%.1f"|format(sd.district_avg) }}</td>
<td>{{ sd.rank_in_district }}/{{ overall.total_schools }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<hr class="section-divider">
<!-- 各三级维度详细分析 -->
{% for sub_dim in framework[dim_name].sub_dimensions %}
{% set sd = sub_dimensions.get(sub_dim, {}) %}
{% if sd %}
{% set sub_index = loop.index %}
{% include 'sections/sub_dimension.html' %}
{% if not loop.last %}
<hr class="section-divider">
{% endif %}
{% endif %}
{% endfor %}
</div>
@@ -0,0 +1,106 @@
{# 第一部分:测评背景与实施(固定内容,不需要LLM生成) #}
<div class="section" id="part1">
<h1>第一部分 测评背景与实施</h1>
<h2>一、测评背景</h2>
<p>为深入贯彻《教育部关于做好普通高中新课程新教材实施工作的指导意见》(教基〔2018〕15号)、《关于新时代推进普通高中育人方式改革的指导意见》(国办发〔2019〕29号)及《基础教育课程教学改革深化行动方案》(教材厅函〔2023〕3号)等一系列国家教育政策导向,积极响应《教育部办公厅关于开展课程实施与教材使用监测工作的通知》(教材厅函〔2023〕5号)的具体要求,上海市教委积极行动,发布了针对性的政策文件,进一步强化国家课程方案的实施转化,并提出建立健全课程实施的监测与反馈机制,以循证决策为引领,持续优化与改进课程规划与实施路径。</p>
<p>学校领导力是学校发展的核心驱动力。实施管理的素养导向立意与决策质量直接影响学校课程教学的整体表现与成效。课程实施管理的重点在于学校课程管理规划与施策,以及这些决策在教学活动中的切实落地。鉴于此,我们从学校领导力视角出发,确保课程领导力、教学变革力、学生发展指导力、教师发展支持力、教育质量评估力、教育条件保障力和数字化赋能力七大维度相互协同,共同作用于学校课程实施的全局。</p>
<h2>二、测评框架</h2>
<p>本报告从学校领导力视角出发,对课程实施监测指标数据进行系统性的分析与指标再建构,构建了学校课程实施的七维度指标体系(见表1-1)。</p>
<table class="data-table">
<caption style="text-align:center; font-weight:600; margin-bottom:10px; color:var(--gray-700);">表1-1 指标体系表</caption>
<thead>
<tr><th style="width:120px;">二级维度</th><th style="width:150px;">三级维度</th><th>指标解读</th></tr>
</thead>
<tbody>
<tr>
<td rowspan="3"><strong>课程领导力</strong></td>
<td>国家标准遵循</td>
<td rowspan="3" style="text-align:left;">1.确保开齐开足国家课程;2.考察学校课程设置的合理性与多样性,包括学科类课程、校本课程、综合实践活动与劳动课程各类课程的课程安排与学生实践;3.关注核心素养导向课程的规范化建设情况</td>
</tr>
<tr><td>课程结构建设</td></tr>
<tr><td>课程规范落实</td></tr>
<tr>
<td rowspan="2"><strong>教学变革力</strong></td>
<td>教学方式变革</td>
<td rowspan="2" style="text-align:left;">1.课中教学方式革新,倡导深度学习,重视个性化教育;2.课后作业设计、管理科学高效</td>
</tr>
<tr><td>作业设计与管理变革</td></tr>
<tr>
<td rowspan="2"><strong>学生发展指导力</strong></td>
<td>学科发展的个性化辅导</td>
<td rowspan="2" style="text-align:left;">1.根据学生的特点和需要,教师进行选课指导、个性化辅导等;2.学校提供个性化的生涯指导服务,建立完善的生涯发展支持体系;3.关注学生综合素质发展</td>
</tr>
<tr><td>学生生涯发展指导</td></tr>
<tr>
<td rowspan="3"><strong>教师发展支持力</strong></td>
<td>培训支持</td>
<td rowspan="3" style="text-align:left;">1.给教师提供入职培训、在职培训以及外出学习机会;2.学校定期组织教研活动,建立教学资源库;3.给教师提供参与科研项目和研究活动的支持</td>
</tr>
<tr><td>教研支持</td></tr>
<tr><td>项目支持</td></tr>
<tr>
<td rowspan="4"><strong>教育质量评估力</strong></td>
<td>科学评价观</td>
<td rowspan="4" style="text-align:left;">1.面向学生核心素养与综合素质发展,确立科学评价观;2.关注学业质量,对标课程标准科学评价学生表现;3.关注综合素质多方面评价;4.关注学生实践活动表现</td>
</tr>
<tr><td>学业质量评估</td></tr>
<tr><td>综合素质评估</td></tr>
<tr><td>实践活动评估</td></tr>
<tr>
<td rowspan="3"><strong>教育条件保障力</strong></td>
<td>区域推进</td>
<td rowspan="3" style="text-align:left;">1.了解学校所在区域教育局对高中教育教学工作的推动情况;2.评估学校信息技术环境、教学设备、场馆设施的支持情况;3.关注学校如何统筹师资配置、校内资源、社区资源等</td>
</tr>
<tr><td>环境支持</td></tr>
<tr><td>资源支持</td></tr>
<tr>
<td rowspan="3"><strong>数字化赋能力</strong></td>
<td>教学方式创新</td>
<td rowspan="3" style="text-align:left;">1.关注数智化资源支持与赋能课程、教学、评价的情况;2.了解学校校内外的数智化资源、信息化平台与信息系统建设情况</td>
</tr>
<tr><td>评价精准化与个性化</td></tr>
<tr><td>课程迭代优化</td></tr>
</tbody>
</table>
<h2>三、测评实施</h2>
<h3>(一)测评对象</h3>
<p>本次监测面向长宁区普通高中学校,共有9所高中学校参与本次监测。每所参与监测的高中学校,均抽取了行政管理部门的学校管理者代表(包括校长、副校长、部门主任等)、各学科教研组组长参加问卷调查。</p>
<h3>(二)测评方法</h3>
<p>基于上海市教师教育学院(上海市教委教研室)开展的中小学课程实施监测的研究框架,采用问卷调查方式采集学校课程实施信息,涵盖学校基础信息、课程实施情况和学科课程实施情况三个维度的数据。</p>
<h3>(三)数据分析</h3>
<p>数据分析流程如下:</p>
<p><strong>第一,指标体系重构。</strong>基于学校领导力视角的七维度重构维度、指标及具体题目的映射关系,形成多级指标体系。</p>
<p><strong>第二,PCA(主成分分析)合成。</strong>对问卷中的各个题目进行标准化处理,利用主成分分析方法,将多个相关变量合成为主成分,提取各三级维度得分。</p>
<p><strong>第三,标准化得分统一量纲。</strong>对PCA合成后的主成分进行标准化处理,将数据统一到均值50、标准差10的正态分布上。68.27%的样本处于[40分, 60分]区间,84.45%处于[30分, 70分],如某所学校得分为60分,意味着其表现超过了约84%的学校。</p>
<p><strong>第四,对三级维度划分水平。</strong>依据维度内涵和学校表现分布,从题目层面确定水平划分的分界点分数,对各学校划分水平1~4(见表1-2)。</p>
<p><strong>第五,聚类分析。</strong>基于标准化得分,对学校进行聚类分析,将具有相似特征的学校归为一类,识别出各维度下不同类型的学校群体。</p>
<h3>(四)三级维度水平划分</h3>
<table class="data-table" style="font-size:13px;">
<caption style="text-align:center; font-weight:600; margin-bottom:10px; color:var(--gray-700);">表1-2 三级维度水平划分表</caption>
<thead>
<tr><th>三级维度</th><th>水平4</th><th>水平3</th><th>水平2</th><th>水平1</th></tr>
</thead>
<tbody>
{% for sub_dim, levels in level_descriptions.items() %}
<tr>
<td style="text-align:left;"><strong>{{ sub_dim }}</strong></td>
<td style="text-align:left; font-size:12px;">{{ levels.get(4, '') }}</td>
<td style="text-align:left; font-size:12px;">{{ levels.get(3, '') }}</td>
<td style="text-align:left; font-size:12px;">{{ levels.get(2, '') }}</td>
<td style="text-align:left; font-size:12px;">{{ levels.get(1, '') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
+140
View File
@@ -0,0 +1,140 @@
{# 第一部分:总体表现 #}
<div class="section" id="part2">
<h1>第二部分 学校课程实施总体表现</h1>
<h2>一、学校课程实施总体状况</h2>
<!-- 得分卡片 -->
<div class="score-cards">
<div class="score-card">
<div class="label">总体得分</div>
<div class="value">{{ "%.1f"|format(overall.score) }}</div>
<div class="diff {{ 'positive' if overall.score > overall.district_avg else 'negative' }}">
{{ "%+.1f"|format(overall.score - overall.district_avg) }} vs 区均值({{ "%.1f"|format(overall.district_avg) }})
</div>
</div>
<div class="score-card">
<div class="label">区内排名</div>
<div class="value">{{ overall.rank_in_district }}/{{ overall.total_schools }}</div>
</div>
<div class="score-card">
<div class="label">同类学校均值</div>
<div class="value" style="font-size:24px">{{ "%.1f"|format(overall.same_type_avg) }}</div>
<div class="diff {{ 'positive' if overall.score > overall.same_type_avg else 'negative' }}">
{{ "%+.1f"|format(overall.score - overall.same_type_avg) }} vs 同类
</div>
</div>
<div class="score-card">
<div class="label">课程实施类型</div>
<div class="value" style="font-size:16px">
<span class="type-badge {{ 'good' if '较好' in (overall.cluster or '') else 'weak' }}">
{{ overall.cluster }}
</span>
</div>
</div>
</div>
<!-- A. 学校画像卡 + 优势-短板象限图 -->
<div class="chart-row">
<div class="chart-box">
<div id="profile-card-chart" class="chart" style="height:420px"></div>
<div class="chart-title">图2-0a 学校课程实施画像总览</div>
</div>
<div class="chart-box">
<div id="quadrant-chart" class="chart" style="height:420px"></div>
<div class="chart-title">图2-0b 三级维度优势-短板象限分析</div>
</div>
</div>
<div class="chart-caption">
{{ llm_sections.get('part2_caption_profile', '') | safe }}
</div>
<!-- 得分对比横向条形图 + 七维度雷达图 -->
<div class="chart-row">
<div class="chart-box">
<div id="score-compare-chart" class="chart"></div>
<div class="chart-title">图2-1 课程实施七维度得分对比</div>
</div>
<div class="chart-box">
<div id="radar-chart" class="chart"></div>
<div class="chart-title">图2-2 课程实施七维度雷达图</div>
</div>
</div>
<div class="chart-caption">
{{ llm_sections.get('part2_caption_scores', '') | safe }}
</div>
<!-- 聚类类型分布 + 两类学校折线对比 -->
<div class="chart-row">
<div class="chart-box">
<div id="cluster-type-dist-chart" class="chart"></div>
<div class="chart-title">图2-3 学校课程实施总体类型分布</div>
</div>
<div class="chart-box">
<div id="cluster-line-chart" class="chart"></div>
<div class="chart-title">图2-4 两类学校课程实施特征对比</div>
</div>
</div>
<div class="chart-caption">
{{ llm_sections.get('part2_caption_cluster', '') | safe }}
</div>
<!-- 聚类类型对比雷达图 + 区内学校排名条形图 -->
<div class="chart-row">
<div class="chart-box">
<div id="cluster-radar-chart" class="chart"></div>
<div class="chart-title">图2-5 学校课程实施类型特征对比(雷达图)</div>
</div>
<div class="chart-box">
<div id="school-ranking-chart" class="chart"></div>
<div class="chart-title">图2-6 区内各校总体得分排名</div>
</div>
</div>
<div class="chart-caption">
{{ llm_sections.get('part2_caption_ranking', '') | safe }}
</div>
<!-- 维度间相关性热力图 -->
<div class="chart-box">
<div id="correlation-chart" class="chart" style="height:450px"></div>
<div class="chart-title">图2-7 课程实施维度间相关性分析</div>
</div>
<div class="chart-caption">
{{ llm_sections.get('part2_caption_correlation', '') | safe }}
</div>
<!-- LLM综合分析 -->
<div class="llm-analysis">
{{ llm_sections.get('part2_overview', '<p>分析加载中...</p>') | safe }}
</div>
<h2>二、分维度状况</h2>
<!-- 维度得分对比表 -->
<table class="data-table">
<thead>
<tr>
<th>维度</th>
<th>{{ school }}</th>
<th>区均值</th>
<th>同类学校均值</th>
<th>差异(vs区)</th>
<th>区内排名</th>
</tr>
</thead>
<tbody>
{% for dim_name, dim_data in dimensions.items() %}
<tr>
<td><strong>{{ dim_name }}</strong></td>
<td>{{ "%.2f"|format(dim_data.score) }}</td>
<td>{{ "%.2f"|format(dim_data.district_avg) }}</td>
<td>{{ "%.2f"|format(dim_data.same_type_avg) }}</td>
<td style="color: {{ 'green' if dim_data.diff_district > 0 else 'red' }}">
{{ "%+.2f"|format(dim_data.diff_district) }}
</td>
<td>{{ dim_data.rank_in_district }}/{{ overall.total_schools }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@@ -0,0 +1,59 @@
{# 单个子维度详情(含金字塔) #}
{# 需要外部传入: sub_dim, sd, part_id, sub_index, school #}
{# sub_index 从1开始,子维度编号从"二"开始("一"是"整体表现" #}
{% set cn_nums = ['', '二', '三', '四', '五', '六', '七', '八'] %}
<h2 id="sub-{{ part_id }}-{{ sub_index }}">{{ cn_nums[sub_index] }}、{{ sub_dim }}</h2>
<div class="score-cards" style="grid-template-columns: repeat(4, 1fr);">
<div class="score-card">
<div class="label">得分</div>
<div class="value" style="font-size:24px">{{ "%.1f"|format(sd.score) }}</div>
<div class="diff {{ 'positive' if sd.diff_district > 0 else 'negative' }}">
{{ "%+.1f"|format(sd.diff_district) }} vs 区均值
</div>
</div>
<div class="score-card">
<div class="label">区均值</div>
<div class="value" style="font-size:24px">{{ "%.1f"|format(sd.district_avg) }}</div>
</div>
<div class="score-card">
<div class="label">水平等级</div>
<div class="level-indicator level-{{ sd.level }}">水平{{ sd.level }}</div>
</div>
<div class="score-card">
<div class="label">排名</div>
<div class="value" style="font-size:24px">{{ sd.rank_in_district }}/{{ overall.total_schools }}</div>
</div>
</div>
<!-- C. 温度计条形图 -->
<div class="chart-box" style="padding:10px 16px;">
<div id="thermo-{{ part_id }}-{{ sub_index }}" class="chart" style="height:80px"></div>
</div>
<!-- 金字塔水平指示器(含各水平学校数量) -->
{% set lv_cn = {4: '四', 3: '三', 2: '二', 1: '一'} %}
<div class="pyramid-container">
{% for lv in [4, 3, 2, 1] %}
{% set lv_count = sd.level_distribution.get('水平' ~ lv, 0) if sd.level_distribution else '' %}
<div class="pyramid-row">
<div class="pyramid-block lv{{ lv }} {{ 'active' if sd.level == lv else 'dimmed' }}">
水平{{ lv_cn[lv] }}{% if lv_count is not none and lv_count != '' %} · {{ lv_count }}所{% endif %}
</div>
{% if sd.level == lv %}
<span class="pyramid-label"><span class="arrow"></span>{{ school }}</span>
{% endif %}
</div>
{% endfor %}
</div>
<p style="text-indent:0; margin:10px 0; color: var(--gray-500); font-size:14px;">
<strong>水平{{ sd.level }}含义:</strong>{{ sd.level_description }}
</p>
<!-- 三级维度LLM分析 -->
{% set sub_key = part_id + '_sub' + sub_index|string + '_' + sub_dim %}
<div class="llm-analysis">
{{ llm_sections.get(sub_key, '<p>分析加载中...</p>') | safe }}
</div>