feat(navigation): add multi-region picker
This commit is contained in:
+57
-11
@@ -1,4 +1,4 @@
|
||||
const state = { stations: [], query: '', searchText: '', locationFilters: [], suggestions: [], activeSuggestion: -1, searchDebounce: null, stationFilter: 'all', selected: null, userLocation: null, map: null, markers: [], userMarker: null, pitch: true };
|
||||
const state = { stations: [], query: '', searchText: '', locationFilters: [], suggestions: [], activeSuggestion: -1, searchDebounce: null, stationFilter: 'all', selected: null, userLocation: null, map: null, markers: [], userMarker: null, pitch: true, regionPicker: null };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => { initClock(); initMap(); loadStations(); });
|
||||
document.addEventListener('pointerdown', event => { if (!event.target?.closest?.('.map-explore-toolbar')) closeStationSearchSuggestions(); });
|
||||
@@ -45,7 +45,7 @@ function stationMatchesLocation(station) { return !state.locationFilters.length
|
||||
function stationTypeScope() { return state.stations.filter(station => state.stationFilter !== 'partner' || station.cooperative); }
|
||||
function filteredStations() { return stationTypeScope().filter(station => stationMatchesLocation(station) && stationMatchesQuery(station)); }
|
||||
function stationSearchScore(station, query) {
|
||||
const fields = [station.name, station.shortName, station.province, station.city, station.district, station.address].filter(Boolean);
|
||||
const fields = [station.name, station.shortName, station.address].filter(Boolean);
|
||||
for (const value of fields) { const text = normalize(value); if (text.includes(query)) return 3; const pinyin = toPinyin(value); if (pinyin.includes(query) || initials(pinyin).includes(query)) return 2; }
|
||||
return 0;
|
||||
}
|
||||
@@ -123,7 +123,8 @@ function stationLocationOptions() {
|
||||
const add = (level, station) => {
|
||||
const province = station.province || '', city = station.city || '', district = station.district || '';
|
||||
const key = level === 'province' ? `province|${province}` : level === 'city' ? `city|${province}|${city}` : `district|${province}|${city}|${district}`;
|
||||
if (!groups.has(key)) groups.set(key, { type: 'location', level, province, city, district, stations: [] });
|
||||
const filter = level === 'province' ? { province, city: '', district: '' } : level === 'city' ? { province, city, district: '' } : { province, city, district };
|
||||
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 }); }
|
||||
@@ -145,13 +146,9 @@ function stationEntitySuggestions(query) {
|
||||
}
|
||||
function renderStationSearchSuggestions() {
|
||||
const box = document.getElementById('stationSearchSuggestions'); if (!box || box.hidden) return;
|
||||
const locations = stationLocationSuggestions(state.searchText), stations = stationEntitySuggestions(state.searchText);
|
||||
state.suggestions = [...locations, ...stations]; state.activeSuggestion = Math.min(state.activeSuggestion, state.suggestions.length - 1);
|
||||
if (!state.suggestions.length) { box.innerHTML = '<span class="suggestion-empty">未找到匹配的位置或站点</span>'; return; }
|
||||
let html = '';
|
||||
if (locations.length) html += `<span class="suggestion-section-label">位置</span>${locations.map((item, index) => `<button type="button" class="explore-suggestion${index === state.activeSuggestion ? ' is-active' : ''}" role="option" onclick="selectStationSearchSuggestion(${index})"><span class="suggestion-symbol is-location">⌖</span><span class="suggestion-copy"><strong>${escapeHTML(item.label)}</strong><small>${escapeHTML(item.path)}</small></span><span class="suggestion-kind">地点</span></button>`).join('')}`;
|
||||
if (stations.length) html += `<span class="suggestion-section-label">加氢站</span>${stations.map((item, offset) => { const index = locations.length + offset, station = item.station; return `<button type="button" class="explore-suggestion${index === state.activeSuggestion ? ' is-active' : ''}" role="option" onclick="selectStationSearchSuggestion(${index})"><span class="suggestion-symbol">H₂</span><span class="suggestion-copy"><strong>${escapeHTML(station.name || station.shortName || '未命名站点')}</strong><small>${escapeHTML(stationRegion(station) || station.address || '')}</small></span><span class="suggestion-kind">加氢站</span></button>`; }).join('')}`;
|
||||
box.innerHTML = html;
|
||||
state.suggestions = stationEntitySuggestions(state.searchText); state.activeSuggestion = Math.min(state.activeSuggestion, state.suggestions.length - 1);
|
||||
if (!state.suggestions.length) { box.innerHTML = '<span class="suggestion-empty">未找到匹配的站点或详细地址</span>'; return; }
|
||||
box.innerHTML = `<span class="suggestion-section-label">加氢站</span>${state.suggestions.map((item, index) => { const station = item.station; return `<button type="button" class="explore-suggestion${index === state.activeSuggestion ? ' is-active' : ''}" role="option" onclick="selectStationSearchSuggestion(${index})"><span class="suggestion-symbol">H₂</span><span class="suggestion-copy"><strong>${escapeHTML(station.name || station.shortName || '未命名站点')}</strong><small>${escapeHTML(station.address || stationRegion(station) || '')}</small></span><span class="suggestion-kind">加氢站</span></button>`; }).join('')}`;
|
||||
}
|
||||
function openStationSearchSuggestions() { const box = document.getElementById('stationSearchSuggestions'); if (!box) return; box.hidden = false; renderStationSearchSuggestions(); }
|
||||
function closeStationSearchSuggestions() { const box = document.getElementById('stationSearchSuggestions'); if (box) box.hidden = true; state.activeSuggestion = -1; }
|
||||
@@ -160,6 +157,7 @@ function renderLocationFilterChips() {
|
||||
const rail = document.getElementById('selectedLocationFilters'); if (!rail) return;
|
||||
rail.hidden = !state.locationFilters.length;
|
||||
rail.innerHTML = state.locationFilters.map(filter => { const key = locationFilterKey(filter), label = locationFilterLabel(filter); return `<button class="selected-location-filter-chip" type="button" onclick="removeLocationFilter('${escapeAttribute(key)}')" aria-label="移除位置筛选 ${escapeAttribute(label)}"><span>${escapeHTML(label)}</span><b aria-hidden="true">×</b></button>`; }).join('');
|
||||
const count = document.getElementById('regionPickerTriggerCount'); if (count) { count.hidden = !state.locationFilters.length; count.textContent = state.locationFilters.length; }
|
||||
}
|
||||
function removeLocationFilter(key) {
|
||||
state.locationFilters = state.locationFilters.filter(filter => locationFilterKey(filter) !== key);
|
||||
@@ -171,6 +169,55 @@ function addLocationFilter(option) {
|
||||
if (!state.locationFilters.some(current => locationFilterKey(current) === locationFilterKey(filter))) state.locationFilters.push(filter);
|
||||
renderLocationFilterChips();
|
||||
}
|
||||
function cloneLocationFilter(filter) { return { level: filter.level, province: filter.province || '', city: filter.city || '', district: filter.district || '', label: filter.label || '' }; }
|
||||
function openRegionPicker() {
|
||||
state.regionPicker = { level: 'province', province: '', city: '', search: '', draft: state.locationFilters.map(cloneLocationFilter), options: [] };
|
||||
const modal = document.getElementById('regionPickerModal'); if (modal) modal.hidden = false;
|
||||
renderRegionPicker(); window.setTimeout(() => document.getElementById('regionPickerSearch')?.focus(), 0);
|
||||
}
|
||||
function closeRegionPicker() { state.regionPicker = null; const modal = document.getElementById('regionPickerModal'); if (modal) modal.hidden = true; }
|
||||
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 === '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);
|
||||
}
|
||||
function regionPickerOptionPath(option) {
|
||||
const parts = option.level === 'province' ? [option.province] : option.level === 'city' ? [option.province, option.city] : [option.province, option.city, option.district];
|
||||
return `${compactPath(parts)} · ${formatNumber(option.stations.length)} 座站点`;
|
||||
}
|
||||
function regionPickerOptionSelected(option) { return state.regionPicker?.draft.some(filter => locationFilterKey(filter) === locationFilterKey(option)); }
|
||||
function renderRegionPicker() {
|
||||
const picker = state.regionPicker; if (!picker) return;
|
||||
const search = document.getElementById('regionPickerSearch'); if (search && search.value !== picker.search) search.value = picker.search;
|
||||
const clear = document.getElementById('regionPickerSearchClear'); if (clear) clear.hidden = !picker.search;
|
||||
const selected = document.getElementById('regionPickerSelected');
|
||||
if (selected) selected.innerHTML = picker.draft.length ? `<span class="region-picker-selected-label">已选</span>${picker.draft.map((filter, index) => `<button type="button" class="region-picker-selected-chip" onclick="removeRegionPickerDraft(${index})">${escapeHTML(locationFilterLabel(filter))}<b aria-hidden="true">×</b></button>`).join('')}` : '<span class="region-picker-selected-placeholder">可同时选择多个省 / 市 / 区县</span>';
|
||||
const path = document.getElementById('regionPickerPath');
|
||||
if (path) {
|
||||
const crumbs = [{ label: '全部省份', level: 'province' }];
|
||||
if (picker.province) crumbs.push({ label: stationGroupName({ province: picker.province }, 'province'), level: 'city' });
|
||||
if (picker.city) crumbs.push({ label: stationGroupName({ city: picker.city }, 'city'), level: 'district' });
|
||||
path.innerHTML = picker.search ? '<span>搜索结果</span>' : crumbs.map((crumb, index) => `<button type="button" class="${index === crumbs.length - 1 ? 'is-current' : ''}" onclick="navigateRegionPicker('${crumb.level}')">${escapeHTML(crumb.label)}</button>`).join('<i>›</i>');
|
||||
}
|
||||
const options = regionPickerOptions(); picker.options = options;
|
||||
const list = document.getElementById('regionPickerList');
|
||||
if (list) list.innerHTML = options.length ? options.map((option, index) => {
|
||||
const selectedOption = regionPickerOptionSelected(option), canDescend = !picker.search && option.level !== 'district';
|
||||
return `<article class="region-picker-row${selectedOption ? ' is-selected' : ''}"><button type="button" class="region-picker-check" onclick="toggleRegionPickerOption(${index})" aria-label="${selectedOption ? '取消选择' : '选择'} ${escapeAttribute(option.path)}"><span>${selectedOption ? '✓' : ''}</span></button><button type="button" class="region-picker-row-main" onclick="toggleRegionPickerOption(${index})"><strong>${escapeHTML(option.label)}</strong><small>${escapeHTML(regionPickerOptionPath(option))}</small></button>${canDescend ? `<button type="button" class="region-picker-next" onclick="descendRegionPicker(${index})" aria-label="浏览${escapeAttribute(option.label)}下级">›</button>` : '<span class="region-picker-next is-empty"></span>'}</article>`;
|
||||
}).join('') : '<div class="region-picker-empty">没有可选择的运营区域</div>';
|
||||
const summary = document.getElementById('regionPickerSummary'); if (summary) summary.textContent = `已选 ${picker.draft.length} 项`;
|
||||
const confirm = document.querySelector('.region-picker-confirm'); if (confirm) confirm.textContent = picker.draft.length ? `确认(${picker.draft.length})` : '确认';
|
||||
}
|
||||
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 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(); }
|
||||
function confirmRegionPicker() { if (!state.regionPicker) return; state.locationFilters = state.regionPicker.draft.map(cloneLocationFilter); state.query = ''; state.searchText = ''; const input = document.getElementById('stationSearch'); if (input) input.value = ''; const clear = document.getElementById('clearSearch'); if (clear) clear.hidden = true; state.selected = null; closeDetail(); closeStationSearchSuggestions(); renderLocationFilterChips(); closeRegionPicker(); render(); if (state.locationFilters.length) { focusSearchResults(); document.getElementById('selectedRegionHint').textContent = `视角: 已筛选 ${state.locationFilters.map(locationFilterLabel).join('、')}`; } else resetMap(); }
|
||||
function focusSearchResults() {
|
||||
const stations = filteredStations(); if (!state.map || !stations.length) return;
|
||||
const longitudes = stations.map(station => Number(station.longitude)), latitudes = stations.map(station => Number(station.latitude));
|
||||
@@ -182,7 +229,6 @@ function focusSearchResults() {
|
||||
function handleStationSearchInput(value) { state.searchText = value; document.getElementById('clearSearch').hidden = !normalize(value); state.activeSuggestion = -1; openStationSearchSuggestions(); window.clearTimeout(state.searchDebounce); state.searchDebounce = window.setTimeout(() => { state.query = value; state.selected = null; closeDetail(); render(); if (normalize(value)) focusSearchResults(); }, 180); }
|
||||
function selectStationSearchSuggestion(index) {
|
||||
const item = state.suggestions[index]; if (!item) return;
|
||||
if (item.type === 'location') { addLocationFilter(item); state.query = ''; state.searchText = ''; document.getElementById('stationSearch').value = ''; document.getElementById('clearSearch').hidden = true; state.map?.setZoomAndCenter(item.level === 'province' ? 7.2 : item.level === 'city' ? 10.5 : 13, item.lnglat); document.getElementById('selectedRegionHint').textContent = `视角: 已筛选 ${state.locationFilters.map(locationFilterLabel).join('、')}`; closeStationSearchSuggestions(); render(); return; }
|
||||
closeStationSearchSuggestions(); selectStation(item.station, true);
|
||||
}
|
||||
function setStationFilter(filter) { if (!['all', 'partner'].includes(filter) || state.stationFilter === filter) return; state.stationFilter = filter; state.selected = null; closeDetail(); document.querySelectorAll('[data-station-filter]').forEach(button => button.classList.toggle('is-active', button.dataset.stationFilter === filter)); render(); renderStationSearchSuggestions(); }
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<div class="map-top-bar"><div class="status-glass-pill"><span class="pulse-ring"></span><span id="mapStatusText">正在同步加氢站数据…</span></div></div>
|
||||
<div class="map-explore-toolbar" aria-label="地图搜索与定位">
|
||||
<button class="locate-btn" id="locateDeviceBtn" type="button" onclick="locateMe()" aria-label="定位附近站点"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M12 2v3M12 19v3M2 12h3M19 12h3"></path><circle cx="12" cy="12" r="8"></circle></svg><span id="locateDeviceLabel">定位</span></button>
|
||||
<div class="explore-search-shell"><label class="entity-search-field" for="stationSearch"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.6-3.6"></path></svg><input id="stationSearch" type="search" autocomplete="off" placeholder="搜索位置、站点或地址" aria-label="搜索位置、站点或地址" oninput="handleStationSearchInput(this.value)" onfocus="openStationSearchSuggestions()" onkeydown="handleSearchKeydown(event)"><button class="search-clear-btn" id="clearSearch" type="button" onclick="clearSearch()" aria-label="清除搜索" hidden>×</button></label><div class="explore-suggestions" id="stationSearchSuggestions" role="listbox" aria-label="搜索建议" hidden></div></div>
|
||||
<button class="region-picker-trigger" id="regionPickerTrigger" type="button" onclick="openRegionPicker()" aria-haspopup="dialog"><span class="region-picker-trigger-icon">⌖</span><span>运营区域</span><b id="regionPickerTriggerCount" hidden>0</b></button>
|
||||
<div class="explore-search-shell"><label class="entity-search-field" for="stationSearch"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.6-3.6"></path></svg><input id="stationSearch" type="search" autocomplete="off" placeholder="搜索站点或详细地址" aria-label="搜索站点或详细地址" oninput="handleStationSearchInput(this.value)" onfocus="openStationSearchSuggestions()" onkeydown="handleSearchKeydown(event)"><button class="search-clear-btn" id="clearSearch" type="button" onclick="clearSearch()" aria-label="清除搜索" hidden>×</button></label><div class="explore-suggestions" id="stationSearchSuggestions" role="listbox" aria-label="站点搜索建议" hidden></div></div>
|
||||
<div class="selected-location-filter-rail" id="selectedLocationFilters" aria-label="已选位置" hidden></div>
|
||||
<div class="filter-chip-rail" id="filterChipRail" role="group" aria-label="站点类型筛选"><button class="map-filter-chip is-active" type="button" data-station-filter="all" onclick="setStationFilter('all')">全部站点</button><button class="map-filter-chip" type="button" data-station-filter="partner" onclick="setStationFilter('partner')">合作站</button></div>
|
||||
<span class="filter-result-meta" id="resultMeta" aria-live="polite">全部 0</span>
|
||||
@@ -61,6 +62,16 @@
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
<div class="region-picker-backdrop" id="regionPickerModal" role="dialog" aria-modal="true" aria-labelledby="regionPickerTitle" hidden>
|
||||
<section class="region-picker-dialog">
|
||||
<header class="region-picker-header"><div><span class="region-picker-eyebrow">加氢站导航</span><h2 id="regionPickerTitle">运营区域选择 <em>多选</em></h2></div><button type="button" class="region-picker-close" onclick="closeRegionPicker()" aria-label="关闭">×</button></header>
|
||||
<div class="region-picker-selected" id="regionPickerSelected"></div>
|
||||
<label class="region-picker-search" for="regionPickerSearch"><span>⌕</span><input id="regionPickerSearch" type="search" autocomplete="off" placeholder="搜索省 / 市 / 区县" aria-label="搜索省、市或区县" oninput="setRegionPickerSearch(this.value)"><button type="button" id="regionPickerSearchClear" onclick="setRegionPickerSearch('')" aria-label="清除搜索" hidden>×</button></label>
|
||||
<nav class="region-picker-path" id="regionPickerPath" aria-label="行政区域路径"></nav>
|
||||
<div class="region-picker-list" id="regionPickerList" aria-live="polite"></div>
|
||||
<footer class="region-picker-footer"><span id="regionPickerSummary">已选 0 项</span><div><button type="button" class="region-picker-text-button" onclick="clearRegionPickerDraft()">清空</button><button type="button" class="region-picker-confirm" onclick="confirmRegionPicker()">确认</button></div></footer>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pinyin-pro@3.28.2/dist/index.js" crossorigin="anonymous"></script>
|
||||
<script src="app.js?v=__ASSET_VERSION__"></script>
|
||||
</body>
|
||||
|
||||
@@ -6,6 +6,51 @@
|
||||
.station-navigation-shell .map-filter-chip { min-height: 27px; padding: 0 9px; border: 1px solid var(--glass-border); border-radius: 8px; background: var(--pill-bg); color: var(--text-muted); font-size: 10px; font-weight: 700; cursor: pointer; }
|
||||
.station-navigation-shell .map-filter-chip.is-active { border-color: rgba(0,113,67,.24); background: rgba(0,113,67,.1); color: var(--accent-primary); }
|
||||
.station-navigation-shell .filter-chip-rail { display: flex; gap: 6px; }
|
||||
.station-navigation-shell .region-picker-trigger { display: inline-flex; align-items: center; gap: 5px; min-height: 29px; padding: 0 9px; border: 1px solid var(--glass-border); border-radius: 9px; background: var(--pill-bg); color: var(--text-main); font-size: 10px; font-weight: 700; white-space: nowrap; cursor: pointer; }
|
||||
.station-navigation-shell .region-picker-trigger:hover { border-color: rgba(0,113,67,.32); color: var(--accent-primary); }
|
||||
.station-navigation-shell .region-picker-trigger-icon { color: var(--accent-primary); font-size: 14px; line-height: 1; }
|
||||
.station-navigation-shell .region-picker-trigger b { display: grid; min-width: 15px; height: 15px; place-items: center; border-radius: 99px; background: var(--accent-primary); color: #fff; font-size: 9px; }
|
||||
.region-picker-backdrop { position: fixed; z-index: 1000; inset: 0; display: grid; place-items: center; padding: 20px; background: rgba(15,23,42,.38); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); }
|
||||
.region-picker-backdrop[hidden] { display: none; }
|
||||
.region-picker-dialog { display: flex; width: min(500px, 100%); max-height: min(720px, calc(100dvh - 40px)); flex-direction: column; overflow: hidden; border: 1px solid var(--glass-border); border-radius: 18px; background: var(--glass-bg); box-shadow: 0 28px 70px rgba(15,23,42,.28); }
|
||||
.region-picker-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 17px 12px; border-bottom: 1px solid var(--glass-border); }
|
||||
.region-picker-eyebrow { display: block; margin-bottom: 3px; color: var(--text-muted); font-size: 9px; font-weight: 700; letter-spacing: .07em; }
|
||||
.region-picker-header h2 { margin: 0; color: var(--text-main); font-size: 16px; letter-spacing: -.02em; }
|
||||
.region-picker-header h2 em { margin-left: 5px; color: var(--accent-primary); font-size: 11px; font-style: normal; font-weight: 700; }
|
||||
.region-picker-close { display: grid; width: 30px; height: 30px; flex: 0 0 30px; place-items: center; border: 1px solid var(--glass-border); border-radius: 9px; background: var(--pill-bg); color: var(--text-muted); font: 400 20px/1 -apple-system, BlinkMacSystemFont, sans-serif; cursor: pointer; }
|
||||
.region-picker-selected { display: flex; min-height: 38px; align-items: center; gap: 6px; overflow-x: auto; padding: 8px 17px; border-bottom: 1px solid var(--glass-border); scrollbar-width: none; }
|
||||
.region-picker-selected::-webkit-scrollbar { display: none; }
|
||||
.region-picker-selected-label { flex: 0 0 auto; color: var(--text-muted); font-size: 10px; font-weight: 700; }
|
||||
.region-picker-selected-placeholder { color: var(--text-muted); font-size: 11px; }
|
||||
.region-picker-selected-chip { display: inline-flex; min-height: 25px; flex: 0 0 auto; align-items: center; gap: 6px; padding: 0 8px; border: 1px solid rgba(0,113,67,.2); border-radius: 99px; background: rgba(0,113,67,.09); color: var(--accent-primary); font-size: 10px; font-weight: 700; cursor: pointer; }
|
||||
.region-picker-selected-chip b { font: 700 14px/1 -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
.region-picker-search { display: flex; height: 37px; align-items: center; gap: 8px; margin: 12px 17px 9px; padding: 0 10px; border: 1px solid var(--glass-border); border-radius: 10px; background: var(--pill-bg); color: var(--text-muted); }
|
||||
.region-picker-search span { font-size: 17px; line-height: 1; }
|
||||
.region-picker-search input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--text-main); font: 11px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
.region-picker-search button { display: grid; width: 19px; height: 19px; place-items: center; border: 0; border-radius: 50%; background: var(--segmented-bg); color: var(--text-muted); font-size: 14px; cursor: pointer; }
|
||||
.region-picker-search button[hidden] { display: none; }
|
||||
.region-picker-path { display: flex; min-height: 28px; align-items: center; gap: 6px; padding: 0 17px 7px; color: var(--text-muted); font-size: 10px; }
|
||||
.region-picker-path button { padding: 0; border: 0; background: transparent; color: var(--text-muted); font-size: inherit; cursor: pointer; }
|
||||
.region-picker-path button.is-current { color: var(--text-main); font-weight: 700; cursor: default; }
|
||||
.region-picker-path i { color: var(--text-sub); font-style: normal; }
|
||||
.region-picker-list { min-height: 240px; flex: 1 1 auto; overflow-y: auto; padding: 2px 9px 10px; }
|
||||
.region-picker-row { display: grid; grid-template-columns: 31px minmax(0, 1fr) 32px; min-height: 54px; align-items: center; border-bottom: 1px solid color-mix(in srgb, var(--glass-border) 72%, transparent); }
|
||||
.region-picker-row.is-selected { background: rgba(0,113,67,.045); }
|
||||
.region-picker-check { display: grid; width: 19px; height: 19px; margin-left: 6px; place-items: center; border: 1px solid var(--glass-border); border-radius: 5px; background: var(--pill-bg); color: #fff; font-size: 12px; cursor: pointer; }
|
||||
.region-picker-row.is-selected .region-picker-check { border-color: var(--accent-primary); background: var(--accent-primary); }
|
||||
.region-picker-row-main { min-width: 0; padding: 8px 3px; border: 0; background: transparent; color: var(--text-main); text-align: left; cursor: pointer; }
|
||||
.region-picker-row-main strong { display: block; overflow: hidden; font-size: 12px; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.region-picker-row-main small { display: block; overflow: hidden; margin-top: 2px; color: var(--text-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.region-picker-next { display: grid; width: 30px; height: 30px; place-items: center; border: 0; background: transparent; color: var(--text-muted); font: 400 22px/1 -apple-system, BlinkMacSystemFont, sans-serif; cursor: pointer; }
|
||||
.region-picker-next:hover { color: var(--accent-primary); }
|
||||
.region-picker-next.is-empty { visibility: hidden; }
|
||||
.region-picker-empty { display: grid; min-height: 170px; place-items: center; color: var(--text-muted); font-size: 11px; }
|
||||
.region-picker-footer { display: flex; min-height: 60px; align-items: center; justify-content: space-between; gap: 14px; padding: 10px 17px; border-top: 1px solid var(--glass-border); }
|
||||
.region-picker-footer > span { color: var(--text-muted); font-size: 11px; }
|
||||
.region-picker-footer > div { display: flex; align-items: center; gap: 8px; }
|
||||
.region-picker-text-button, .region-picker-confirm { min-height: 32px; padding: 0 12px; border-radius: 8px; font-size: 11px; font-weight: 700; cursor: pointer; }
|
||||
.region-picker-text-button { border: 1px solid var(--glass-border); background: var(--pill-bg); color: var(--text-muted); }
|
||||
.region-picker-confirm { border: 1px solid var(--accent-primary); background: var(--accent-primary); color: #fff; }
|
||||
.station-navigation-shell .selected-location-filter-rail { display: flex; align-items: center; gap: 6px; min-width: 0; max-width: min(360px, 30vw); overflow-x: auto; scrollbar-width: none; }
|
||||
.station-navigation-shell .selected-location-filter-rail::-webkit-scrollbar { display: none; }
|
||||
.station-navigation-shell .selected-location-filter-rail[hidden] { display: none; }
|
||||
@@ -76,6 +121,9 @@
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .region-picker-trigger { order: 2; min-height: 36px; padding: 0 10px; border-radius: 10px; }
|
||||
.station-navigation-shell .explore-search-shell { order: 3; flex: 1 1 150px; }
|
||||
|
||||
.station-navigation-shell .locate-btn {
|
||||
flex-basis: 36px;
|
||||
width: 36px;
|
||||
@@ -89,10 +137,18 @@
|
||||
}
|
||||
|
||||
.station-navigation-shell .entity-search-field input { font-size: 12px; }
|
||||
.station-navigation-shell .selected-location-filter-rail { order: 3; flex: 1 0 100%; max-width: none; padding-bottom: 1px; }
|
||||
.station-navigation-shell .filter-chip-rail { order: 4; max-width: none; overflow: visible; }
|
||||
.station-navigation-shell .selected-location-filter-rail { order: 4; flex: 1 0 100%; max-width: none; padding-bottom: 1px; }
|
||||
.station-navigation-shell .filter-chip-rail { order: 5; max-width: none; overflow: visible; }
|
||||
.station-navigation-shell .map-filter-chip { min-height: 30px; padding: 0 10px; font-size: 10px; }
|
||||
.station-navigation-shell .filter-result-meta { order: 5; padding-right: 5px; font-size: 9px; }
|
||||
.station-navigation-shell .filter-result-meta { order: 6; padding-right: 5px; font-size: 9px; }
|
||||
|
||||
.region-picker-backdrop { align-items: end; padding: 0; }
|
||||
.region-picker-dialog { width: 100%; max-height: min(82dvh, 700px); border-radius: 20px 20px 0 0; }
|
||||
.region-picker-header { padding: 14px 15px 10px; }
|
||||
.region-picker-selected { padding: 8px 15px; }
|
||||
.region-picker-search { margin: 10px 15px 7px; }
|
||||
.region-picker-path { padding: 0 15px 6px; }
|
||||
.region-picker-footer { padding: 10px 15px calc(10px + env(safe-area-inset-bottom)); }
|
||||
|
||||
.station-navigation-shell .explore-suggestions {
|
||||
width: calc(100vw - 32px);
|
||||
|
||||
@@ -41,12 +41,28 @@ vm.runInNewContext(`
|
||||
globalThis.cityNodeCount = buildStationNodes(filteredStations(), 'city').length;
|
||||
globalThis.stationNodeCount = buildStationNodes(filteredStations(), 'station').length;
|
||||
globalThis.locationOptionCount = stationLocationOptions().length;
|
||||
globalThis.provinceOptionScope = stationLocationOptions().find(option => option.level === 'province' && option.province === '广东省');
|
||||
globalThis.cityOptionScope = stationLocationOptions().find(option => option.level === 'city' && option.city === '广州市');
|
||||
`, sandbox);
|
||||
assert.equal(sandbox.hierarchy.join(','), 'province,city,station');
|
||||
assert.equal(sandbox.provinceNodeCount, 2);
|
||||
assert.equal(sandbox.cityNodeCount, 3);
|
||||
assert.equal(sandbox.stationNodeCount, 3);
|
||||
assert.equal(sandbox.locationOptionCount, 8);
|
||||
assert.equal(sandbox.provinceOptionScope.city, '');
|
||||
assert.equal(sandbox.provinceOptionScope.district, '');
|
||||
assert.equal(sandbox.cityOptionScope.district, '');
|
||||
vm.runInNewContext(`
|
||||
state.regionPicker = { level: 'province', province: '', city: '', search: '', draft: [], options: [] };
|
||||
globalThis.regionProvinceLabels = regionPickerOptions().map(option => option.label).sort().join(',');
|
||||
state.regionPicker = { level: 'city', province: '广东省', city: '', search: '', draft: [], options: [] };
|
||||
globalThis.regionCityLabels = regionPickerOptions().map(option => option.label).sort().join(',');
|
||||
state.regionPicker = { level: 'province', province: '', city: '', search: '嘉兴', draft: [], options: [] };
|
||||
globalThis.regionSearchLabels = regionPickerOptions().map(option => option.label).join(',');
|
||||
`, sandbox);
|
||||
assert.equal(sandbox.regionProvinceLabels, '广东,浙江');
|
||||
assert.equal(sandbox.regionCityLabels, '佛山,广州');
|
||||
assert.match(sandbox.regionSearchLabels, /嘉兴/);
|
||||
vm.runInNewContext(`
|
||||
globalThis.inferredDistrict = stationDistrictName({ province: '浙江省', city: '嘉兴市', address: '嘉兴市海盐县海盐经济开发区' });
|
||||
state.locationFilters = [];
|
||||
@@ -78,6 +94,7 @@ assert.match(source, /stationSearchSuggestions/);
|
||||
assert.match(source, /handleStationSearchInput/);
|
||||
assert.match(source, /locationFilters/);
|
||||
assert.match(source, /renderLocationFilterChips/);
|
||||
assert.match(source, /openRegionPicker/);
|
||||
assert.match(source, /kpiTotal|kpiPartner/);
|
||||
const index = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||
assert.match(index, /station-summary-strip/);
|
||||
|
||||
Reference in New Issue
Block a user