'use strict';
// 校区地图:中国地图 + 学校点位(启用=涟漪点亮,停用/未定位=灰点)+ 右侧列表。
// 数据:GET /api/schools(JSON 种子 + 运行时合并);底图:/vendor/china-map.json
(function () {
const $ = (id) => document.getElementById(id);
const CENTER = [104.5, 36];
const ZOOM = 1.55;
let chart = null;
let schools = [];
let cityPoints = [];
let geoProvinceNames = [];
let cityLabelsVisible = false;
let inited = false;
let loading = false;
let selectedId = null;
function esc(value) {
const div = document.createElement('div');
div.textContent = String(value == null ? '' : value);
return div.innerHTML;
}
function fmtDate(ymd) {
const s = String(ymd || '');
return /^\d{8}$/.test(s) ? `${Number(s.slice(4, 6))}/${Number(s.slice(6, 8))}` : '';
}
function opsText(school) {
if (!school.ops) return '运营数据不可用';
if (!school.ops.shopCount) return '未关联运营数据';
return `${school.ops.shopCount} 店${school.ops.lastDate ? ' · 最近 ' + fmtDate(school.ops.lastDate) : ''}`;
}
function shortProvince(name) {
const raw = String(name || '');
const short = raw
.replace(/特别行政区$/, '')
.replace(/(壮族|回族|维吾尔)?自治区$/, '')
.replace(/省$/, '')
.replace(/市$/, '');
return short || raw;
}
/** 门店数越多颜色越深,用来区分学校的经营体量 */
function shopColor(count) {
const n = Number(count) || 0;
if (n >= 10) return '#e8501f';
if (n >= 4) return '#ff8a5c';
if (n >= 1) return '#ffb894';
return '#d8d1c9';
}
/** 每省已点亮(启用且已定位)的学校数 */
function provinceCounts() {
const byShort = new Map();
for (const full of geoProvinceNames) byShort.set(shortProvince(full), full);
const counts = new Map();
for (const school of schools) {
if (!(school.enabled && school.lng != null)) continue;
const full = byShort.get(shortProvince(school.province)) || school.province;
if (!full) continue;
counts.set(full, (counts.get(full) || 0) + 1);
}
return counts;
}
/** 省份按点亮学校数分档着色(比学校点更浅,避免压过点位) */
function provinceColor(count) {
const n = Number(count) || 0;
if (n >= 10) return '#ef8455';
if (n >= 4) return '#f4a684';
if (n >= 2) return '#f8c9b2';
if (n >= 1) return '#f9dccc';
return '#f7f5f2';
}
function buildCityPoints(data) {
const points = [];
for (const info of Object.values(data || {})) {
const cities = (info && info.cities) || {};
for (const [name, coord] of Object.entries(cities)) {
if (Array.isArray(coord) && coord.length === 2) points.push({ name, value: coord });
}
}
return points;
}
function toPoint(school) {
return {
name: school.name,
value: [school.lng, school.lat],
schoolId: school.id,
itemStyle: { color: shopColor(school.ops && school.ops.shopCount) },
};
}
function buildOption() {
const on = schools.filter((s) => s.enabled && s.lng != null && s.lat != null);
const off = schools.filter((s) => !(s.enabled && s.lng != null && s.lat != null));
return {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255,255,255,.96)',
borderColor: '#e8e2db',
borderWidth: 1,
padding: [8, 10],
textStyle: { color: '#3f3a35', fontSize: 12 },
formatter(params) {
const school = params.data && schools.find((s) => s.id === params.data.schoolId);
if (!school) {
const count = provinceCounts().get(params.name) || 0;
return `${esc(params.name)}
点亮学校:${count} 所`;
}
const status = school.enabled ? '已启用' : '已停用';
const point = school.lng == null ? '未定位' : `${school.lng.toFixed(4)}, ${school.lat.toFixed(4)}`;
return `${esc(school.name)}
${esc(school.province || '—')}${school.city ? ' · ' + esc(school.city) : ''}
${status} · ${opsText(school)}
${point}`;
},
},
geo: {
map: 'china',
roam: true,
center: CENTER,
zoom: ZOOM,
scaleLimit: { min: 1, max: 12 },
itemStyle: { areaColor: '#f7f5f2', borderColor: '#ddd6cd', borderWidth: 0.8 },
regions: [...provinceCounts().entries()].map(([name, count]) => ({
name,
itemStyle: { areaColor: provinceColor(count) },
})),
label: { show: true, color: '#8f8478', fontSize: 10, formatter: (p) => shortProvince(p.name) },
emphasis: { itemStyle: { areaColor: '#efeae4' }, label: { show: true, color: '#5b534c', fontSize: 10 } },
select: { disabled: true },
},
series: [
{
id: 'cities',
type: 'scatter',
coordinateSystem: 'geo',
data: cityPoints,
symbolSize: 2,
itemStyle: { color: '#d5cdc4' },
label: {
show: cityLabelsVisible,
position: 'right',
distance: 2,
fontSize: 9,
color: '#a39a91',
formatter: (p) => p.name,
},
labelLayout: { hideOverlap: true },
silent: true,
zlevel: 0,
},
{
id: 'on',
type: 'effectScatter',
coordinateSystem: 'geo',
data: on.map(toPoint),
symbolSize: 11,
showEffectOn: 'render',
rippleEffect: { brushType: 'stroke', scale: 3.4, period: 3.6 },
itemStyle: { shadowBlur: 8, shadowColor: 'rgba(255,107,53,.35)' },
label: {
show: true,
position: 'right',
distance: 6,
formatter: (p) => p.name,
fontSize: 11,
color: '#5b534c',
},
emphasis: { scale: 1.4 },
zlevel: 2,
},
{
id: 'off',
type: 'scatter',
coordinateSystem: 'geo',
data: off.map(toPoint),
symbolSize: 6,
itemStyle: { color: '#b8b2ad' },
label: { show: false },
zlevel: 1,
},
],
};
}
function renderLegend() {
const box = $('schoolsLegend');
if (!box) return;
const hasOps = schools.some((s) => s.ops && s.ops.shopCount > 0);
if (!hasOps) {
box.innerHTML = '';
return;
}
const litProvinces = provinceCounts();
const maxLit = Math.max(0, ...litProvinces.values());
box.innerHTML = '每省点亮学校'
+ '1'
+ '2-3'
+ '4-9'
+ '10+'
+ ''
+ '门店数'
+ '0'
+ '1-3'
+ '4-9'
+ '10+'
+ ''
+ '停用 / 未定位';
}
function renderMap(force) {
const box = $('schoolsMap');
if (!box) return;
if (!chart) {
chart = echarts.init(box);
chart.on('georoam', () => {
const option = chart.getOption();
const zoom = (option.geo && option.geo[0] && option.geo[0].zoom) || ZOOM;
const shouldShow = zoom >= 2;
if (shouldShow === cityLabelsVisible) return;
cityLabelsVisible = shouldShow;
chart.setOption({ series: [{ id: 'cities', label: { show: shouldShow } }] });
});
}
chart.setOption(buildOption(), !!force);
renderLegend();
}
function renderList(data) {
const list = $('schoolsList');
const count = $('schoolsCount');
if (!list) return;
const enabled = data.enabledCount || 0;
if (count) count.textContent = `${enabled} / ${schools.length} 已启用`;
if (!schools.length) {
list.innerHTML = '
还没有学校
内置学校缺失,请检查 server/schools.seed.json地图加载失败
${esc(e.message || e)}