Files
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 71db82393a Initial commit
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-13 15:38:41 +08:00

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>