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,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>
|
||||
Reference in New Issue
Block a user