|
|
|
@@ -52,6 +52,9 @@ function stationSearchScore(station, query) {
|
|
|
|
|
function toPinyin(value) { const converter = globalThis.pinyinPro?.pinyin; return converter ? normalize(converter(String(value), { toneType: 'none', separator: ' ' })) : ''; }
|
|
|
|
|
function initials(value) { return String(value).split(/\s+/).map(word => word[0] || '').join(''); }
|
|
|
|
|
function normalize(value) { return String(value || '').trim().toLocaleLowerCase(); }
|
|
|
|
|
function adminNameKey(value) { return String(value || '').trim().replace(/(特别行政区|壮族自治区|回族自治区|维吾尔自治区|自治区|省|市)$/, ''); }
|
|
|
|
|
function isMunicipalityProvince(value) { return ['北京', '上海', '天津', '重庆'].includes(adminNameKey(value)); }
|
|
|
|
|
function isMunicipalityCity(province, city) { return isMunicipalityProvince(province) && adminNameKey(province) === adminNameKey(city); }
|
|
|
|
|
function hasCoordinates(station) { return Number.isFinite(Number(station.longitude)) && Number.isFinite(Number(station.latitude)); }
|
|
|
|
|
function coords(station) { return [Number(station.longitude), Number(station.latitude)]; }
|
|
|
|
|
function stationDistrictName(station) { if (station?.district) return String(station.district).trim(); let address = String(station?.address || '').trim(); for (const prefix of [station?.province, station?.city]) { const normalized = String(prefix || '').trim(); if (normalized) address = address.split(normalized).join(''); } return address.match(/^(.{2,10}?(?:区|县|旗|市))/)?.[1] || ''; }
|
|
|
|
@@ -106,7 +109,14 @@ function renderList() {
|
|
|
|
|
document.getElementById('rankViewportMeta').textContent = `当前视野 ${formatNumber(visibleNodes.length)} · 全部 ${formatNumber(allNodes.length)}`;
|
|
|
|
|
document.getElementById('panelRankTitle').textContent = state.userLocation && level === 'station' ? `附近${state.stationFilter === 'partner' ? '合作' : ''}加氢站` : level === 'province' ? '加氢站省份 TOP 排名' : level === 'city' ? '加氢站城市 TOP 排名' : '加氢站 TOP 排名';
|
|
|
|
|
if (!display.length) { box.innerHTML = '<div class="rank-empty">未找到匹配站点<br>请尝试名称、地址、城市或拼音首字母</div>'; return; }
|
|
|
|
|
box.innerHTML = display.slice(0, 100).map((node, index) => { const distance = node.kind === 'station' ? stationDistance(node.station) : Number.POSITIVE_INFINITY, partnerTag = node.kind === 'station' ? `<span class="station-list-partner-tag${node.cooperative ? '' : ' is-placeholder'}"${node.cooperative ? '' : ' aria-hidden="true"'}>${node.cooperative ? '合作' : ''}</span>` : '', rowValue = Number.isFinite(distance) ? `${formatNumber(distance, 1)} km` : node.kind === 'station' ? '' : `${formatNumber(node.count)} 座`; return `<button type="button" class="rank-glass-row${node.kind === 'station' && state.selected?.id === node.station.id ? ' is-selected' : ''}" data-node-id="${escapeAttribute(node.id)}"><span class="r-badge ${index < 3 ? `r-top${index + 1}` : ''}">${index + 1}</span><span class="r-name">${partnerTag}<span class="station-list-name">${escapeHTML(node.name)}</span></span>${rowValue ? `<span class="r-val">${rowValue}</span>` : ''}</button>`; }).join('');
|
|
|
|
|
box.innerHTML = display.slice(0, 100).map((node, index) => {
|
|
|
|
|
const station = node.kind === 'station' ? node.station : null, distance = station ? stationDistance(station) : Number.POSITIVE_INFINITY;
|
|
|
|
|
const partnerTag = station ? `<span class="station-list-partner-tag${node.cooperative ? '' : ' is-placeholder'}"${node.cooperative ? '' : ' aria-hidden="true"'}>${node.cooperative ? '合作' : ''}</span>` : '';
|
|
|
|
|
const rowValue = Number.isFinite(distance) ? `${formatNumber(distance, 1)} km` : station ? '' : `${formatNumber(node.count)} 座`;
|
|
|
|
|
const price = station?.cooperative ? stationUnitPrice(station) : '';
|
|
|
|
|
const valueStack = price || rowValue ? `<span class="r-value-stack">${price ? `<span class="station-list-price">${escapeHTML(price)}</span>` : ''}${rowValue ? `<span class="r-val">${escapeHTML(rowValue)}</span>` : ''}</span>` : '';
|
|
|
|
|
return `<button type="button" class="rank-glass-row${station && state.selected?.id === station.id ? ' is-selected' : ''}" data-node-id="${escapeAttribute(node.id)}"><span class="r-badge ${index < 3 ? `r-top${index + 1}` : ''}">${index + 1}</span><span class="r-name">${partnerTag}<span class="station-list-name">${escapeHTML(node.name)}</span></span>${valueStack}</button>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
box.querySelectorAll('[data-node-id]').forEach(button => button.addEventListener('click', () => { const node = display.find(item => item.id === button.dataset.nodeId); if (node?.kind === 'station') selectStation(node.station, true); else if (node) state.map?.setZoomAndCenter(nextZoomForLevel(node.level), node.lnglat); }));
|
|
|
|
|
}
|
|
|
|
|
function compareNodes(left, right) { if (state.userLocation && left.kind === 'station' && right.kind === 'station') return stationDistance(left.station) - stationDistance(right.station); return right.count - left.count || String(left.name || '').localeCompare(String(right.name || ''), 'zh-CN'); }
|
|
|
|
@@ -117,7 +127,7 @@ function fuzzySearchScore(value, query) {
|
|
|
|
|
const pinyin = toPinyin(value); if (pinyin === needle || pinyin.startsWith(needle) || initials(pinyin).startsWith(needle)) return 3;
|
|
|
|
|
return pinyin.includes(needle) || initials(pinyin).includes(needle) ? 4 : Number.POSITIVE_INFINITY;
|
|
|
|
|
}
|
|
|
|
|
function compactPath(parts) { return parts.filter(Boolean).join(' · '); }
|
|
|
|
|
function compactPath(parts) { return parts.filter(Boolean).filter((part, index, list) => index === 0 || adminNameKey(part) !== adminNameKey(list[index - 1])).join(' · '); }
|
|
|
|
|
function stationLocationOptions() {
|
|
|
|
|
const groups = new Map();
|
|
|
|
|
const add = (level, station) => {
|
|
|
|
@@ -127,12 +137,13 @@ function stationLocationOptions() {
|
|
|
|
|
if (!groups.has(key)) groups.set(key, { type: 'location', level, ...filter, stations: [] });
|
|
|
|
|
groups.get(key).stations.push(station);
|
|
|
|
|
};
|
|
|
|
|
for (const station of stationTypeScope()) { const district = stationDistrictName(station); if (station.province) add('province', station); if (station.province && station.city) add('city', station); if (station.province && station.city && district) add('district', { ...station, district }); }
|
|
|
|
|
for (const station of stationTypeScope()) { const district = stationDistrictName(station); if (station.province) add('province', station); if (station.province && station.city && !isMunicipalityCity(station.province, station.city)) add('city', station); if (station.province && station.city && district) add('district', { ...station, district }); }
|
|
|
|
|
return [...groups.values()].map(option => {
|
|
|
|
|
const target = option.level === 'province' ? option.province : option.level === 'city' ? option.city : option.district;
|
|
|
|
|
const longitude = option.stations.reduce((sum, station) => sum + Number(station.longitude), 0) / option.stations.length;
|
|
|
|
|
const latitude = option.stations.reduce((sum, station) => sum + Number(station.latitude), 0) / option.stations.length;
|
|
|
|
|
return { ...option, label: stationGroupName({ province: target, city: target }, option.level === 'province' ? 'province' : 'city'), path: compactPath([option.province, option.city, option.district]), lnglat: [longitude, latitude] };
|
|
|
|
|
const label = option.level === 'district' ? option.district : stationGroupName({ province: target, city: target }, option.level === 'province' ? 'province' : 'city');
|
|
|
|
|
return { ...option, label, path: compactPath([option.province, option.city, option.district]), lnglat: [longitude, latitude] };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function stationLocationSuggestions(query) {
|
|
|
|
@@ -180,9 +191,10 @@ function regionPickerOptions() {
|
|
|
|
|
const picker = state.regionPicker; if (!picker) return [];
|
|
|
|
|
const all = stationLocationOptions(); const needle = normalize(picker.search);
|
|
|
|
|
if (needle) return all.map(option => ({ ...option, score: Math.min(fuzzySearchScore(option.label, needle), fuzzySearchScore(option.path, needle)) })).filter(option => Number.isFinite(option.score)).sort((left, right) => left.score - right.score || left.label.localeCompare(right.label, 'zh-CN')).slice(0, 80);
|
|
|
|
|
if (picker.level === 'province') return all.filter(option => option.level === 'province');
|
|
|
|
|
if (picker.level === 'province') return all.filter(option => option.level === 'province').sort((left, right) => right.stations.length - left.stations.length || left.label.localeCompare(right.label, 'zh-CN'));
|
|
|
|
|
if (picker.level === 'city' && isMunicipalityProvince(picker.province)) return all.filter(option => option.level === 'district' && option.province === picker.province);
|
|
|
|
|
if (picker.level === 'city') return all.filter(option => option.level === 'city' && option.province === picker.province);
|
|
|
|
|
return all.filter(option => option.level === 'district' && option.province === picker.province && option.city === picker.city);
|
|
|
|
|
return all.filter(option => option.level === 'district' && option.province === picker.province && (!picker.city || option.city === picker.city));
|
|
|
|
|
}
|
|
|
|
|
function regionPickerOptionPath(option) {
|
|
|
|
|
const parts = option.level === 'province' ? [option.province] : option.level === 'city' ? [option.province, option.city] : [option.province, option.city, option.district];
|
|
|
|
@@ -213,7 +225,7 @@ function renderRegionPicker() {
|
|
|
|
|
}
|
|
|
|
|
function setRegionPickerSearch(value) { if (!state.regionPicker) return; state.regionPicker.search = value; renderRegionPicker(); }
|
|
|
|
|
function navigateRegionPicker(level) { if (!state.regionPicker) return; state.regionPicker.search = ''; if (level === 'province') { state.regionPicker.level = 'province'; state.regionPicker.province = ''; state.regionPicker.city = ''; } else if (level === 'city') { state.regionPicker.level = 'city'; state.regionPicker.city = ''; } else state.regionPicker.level = 'district'; renderRegionPicker(); }
|
|
|
|
|
function descendRegionPicker(index) { const option = state.regionPicker?.options[index]; if (!option || option.level === 'district') return; state.regionPicker.search = ''; state.regionPicker.level = option.level === 'province' ? 'city' : 'district'; state.regionPicker.province = option.province; state.regionPicker.city = option.city || ''; renderRegionPicker(); }
|
|
|
|
|
function descendRegionPicker(index) { const option = state.regionPicker?.options[index]; if (!option || option.level === 'district') return; state.regionPicker.search = ''; state.regionPicker.level = option.level === 'province' && isMunicipalityProvince(option.province) ? 'district' : option.level === 'province' ? 'city' : 'district'; state.regionPicker.province = option.province; state.regionPicker.city = option.level === 'province' && isMunicipalityProvince(option.province) ? '' : option.city || ''; renderRegionPicker(); }
|
|
|
|
|
function toggleRegionPickerOption(index) { const option = state.regionPicker?.options[index]; if (!option) return; const key = locationFilterKey(option), draft = state.regionPicker.draft; const found = draft.findIndex(filter => locationFilterKey(filter) === key); if (found >= 0) draft.splice(found, 1); else draft.push(cloneLocationFilter(option)); renderRegionPicker(); }
|
|
|
|
|
function removeRegionPickerDraft(index) { if (!state.regionPicker) return; state.regionPicker.draft.splice(index, 1); renderRegionPicker(); }
|
|
|
|
|
function clearRegionPickerDraft() { if (!state.regionPicker) return; state.regionPicker.draft = []; renderRegionPicker(); }
|
|
|
|
@@ -239,11 +251,14 @@ function selectStation(station, fit = false) {
|
|
|
|
|
if (!station) return; state.selected = station; if (fit) state.map?.setZoomAndCenter(14.5, coords(station));
|
|
|
|
|
document.getElementById('detailName').textContent = station.name || station.shortName || '未命名站点'; document.getElementById('detailRegion').textContent = stationRegion(station);
|
|
|
|
|
const cooperative = Boolean(station.cooperative), tag = document.getElementById('detailType'); tag.textContent = cooperative ? '合作站' : '外部站'; tag.classList.toggle('is-station', cooperative);
|
|
|
|
|
const distance = stationDistance(station); document.getElementById('detailFields').innerHTML = `${detailField('站点状态', cooperative ? '合作站点' : '外部站点')}${detailField('行政区域', stationRegion(station))}${detailField('详细地址', station.address || '暂无详细地址', true)}${Number.isFinite(distance) ? detailField('距离我', `${formatNumber(distance, 1)} km`) : ''}`;
|
|
|
|
|
const distance = stationDistance(station), partnerFields = cooperative ? `${detailField('单价', stationUnitPrice(station) || '暂无')}${detailField('联系人', station.contactPerson || '暂无')}${detailPhoneField('联系方式', station.contactPhone)}` : '';
|
|
|
|
|
document.getElementById('detailFields').innerHTML = `${detailField('站点状态', cooperative ? '合作站点' : '外部站点')}${partnerFields}${detailField('行政区域', stationRegion(station))}${detailField('详细地址', station.address || '暂无详细地址', true)}${Number.isFinite(distance) ? detailField('距离我', `${formatNumber(distance, 1)} km`) : ''}`;
|
|
|
|
|
document.getElementById('stationDetail').hidden = false; renderMarkers(); renderList();
|
|
|
|
|
}
|
|
|
|
|
function detailField(label, value, full = false) { return `<div class="detail-field${full ? ' is-full' : ''}"><dt>${escapeHTML(label)}</dt><dd>${escapeHTML(value)}</dd></div>`; }
|
|
|
|
|
function stationRegion(station) { return [station.province, station.city, stationDistrictName(station)].filter(value => value && value !== '[]').filter((value, index, list) => index === 0 || value !== list[index - 1]).join(' · '); }
|
|
|
|
|
function detailPhoneField(label, phone) { const value = String(phone || '').trim(), href = value.replace(/[^\d+]/g, ''); return `<div class="detail-field"><dt>${escapeHTML(label)}</dt><dd>${href ? `<a class="detail-phone-link" href="tel:${escapeAttribute(href)}">${escapeHTML(value)}</a>` : '暂无'}</dd></div>`; }
|
|
|
|
|
function stationUnitPrice(station) { const value = Number(station?.unitPrice); return Number.isFinite(value) && value > 0 ? `¥${formatNumber(value, 2)} / kg` : ''; }
|
|
|
|
|
function stationRegion(station) { return compactPath([station.province, station.city, stationDistrictName(station)].filter(value => value && value !== '[]')); }
|
|
|
|
|
function closeDetail() { const detail = document.getElementById('stationDetail'); if (detail) detail.hidden = true; }
|
|
|
|
|
|
|
|
|
|
function locateMe() { if (!navigator.geolocation) return locationFailed('当前设备不支持定位'); const button = document.getElementById('locateDeviceBtn'); button.disabled = true; button.classList.remove('is-error'); document.getElementById('locateDeviceLabel').textContent = '定位中'; navigator.geolocation.getCurrentPosition(position => { state.userLocation = wgs84ToGcj02(Number(position.coords.longitude), Number(position.coords.latitude)); button.disabled = false; button.classList.add('is-active'); document.getElementById('locateDeviceLabel').textContent = '已定位'; state.map?.setZoomAndCenter(13, state.userLocation); document.getElementById('selectedRegionHint').textContent = '视角: 我的位置附近'; render(); }, error => locationFailed(error?.code === 1 ? '请允许位置权限' : '定位失败'), { enableHighAccuracy: true, timeout: 10000, maximumAge: 30000 }); }
|
|
|
|
@@ -254,7 +269,7 @@ function distanceKm(origin, target) { const radians = value => value * Math.PI /
|
|
|
|
|
function resetMap() { state.map?.setZoomAndCenter(4.8, [108.948024, 34.263161]); state.map?.setPitch(30); document.getElementById('selectedRegionHint').textContent = '视角: 全国加氢站网络'; }
|
|
|
|
|
function toggleMapPitch() { state.pitch = !state.pitch; state.map?.setPitch(state.pitch ? 30 : 0); }
|
|
|
|
|
function setTheme(theme) { document.body.className = theme; document.querySelectorAll('.bw-btn').forEach(button => button.classList.remove('active')); document.querySelector(theme === 'theme-white' ? '.theme-white-btn' : '.theme-dark-btn')?.classList.add('active'); state.map?.setMapStyle(theme === 'theme-white' ? 'amap://styles/light' : 'amap://styles/darkblue'); }
|
|
|
|
|
function navigateToStation() { if (!state.selected) return; const [lng, lat] = coords(state.selected), name = encodeURIComponent(state.selected.name || state.selected.shortName || '加氢站'); const overlay = document.getElementById('navigationOverlay'); overlay.hidden = false; window.setTimeout(() => window.location.assign(`https://uri.amap.com/navigation?to=${lng.toFixed(6)},${lat.toFixed(6)},${name}&mode=car&coordinate=gaode&callnative=1`), 140); }
|
|
|
|
|
function navigateToStation() { if (!state.selected) return; const [lng, lat] = coords(state.selected), name = encodeURIComponent(state.selected.name || state.selected.shortName || '加氢站'), target = window.open('', '_blank'); const overlay = document.getElementById('navigationOverlay'); overlay.hidden = false; window.setTimeout(() => { const url = `https://uri.amap.com/navigation?to=${lng.toFixed(6)},${lat.toFixed(6)},${name}&mode=car&coordinate=gaode&callnative=1`; if (target) { target.opener = null; target.location.href = url; } else window.open(url, '_blank', 'noopener'); }, 140); }
|
|
|
|
|
function setStatus(text, error = false) { const status = document.getElementById('mapStatusText'); if (status) status.textContent = text; status?.parentElement?.classList.toggle('is-error', error); }
|
|
|
|
|
function setHTML(id, value) { const element = document.getElementById(id); if (element) element.innerHTML = value; }
|
|
|
|
|
function formatNumber(value, digits = 0) { return Number(value || 0).toLocaleString('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits }); }
|
|
|
|
|