Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
385 lines
16 KiB
HTML
385 lines
16 KiB
HTML
<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>
|