feat(map): support multi-location station filters
This commit is contained in:
+56
-12
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user