| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- '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 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 toPoint(school) {
- return {
- name: school.name,
- value: [school.lng, school.lat],
- schoolId: school.id,
- };
- }
- 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 `<b>${esc(school.name)}</b><br>${esc(school.province || '—')}${school.city ? ' · ' + esc(school.city) : ''}<br>${status} · ${opsText(school)}<br><span style="color:#9a938c">${point}</span>`;
- },
- },
- geo: {
- map: 'china',
- roam: true,
- center: CENTER,
- zoom: ZOOM,
- scaleLimit: { min: 1, max: 12 },
- itemStyle: { areaColor: '#f7f5f2', borderColor: '#ddd6cd', borderWidth: 0.8 },
- emphasis: { itemStyle: { areaColor: '#efeae4' }, label: { show: false } },
- select: { disabled: true },
- },
- series: [
- {
- id: 'on',
- type: 'effectScatter',
- coordinateSystem: 'geo',
- data: on.map(toPoint),
- symbolSize: 11,
- showEffectOn: 'render',
- rippleEffect: { brushType: 'stroke', scale: 3.4, period: 3.6 },
- itemStyle: { color: '#ff6b35', shadowBlur: 8, shadowColor: 'rgba(255,107,53,.45)' },
- 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 renderMap(force) {
- const box = $('schoolsMap');
- if (!box) return;
- if (!chart) chart = echarts.init(box);
- chart.setOption(buildOption(), !!force);
- }
- 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 = '<div class="empty-hint ops-empty" style="min-height:220px"><p>还没有学校</p><small>内置学校缺失,请检查 server/schools.seed.json</small></div>';
- 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) => `
- <div class="schools-item${s.id === selectedId ? ' on' : ''}" data-id="${esc(s.id)}">
- <div class="schools-item-name">
- <span class="schools-dot${s.enabled && s.lng != null ? '' : ' off'}"></span>
- <span>${esc(s.name)}</span>
- ${s.seed ? '<span class="badge badge-muted">内置</span>' : ''}
- ${s.lng == null ? '<span class="badge badge-warn">未定位</span>' : ''}
- </div>
- <div class="schools-item-meta">
- <span>${esc(s.city || province)}</span>
- <span>${esc(opsText(s))}</span>
- </div>
- </div>`).join('');
- return `<div class="schools-group">${esc(province)} <em>${onCount}/${items.length}</em></div>${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] = await Promise.all([
- fetch('/vendor/china-map.json'),
- fetch('/api/schools'),
- ]);
- if (!geoRes.ok) throw new Error('底图加载失败');
- const geo = await geoRes.json();
- const data = await listRes.json();
- echarts.registerMap('china', geo);
- schools = data.schools || [];
- renderList(data);
- renderMap(true);
- } catch (e) {
- const box = $('schoolsMap');
- if (box) box.innerHTML = `<div class="empty-hint ops-empty"><p>地图加载失败</p><small>${esc(e.message || e)}</small></div>`;
- } finally {
- loading = false;
- }
- }
- window.shanSchoolsInit = function () {
- if (!inited) {
- inited = true;
- bind();
- load();
- } else if (chart) {
- chart.resize();
- }
- };
- })();
|