|
@@ -9,6 +9,8 @@
|
|
|
|
|
|
|
|
let chart = null;
|
|
let chart = null;
|
|
|
let schools = [];
|
|
let schools = [];
|
|
|
|
|
+ let cityPoints = [];
|
|
|
|
|
+ let cityLabelsVisible = false;
|
|
|
let inited = false;
|
|
let inited = false;
|
|
|
let loading = false;
|
|
let loading = false;
|
|
|
let selectedId = null;
|
|
let selectedId = null;
|
|
@@ -30,11 +32,42 @@
|
|
|
return `${school.ops.shopCount} 店${school.ops.lastDate ? ' · 最近 ' + fmtDate(school.ops.lastDate) : ''}`;
|
|
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) {
|
|
function toPoint(school) {
|
|
|
return {
|
|
return {
|
|
|
name: school.name,
|
|
name: school.name,
|
|
|
value: [school.lng, school.lat],
|
|
value: [school.lng, school.lat],
|
|
|
schoolId: school.id,
|
|
schoolId: school.id,
|
|
|
|
|
+ itemStyle: { color: shopColor(school.ops && school.ops.shopCount) },
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -64,10 +97,30 @@
|
|
|
zoom: ZOOM,
|
|
zoom: ZOOM,
|
|
|
scaleLimit: { min: 1, max: 12 },
|
|
scaleLimit: { min: 1, max: 12 },
|
|
|
itemStyle: { areaColor: '#f7f5f2', borderColor: '#ddd6cd', borderWidth: 0.8 },
|
|
itemStyle: { areaColor: '#f7f5f2', borderColor: '#ddd6cd', borderWidth: 0.8 },
|
|
|
- emphasis: { itemStyle: { areaColor: '#efeae4' }, label: { show: false } },
|
|
|
|
|
|
|
+ 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 },
|
|
select: { disabled: true },
|
|
|
},
|
|
},
|
|
|
series: [
|
|
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',
|
|
id: 'on',
|
|
|
type: 'effectScatter',
|
|
type: 'effectScatter',
|
|
@@ -76,7 +129,7 @@
|
|
|
symbolSize: 11,
|
|
symbolSize: 11,
|
|
|
showEffectOn: 'render',
|
|
showEffectOn: 'render',
|
|
|
rippleEffect: { brushType: 'stroke', scale: 3.4, period: 3.6 },
|
|
rippleEffect: { brushType: 'stroke', scale: 3.4, period: 3.6 },
|
|
|
- itemStyle: { color: '#ff6b35', shadowBlur: 8, shadowColor: 'rgba(255,107,53,.45)' },
|
|
|
|
|
|
|
+ itemStyle: { shadowBlur: 8, shadowColor: 'rgba(255,107,53,.35)' },
|
|
|
label: {
|
|
label: {
|
|
|
show: true,
|
|
show: true,
|
|
|
position: 'right',
|
|
position: 'right',
|
|
@@ -102,11 +155,39 @@
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ 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 = '<span class="schools-legend-title">门店数</span>'
|
|
|
|
|
+ + '<span><i style="background:#d8d1c9"></i>0</span>'
|
|
|
|
|
+ + '<span><i style="background:#ffb894"></i>1-3</span>'
|
|
|
|
|
+ + '<span><i style="background:#ff8a5c"></i>4-9</span>'
|
|
|
|
|
+ + '<span><i style="background:#e8501f"></i>10+</span>'
|
|
|
|
|
+ + '<span class="schools-legend-sep"></span>'
|
|
|
|
|
+ + '<span><i class="hollow"></i>停用 / 未定位</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function renderMap(force) {
|
|
function renderMap(force) {
|
|
|
const box = $('schoolsMap');
|
|
const box = $('schoolsMap');
|
|
|
if (!box) return;
|
|
if (!box) return;
|
|
|
- if (!chart) chart = echarts.init(box);
|
|
|
|
|
|
|
+ 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);
|
|
chart.setOption(buildOption(), !!force);
|
|
|
|
|
+ renderLegend();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderList(data) {
|
|
function renderList(data) {
|
|
@@ -133,7 +214,7 @@
|
|
|
const rows = items.map((s) => `
|
|
const rows = items.map((s) => `
|
|
|
<div class="schools-item${s.id === selectedId ? ' on' : ''}" data-id="${esc(s.id)}">
|
|
<div class="schools-item${s.id === selectedId ? ' on' : ''}" data-id="${esc(s.id)}">
|
|
|
<div class="schools-item-name">
|
|
<div class="schools-item-name">
|
|
|
- <span class="schools-dot${s.enabled && s.lng != null ? '' : ' off'}"></span>
|
|
|
|
|
|
|
+ <span class="schools-dot${s.enabled && s.lng != null ? '' : ' off'}"${s.enabled && s.lng != null ? ` style="background:${shopColor(s.ops && s.ops.shopCount)}"` : ''}></span>
|
|
|
<span>${esc(s.name)}</span>
|
|
<span>${esc(s.name)}</span>
|
|
|
${s.seed ? '<span class="badge badge-muted">内置</span>' : ''}
|
|
${s.seed ? '<span class="badge badge-muted">内置</span>' : ''}
|
|
|
${s.lng == null ? '<span class="badge badge-warn">未定位</span>' : ''}
|
|
${s.lng == null ? '<span class="badge badge-warn">未定位</span>' : ''}
|
|
@@ -176,14 +257,18 @@
|
|
|
if (loading) return;
|
|
if (loading) return;
|
|
|
loading = true;
|
|
loading = true;
|
|
|
try {
|
|
try {
|
|
|
- const [geoRes, listRes] = await Promise.all([
|
|
|
|
|
|
|
+ const [geoRes, listRes, cityRes] = await Promise.all([
|
|
|
fetch('/vendor/china-map.json'),
|
|
fetch('/vendor/china-map.json'),
|
|
|
fetch('/api/schools'),
|
|
fetch('/api/schools'),
|
|
|
|
|
+ fetch('/vendor/cn-cities.json').catch(() => null),
|
|
|
]);
|
|
]);
|
|
|
if (!geoRes.ok) throw new Error('底图加载失败');
|
|
if (!geoRes.ok) throw new Error('底图加载失败');
|
|
|
const geo = await geoRes.json();
|
|
const geo = await geoRes.json();
|
|
|
const data = await listRes.json();
|
|
const data = await listRes.json();
|
|
|
echarts.registerMap('china', geo);
|
|
echarts.registerMap('china', geo);
|
|
|
|
|
+ if (cityRes && cityRes.ok) {
|
|
|
|
|
+ try { cityPoints = buildCityPoints(await cityRes.json()); } catch { cityPoints = []; }
|
|
|
|
|
+ }
|
|
|
schools = data.schools || [];
|
|
schools = data.schools || [];
|
|
|
renderList(data);
|
|
renderList(data);
|
|
|
renderMap(true);
|
|
renderMap(true);
|