'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 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 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 = schools.find((s) => s.id === params.data.schoolId); if (!school) return params.name; 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 }, 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; } box.innerHTML = '门店数' + '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
'; return; } const groups = new Map(); for (const school of schools) { const key = school.province || '未标注省份'; if (!groups.has(key)) groups.set(key, []); groups.get(key).push(school); } list.innerHTML = [...groups.entries()].map(([province, items]) => { const onCount = items.filter((s) => s.enabled && s.lng != null).length; const rows = items.map((s) => `
${esc(s.name)} ${s.seed ? '内置' : ''} ${s.lng == null ? '未定位' : ''}
${esc(s.city || province)} ${esc(opsText(s))}
`).join(''); return `
${esc(province)} ${onCount}/${items.length}
${rows}`; }).join(''); list.querySelectorAll('[data-id]').forEach((el) => { el.addEventListener('click', () => selectSchool(el.dataset.id, true)); }); } function selectSchool(id, fly) { const school = schools.find((s) => s.id === id); if (!school) return; selectedId = id; renderList({ enabledCount: schools.filter((s) => s.enabled && s.lng != null).length }); if (chart && school.lng != null) { if (fly) chart.setOption({ geo: { center: [school.lng, school.lat], zoom: 8 } }); chart.dispatchAction({ type: 'highlight', seriesId: 'on', name: school.name }); } } function bind() { const reset = $('btnSchoolsReset'); if (reset) { reset.addEventListener('click', () => { if (chart) chart.setOption({ geo: { center: CENTER, zoom: ZOOM } }); }); } window.addEventListener('resize', () => { if (chart) chart.resize(); }); } async function load() { if (loading) return; loading = true; try { const [geoRes, listRes, cityRes] = await Promise.all([ fetch('/vendor/china-map.json'), fetch('/api/schools'), fetch('/vendor/cn-cities.json').catch(() => null), ]); if (!geoRes.ok) throw new Error('底图加载失败'); const geo = await geoRes.json(); const data = await listRes.json(); echarts.registerMap('china', geo); if (cityRes && cityRes.ok) { try { cityPoints = buildCityPoints(await cityRes.json()); } catch { cityPoints = []; } } schools = data.schools || []; renderList(data); renderMap(true); } catch (e) { const box = $('schoolsMap'); if (box) box.innerHTML = `

地图加载失败

${esc(e.message || e)}
`; } finally { loading = false; } } // 运营数据变更后,学校的关联门店数/最近日期会变,需要重新拉取 window.addEventListener('ops:data-changed', () => { if (inited) load(); }); window.shanSchoolsInit = function () { if (!inited) { inited = true; bind(); load(); } else if (chart) { chart.resize(); } }; })();