feat(map): support multi-location station filters

This commit is contained in:
lingniu
2026-08-12 11:23:16 +08:00
parent 6ef98d03c2
commit 9452e30538
2 changed files with 63 additions and 12 deletions
+56 -12
View File
@@ -111,7 +111,7 @@ let exploreSuggestionItems = [];
let activeExploreSuggestionIndex = -1;
let detailExpanded = false;
let navigationLaunchTimer = null;
const filterState = { province: '', city: '', district: '', query: '' };
const filterState = { province: '', city: '', district: '', query: '', stationLocations: [] };
const REGION_ZOOM = { city: 7, district: 9.5, vehicle: 12 };
const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']);
@@ -178,6 +178,24 @@ function stationMatchesQuery(station, query = filterState.query) {
return [station?.name, station?.shortName, station?.address].some(value => normalizeSearch(value).includes(needle));
}
function stationLocationFilterKey(location) {
return [location?.province, location?.city, location?.district].map(value => String(value || '').trim()).join('|');
}
function stationMatchesLocationFilter(station, location) {
if (location?.province && station.province !== location.province) return false;
if (location?.city && station.city !== location.city) return false;
if (location?.district && stationDistrictName(station) !== location.district) return false;
return true;
}
function stationLocationFilterLabel(location) {
const level = location?.district ? 'district' : location?.city ? 'city' : 'province';
const raw = location?.[level] || '';
if (currentLang === 'en') return administrativeEnglishName(raw, level);
return level === 'province' ? compactProvinceName(raw) : compactRegionName(raw);
}
function selectedVehicleFilterGroup() {
if (filterState.district) return vehicleFilterGroups.districts.find(group => group.adcode === filterState.district) || null;
if (filterState.city) return vehicleFilterGroups.cities.find(group => group.adcode === filterState.city) || null;
@@ -196,16 +214,18 @@ function filteredVehicles(vehicles = dashboard?.vehicles || []) {
}
function filteredStations(stations = dashboard?.stations || []) {
const selectedLocations = filterState.stationLocations || [];
return stations.filter(station => {
if (filterState.province && station.province !== filterState.province) return false;
if (filterState.city && station.city !== filterState.city) return false;
if (filterState.district && stationDistrictName(station) !== filterState.district) return false;
if (selectedLocations.length && !selectedLocations.some(location => stationMatchesLocationFilter(station, location))) return false;
if (!selectedLocations.length && filterState.province && station.province !== filterState.province) return false;
if (!selectedLocations.length && filterState.city && station.city !== filterState.city) return false;
if (!selectedLocations.length && filterState.district && stationDistrictName(station) !== filterState.district) return false;
return stationMatchesQuery(station);
});
}
function isFilterActive() {
return Boolean(filterState.province || filterState.city || filterState.district || normalizeSearch(filterState.query));
return Boolean(filterState.province || filterState.city || filterState.district || filterState.stationLocations?.length || normalizeSearch(filterState.query));
}
function uniqueSortedOptions(values) {
@@ -474,9 +494,18 @@ function closeExploreSuggestions() {
async function selectExploreLocation(option) {
filterState.query = '';
if (currentMode === 'station') {
filterState.province = option.province || '';
filterState.city = option.city || '';
filterState.district = option.district || '';
const location = {
province: option.province || '',
city: option.city || '',
district: option.district || ''
};
const key = stationLocationFilterKey(location);
if (!filterState.stationLocations.some(item => stationLocationFilterKey(item) === key)) filterState.stationLocations.push(location);
// Search selections are multi-select. The conventional dropdowns remain
// available as a separate, single-location filter path.
filterState.province = '';
filterState.city = '';
filterState.district = '';
} else {
vehicleFilterGroups = {
provinces: vehicleExploreCatalog.provinces.length ? vehicleExploreCatalog.provinces : vehicleFilterGroups.provinces,
@@ -489,7 +518,7 @@ async function selectExploreLocation(option) {
}
closeExploreSuggestions();
updateFilterToolbar();
focusCurrentRegion(option.level);
focusCurrentRegion(currentMode === 'station' && filterState.stationLocations.length > 1 ? 'province' : option.level);
applyActiveFilters();
void syncFilterControls(dashboard);
}
@@ -542,11 +571,25 @@ function selectedLocationLabel(level) {
function renderFilterChips() {
const rail = document.getElementById('filterChipRail');
if (!rail) return;
if (currentMode === 'station' && filterState.stationLocations?.length) {
rail.hidden = false;
rail.innerHTML = filterState.stationLocations.map((location, index) => {
const label = stationLocationFilterLabel(location);
return `<span class="filter-chip">${escapeHTML(label)}<button type="button" onclick="removeStationLocationFilter(${index})" aria-label="清除${escapeHTML(label)}">×</button></span>`;
}).join('');
return;
}
const levels = ['province', 'city', 'district'].filter(level => filterState[level]);
rail.hidden = !levels.length;
rail.innerHTML = levels.map(level => `<span class="filter-chip">${escapeHTML(selectedLocationLabel(level))}<button type="button" onclick="clearLocationChip('${level}')" aria-label="清除${escapeHTML(selectedLocationLabel(level))}">×</button></span>`).join('');
}
function removeStationLocationFilter(index) {
if (currentMode !== 'station') return;
filterState.stationLocations.splice(index, 1);
applyActiveFilters();
}
function clearLocationChip(level) {
filterState[level] = '';
if (level === 'province') { filterState.city = ''; filterState.district = ''; }
@@ -661,6 +704,7 @@ function clearNameFilter() {
}
async function handleRegionFilterChange(level, value) {
if (currentMode === 'station') filterState.stationLocations = [];
filterState[level] = value;
if (level === 'province') { filterState.city = ''; filterState.district = ''; }
if (level === 'city') filterState.district = '';
@@ -670,7 +714,7 @@ async function handleRegionFilterChange(level, value) {
}
function clearAllFilters() {
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = '';
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; filterState.stationLocations = [];
closeExploreSuggestions();
closeEntityDetails();
resetMapView();
@@ -1500,7 +1544,7 @@ function switchRankTab(type) {
function switchMode(mode) {
currentMode = mode;
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = '';
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; filterState.stationLocations = [];
closeExploreSuggestions();
selectedEntity = null;
const detailCard = document.getElementById('mapDetailCard');
@@ -1589,7 +1633,7 @@ function renderUserLocationMarker() {
function handleLocationSuccess(position) {
const coordinates = locationCoordinates(position);
if (!coordinates) { handleLocationError(); return; }
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = '';
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; filterState.stationLocations = [];
selectedEntity = null;
const detailCard = document.getElementById('mapDetailCard');
if (detailCard) detailCard.hidden = true;
+7
View File
@@ -121,6 +121,13 @@ filterState.query = '';
filterState.province = '广东省'; filterState.city = '广州市'; filterState.district = '黄埔区';
assert.deepEqual(filteredStations().map(station => station.id), ['GD-1']);
filterState.province = ''; filterState.city = ''; filterState.district = '';
filterState.stationLocations = [
{ province: '广东省', city: '广州市', district: '' },
{ province: '浙江省', city: '嘉兴市', district: '' }
];
assert.deepEqual(filteredStations().map(station => station.id).sort(), ['GD-1', 'ZJ-1']);
assert.equal(stationLocationFilterLabel(filterState.stationLocations[0]), '广州');
filterState.stationLocations = [];
assert.ok(Math.abs(distanceKm([113, 23], [114, 23]) - 102.4) < 1);
assert.equal(locationCoordinates({ coords: { longitude: NaN, latitude: 23 } }), null);
map = {