Initial commit
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
71db82393a
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user