Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
323 lines
15 KiB
HTML
323 lines
15 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 subtitle = (profileData.school_type ? (tDim(profileData.school_type) || profileData.school_type) + ' · ' : '')
|
|
+ I18N.ec_district_rank_n.replace('{rank}', profileData.rank);
|
|
|
|
const option = {
|
|
title: [
|
|
{ text: profileData.school_name, left: 'center', top: 6, textStyle: { fontSize: 16, fontWeight: 'bold', color: '#1e3a5f' } },
|
|
{ text: subtitle, 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 }],
|
|
},
|
|
{ type: 'pie', center: ['50%', '92%'], radius: ['0%', '0%'], silent: true, data: [] },
|
|
],
|
|
graphic: (function() {
|
|
var items = [];
|
|
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: shortDim(sig.name), textAlign: 'center', fontSize: 9, fill: '#374151' },
|
|
});
|
|
});
|
|
var summaryText = I18N.ec_level_dist_summary + ': ';
|
|
summaryText += I18N.ec_lv_short + '4=' + levelCounts[4] + ' '
|
|
+ I18N.ec_lv_short + '3=' + levelCounts[3] + ' '
|
|
+ I18N.ec_lv_short + '2=' + levelCounts[2] + ' '
|
|
+ I18N.ec_lv_short + '1=' + 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: tSub(item.name),
|
|
originalName: 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> (' + tDim(d.parent) + ')<br/>' +
|
|
I18N.ec_quad_score + ': ' + d.value[0] + '<br/>' +
|
|
I18N.ec_quad_diff + ': ' + (d.value[1] > 0 ? '+' : '') + d.value[1] + '<br/>' +
|
|
I18N.ec_quad_level + ': ' + d.level;
|
|
}
|
|
},
|
|
grid: { left: '12%', right: '5%', bottom: '12%', top: '8%' },
|
|
xAxis: {
|
|
name: I18N.ec_quad_x_axis, nameLocation: 'center', nameGap: 28,
|
|
nameTextStyle: { fontSize: 12 },
|
|
min: 20, max: 80,
|
|
splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e5e7eb' } },
|
|
},
|
|
yAxis: {
|
|
name: I18N.ec_quad_y_axis, 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;
|
|
if (LANG === 'en') {
|
|
return n.length > 14 ? n.substring(0, 14) + '…' : n;
|
|
}
|
|
return n.length > 5 ? n.substring(0, 5) + '…' : n;
|
|
},
|
|
color: '#374151',
|
|
},
|
|
}],
|
|
graphic: [
|
|
{ type: 'text', right: '8%', top: '10%',
|
|
style: { text: '■ ' + I18N.ec_quadrant_q1, fill: '#059669', fontSize: 11, fontWeight: 'bold' } },
|
|
{ type: 'text', left: '14%', bottom: '14%',
|
|
style: { text: '■ ' + I18N.ec_quadrant_q3, fill: '#dc2626', fontSize: 11, fontWeight: 'bold' } },
|
|
{ type: 'text', right: '8%', bottom: '14%',
|
|
style: { text: '⊙ ' + I18N.ec_quadrant_q4, fill: '#d97706', fontSize: 11, fontWeight: 'bold' } },
|
|
{ type: 'text', left: '14%', top: '10%',
|
|
style: { text: '↗ ' + I18N.ec_quadrant_q2, 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: I18N.ec_quad_district_avg_marker, 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() {
|
|
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: I18N.ec_thermo_self_marker + score, textAlign: 'center', fontSize: 11, fontWeight: 'bold', fill: '#1e3a5f' },
|
|
});
|
|
items.push({
|
|
type: 'text', left: toPercent(distAvg), bottom: '0%',
|
|
style: { text: I18N.ec_thermo_dist_marker, textAlign: 'center', fontSize: 9, fill: '#6b7280' },
|
|
});
|
|
if (Math.abs(sameTypeAvg - distAvg) > 1) {
|
|
items.push({
|
|
type: 'text', left: toPercent(sameTypeAvg), bottom: '0%',
|
|
style: { text: I18N.ec_thermo_same_marker, 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) {
|
|
// 名字本地化(gain 项是子维度名,current/potential 是固定文案)
|
|
var displayName;
|
|
if (step.type === 'current') {
|
|
displayName = I18N.ec_waterfall_current;
|
|
} else if (step.type === 'potential') {
|
|
displayName = I18N.ec_waterfall_potential;
|
|
} else {
|
|
displayName = tSub(step.name) || step.name;
|
|
}
|
|
var maxLen = LANG === 'en' ? 14 : 6;
|
|
var shortName = displayName.length > maxLen ? displayName.substring(0, maxLen) + '…' : displayName;
|
|
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];
|
|
var detailName = step.type === 'gain' ? (tSub(step.name) || step.name) : step.name;
|
|
if (step.type === 'current') return I18N.ec_waterfall_current + ': <strong>' + step.value + '</strong> ' + I18N.ec_waterfall_pts_unit;
|
|
if (step.type === 'potential') return I18N.ec_waterfall_potential + ': <strong>' + step.value + '</strong> ' + I18N.ec_waterfall_pts_unit + ' (+' + waterfallData.total_gain + ')';
|
|
return '<strong>' + detailName + '</strong><br/>' +
|
|
(step.detail || '') + '<br/>' + I18N.ec_waterfall_contrib + ': +' + step.value + ' ' + I18N.ec_waterfall_pts_unit;
|
|
},
|
|
},
|
|
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>
|