feat(navigation): refine mobile station explorer
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
const state = { stations: [], query: '', searchText: '', locationFilter: { province: '', city: '', district: '' }, 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 };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => { initClock(); initMap(); loadStations(); });
|
||||
document.addEventListener('pointerdown', event => { if (!event.target?.closest?.('.map-explore-toolbar')) closeStationSearchSuggestions(); });
|
||||
@@ -31,12 +31,17 @@ function updateSummary(summary) {
|
||||
const total = Number(summary.totalStations || 0), partner = Number(summary.cooperativeStations || 0), external = Math.max(0, total - partner);
|
||||
setHTML('kpiTotal', `${formatNumber(total)} <small>座</small>`); setHTML('kpiPartner', `${formatNumber(partner)} <small>座</small>`);
|
||||
setHTML('statusTotal', `${formatNumber(total)} <small>座</small>`); setHTML('statusPartner', `${formatNumber(partner)} <small>座</small>`); setHTML('statusExternal', `${formatNumber(external)} <small>座</small>`);
|
||||
setHTML('mobileStatusTotal', `${formatNumber(total)} <small>座</small>`); setHTML('mobileStatusPartner', `${formatNumber(partner)} <small>座</small>`); setHTML('mobileStatusExternal', `${formatNumber(external)} <small>座</small>`);
|
||||
document.getElementById('partnerSegment').style.width = `${total ? partner * 100 / total : 0}%`;
|
||||
document.getElementById('externalSegment').style.width = `${total ? external * 100 / total : 0}%`;
|
||||
document.getElementById('mobilePartnerSegment').style.width = `${total ? partner * 100 / total : 0}%`;
|
||||
document.getElementById('mobileExternalSegment').style.width = `${total ? external * 100 / total : 0}%`;
|
||||
}
|
||||
|
||||
function stationMatchesQuery(station, query = state.query) { const needle = normalize(query); return !needle || [station.name, station.shortName, station.address].some(value => normalize(value).includes(needle)); }
|
||||
function stationMatchesLocation(station) { const filter = state.locationFilter; return (!filter.province || station.province === filter.province) && (!filter.city || station.city === filter.city) && (!filter.district || stationDistrictName(station) === filter.district); }
|
||||
function locationFilterKey(filter) { return [filter.level || 'location', filter.province || '', filter.city || '', filter.district || ''].join('|'); }
|
||||
function stationMatchesLocationFilter(station, filter) { return (!filter.province || station.province === filter.province) && (!filter.city || station.city === filter.city) && (!filter.district || stationDistrictName(station) === filter.district); }
|
||||
function stationMatchesLocation(station) { return !state.locationFilters.length || state.locationFilters.some(filter => stationMatchesLocationFilter(station, filter)); }
|
||||
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) {
|
||||
@@ -101,7 +106,7 @@ 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' && node.cooperative ? '<span class="station-list-partner-tag">合作</span>' : ''; 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><span class="r-val">${Number.isFinite(distance) ? `${formatNumber(distance, 1)} km` : `${formatNumber(node.count)} 座`}</span></button>`; }).join('');
|
||||
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.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'); }
|
||||
@@ -150,6 +155,22 @@ function renderStationSearchSuggestions() {
|
||||
}
|
||||
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; }
|
||||
function locationFilterLabel(filter) { return filter.label || (filter.district ? stationGroupName({ city: filter.district }, 'city') : filter.city ? stationGroupName({ city: filter.city }, 'city') : stationGroupName({ province: filter.province }, 'province')); }
|
||||
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('');
|
||||
}
|
||||
function removeLocationFilter(key) {
|
||||
state.locationFilters = state.locationFilters.filter(filter => locationFilterKey(filter) !== key);
|
||||
state.selected = null; closeDetail(); renderLocationFilterChips(); render(); renderStationSearchSuggestions();
|
||||
document.getElementById('selectedRegionHint').textContent = state.locationFilters.length ? `视角: 已筛选 ${state.locationFilters.map(locationFilterLabel).join('、')}` : '视角: 全国加氢站网络';
|
||||
}
|
||||
function addLocationFilter(option) {
|
||||
const filter = { level: option.level, province: option.province, city: option.city, district: option.district, label: option.label };
|
||||
if (!state.locationFilters.some(current => locationFilterKey(current) === locationFilterKey(filter))) state.locationFilters.push(filter);
|
||||
renderLocationFilterChips();
|
||||
}
|
||||
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));
|
||||
@@ -161,11 +182,11 @@ 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') { state.locationFilter = { province: item.province, city: item.city, district: item.district }; 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 = `视角: ${item.path}`; closeStationSearchSuggestions(); render(); 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(); }
|
||||
function clearSearch() { window.clearTimeout(state.searchDebounce); document.getElementById('stationSearch').value = ''; state.query = ''; state.searchText = ''; state.locationFilter = { province: '', city: '', district: '' }; closeStationSearchSuggestions(); closeDetail(); resetMap(); render(); }
|
||||
function clearSearch() { window.clearTimeout(state.searchDebounce); document.getElementById('stationSearch').value = ''; state.query = ''; state.searchText = ''; state.locationFilters = []; renderLocationFilterChips(); closeStationSearchSuggestions(); closeDetail(); resetMap(); render(); }
|
||||
function handleSearchKeydown(event) { if (event.key === 'Escape') { closeStationSearchSuggestions(); event.currentTarget.blur(); return; } if (!['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) return; const box = document.getElementById('stationSearchSuggestions'); if (box?.hidden) openStationSearchSuggestions(); if (event.key === 'Enter' && state.activeSuggestion >= 0) { event.preventDefault(); selectStationSearchSuggestion(state.activeSuggestion); return; } if (event.key !== 'Enter') { event.preventDefault(); const direction = event.key === 'ArrowDown' ? 1 : -1; state.activeSuggestion = (state.activeSuggestion + direction + state.suggestions.length) % Math.max(state.suggestions.length, 1); renderStationSearchSuggestions(); } }
|
||||
|
||||
function selectStation(station, fit = false) {
|
||||
|
||||
@@ -33,12 +33,15 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="glass-panel station-summary-strip" aria-label="加氢站类型分布"><div class="panel-header"><span class="panel-title">加氢站类型分布</span><span class="panel-tag">STATUS</span></div><div class="status-glass-grid3"><div class="glass-cell"><div class="cell-num" id="mobileStatusTotal">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-total"></span>全部站点</div></div><div class="glass-cell"><div class="cell-num" id="mobileStatusPartner">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-mint"></span>合作站</div></div><div class="glass-cell"><div class="cell-num" id="mobileStatusExternal">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-blue"></span>外部站</div></div></div><div class="liquid-progress-bar"><div class="seg seg-running" id="mobilePartnerSegment"></div><div class="seg seg-stopped" id="mobileExternalSegment"></div></div></section>
|
||||
|
||||
<main class="cockpit-main-grid">
|
||||
<section class="map-spatial-container floating-glass">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
@@ -53,7 +56,7 @@
|
||||
<div class="map-bottom-info"><span>地图引擎: 羚牛氢能 GIS (AMap 3D Engine)</span><span>站点数据服务 · 公开目录</span><span id="selectedRegionHint">视角: 全国加氢站网络</span></div>
|
||||
</section>
|
||||
<aside class="sidebar-operations">
|
||||
<div class="glass-panel sidebar-card"><div class="panel-header"><span class="panel-title">加氢站类型分布</span><span class="panel-tag">STATUS</span></div><div class="status-glass-grid3"><div class="glass-cell"><div class="cell-num" id="statusTotal">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-total"></span>全部站点</div></div><div class="glass-cell"><div class="cell-num" id="statusPartner">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-mint"></span>合作站</div></div><div class="glass-cell"><div class="cell-num" id="statusExternal">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-blue"></span>外部站</div></div></div><div class="liquid-progress-bar"><div class="seg seg-running" id="partnerSegment"></div><div class="seg seg-stopped" id="externalSegment"></div></div></div>
|
||||
<div class="glass-panel sidebar-card station-status-card"><div class="panel-header"><span class="panel-title">加氢站类型分布</span><span class="panel-tag">STATUS</span></div><div class="status-glass-grid3"><div class="glass-cell"><div class="cell-num" id="statusTotal">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-total"></span>全部站点</div></div><div class="glass-cell"><div class="cell-num" id="statusPartner">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-mint"></span>合作站</div></div><div class="glass-cell"><div class="cell-num" id="statusExternal">-- <small>座</small></div><div class="cell-label"><span class="status-dot dot-blue"></span>外部站</div></div></div><div class="liquid-progress-bar"><div class="seg seg-running" id="partnerSegment"></div><div class="seg seg-stopped" id="externalSegment"></div></div></div>
|
||||
<div class="glass-panel sidebar-card flex-fill-auto"><div class="panel-header"><div class="panel-heading-stack"><span class="panel-title" id="panelRankTitle">加氢站省份 TOP 排名</span><span class="viewport-meta" id="rankViewportMeta"></span></div><div class="glass-tab-control"><button class="gtab active" id="rankPrimaryTab">按站点数</button></div></div><div class="liquid-ranking-list" id="stationList"></div></div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
@@ -125,6 +125,11 @@ def _prewarm_station_directory_cache():
|
||||
class StationNavigationHandler(SimpleHTTPRequestHandler):
|
||||
server_version = "LingniuStationNavigation/1.0"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Static files must resolve from this release directory even when the
|
||||
# local launcher is invoked from the repository root.
|
||||
super().__init__(*args, directory=str(ROOT), **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
if path in {"/", "/index.html"}:
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
/* Keep the public service visually identical to the main map shell. */
|
||||
.station-navigation-shell .header-kpi-group { display: flex; }
|
||||
.station-navigation-shell .station-summary-strip { display: none; }
|
||||
.station-navigation-shell .station-summary-strip .panel-header { margin-bottom: 8px; }
|
||||
.station-navigation-shell .station-summary-strip .status-glass-grid3 { margin-bottom: 7px; }
|
||||
.station-navigation-shell .station-summary-strip .glass-cell { padding: 6px 9px; }
|
||||
.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 .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; }
|
||||
.station-navigation-shell .selected-location-filter-chip { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 7px; min-height: 29px; max-width: 132px; padding: 0 8px 0 10px; border: 1px solid rgba(0,113,67,.22); border-radius: 999px; background: rgba(0,113,67,.09); color: var(--accent-primary); font-size: 10px; font-weight: 700; cursor: pointer; }
|
||||
.station-navigation-shell .selected-location-filter-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.station-navigation-shell .selected-location-filter-chip b { font: 700 15px/1 -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
.station-navigation-shell .province-info-card.is-station-point { min-width: 92px; }
|
||||
.station-navigation-shell .province-info-card.is-station-point .p-total { color: var(--accent-primary); }
|
||||
.station-navigation-shell .province-info-card.is-external .p-total { color: var(--text-muted); }
|
||||
@@ -10,11 +19,136 @@
|
||||
.station-navigation-shell .rank-glass-row.is-selected { background: rgba(0,113,67,.09); border-color: rgba(0,113,67,.2); }
|
||||
.station-navigation-shell .r-name { display: flex; min-width: 0; align-items: flex-start; gap: 5px; line-height: 1.35; }
|
||||
.station-navigation-shell .station-list-name { min-width: 0; overflow-wrap: anywhere; white-space: normal; }
|
||||
.station-navigation-shell .station-list-partner-tag { flex: 0 0 auto; margin-top: 1px; padding: 2px 5px; border-radius: 4px; background: rgba(0,113,67,.1); color: var(--accent-primary); font-size: 9px; font-weight: 700; line-height: 1; }
|
||||
.station-navigation-shell .station-list-partner-tag { flex: 0 0 30px; min-width: 30px; margin-top: 1px; padding: 2px 5px; border-radius: 4px; background: rgba(0,113,67,.1); color: var(--accent-primary); font-size: 9px; font-weight: 700; line-height: 1; text-align: center; }
|
||||
.station-navigation-shell .station-list-partner-tag.is-placeholder { visibility: hidden; }
|
||||
.station-navigation-shell .rank-empty { display: grid; min-height: 160px; place-items: center; color: var(--text-muted); font-size: 12px; text-align: center; }
|
||||
.station-navigation-shell .map-detail-card .detail-grid { margin-bottom: 0; }
|
||||
@media (max-width: 760px) {
|
||||
.station-navigation-shell .header-kpi-group { display: flex; }
|
||||
.station-navigation-shell .filter-chip-rail { max-width: 124px; overflow: hidden; }
|
||||
.station-navigation-shell .map-filter-chip { padding: 0 7px; font-size: 9px; }
|
||||
/* Mobile is navigation-first: keep the first screen focused on map,
|
||||
search and the next action, rather than dashboard chrome. */
|
||||
.station-navigation-shell .liquid-header {
|
||||
position: relative;
|
||||
padding: 9px 10px 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .header-left {
|
||||
width: calc(100% - 78px);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .brand-block { flex-basis: 118px; min-width: 96px; }
|
||||
.station-navigation-shell .brand-logo-svg { max-width: 118px; }
|
||||
.station-navigation-shell .brand-divider { height: 18px; }
|
||||
.station-navigation-shell .cockpit-title { font-size: 14px; }
|
||||
|
||||
.station-navigation-shell .header-controls {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: auto;
|
||||
order: initial;
|
||||
}
|
||||
|
||||
.station-navigation-shell .theme-switcher-bw { padding: 2px; border-radius: 9px; }
|
||||
.station-navigation-shell .theme-switcher-bw .bw-btn { display: grid; width: 31px; min-height: 28px; padding: 4px; place-items: center; }
|
||||
.station-navigation-shell .theme-switcher-bw .bw-btn span { display: none; }
|
||||
.station-navigation-shell .header-kpi-group,
|
||||
.station-navigation-shell .station-status-card { display: none; }
|
||||
.station-navigation-shell .station-summary-strip { display: block; padding: 9px 11px; border-radius: 14px; flex: 0 0 auto; }
|
||||
.station-navigation-shell .station-summary-strip .panel-header { margin-bottom: 7px; padding-bottom: 5px; }
|
||||
.station-navigation-shell .station-summary-strip .status-glass-grid3 { gap: 5px; margin-bottom: 6px; }
|
||||
.station-navigation-shell .station-summary-strip .glass-cell { padding: 6px; }
|
||||
|
||||
.station-navigation-shell .map-spatial-container {
|
||||
height: clamp(430px, 58dvh, 540px);
|
||||
min-height: clamp(430px, 58dvh, 540px);
|
||||
}
|
||||
|
||||
.station-navigation-shell .map-top-bar { display: none; }
|
||||
|
||||
.station-navigation-shell .map-explore-toolbar {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .locate-btn {
|
||||
flex-basis: 36px;
|
||||
width: 36px;
|
||||
min-height: 36px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .entity-search-field {
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.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 .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 .explore-suggestions {
|
||||
width: calc(100vw - 32px);
|
||||
max-height: min(300px, calc(100dvh - 248px));
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .map-action-controls {
|
||||
right: 8px;
|
||||
bottom: 31px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .map-action-controls .glass-btn:nth-child(1),
|
||||
.station-navigation-shell .map-action-controls .glass-btn:nth-child(2) { display: none; }
|
||||
|
||||
.station-navigation-shell .map-action-controls .glass-btn {
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .map-detail-card {
|
||||
bottom: 46px;
|
||||
max-height: min(42dvh, 300px);
|
||||
padding: 12px;
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .detail-title-wrap h2 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.station-navigation-shell .detail-navigate-btn,
|
||||
.station-navigation-shell .detail-close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
.station-navigation-shell .detail-navigate-btn svg { width: 15px; height: 15px; }
|
||||
.station-navigation-shell .sidebar-operations { gap: 8px; }
|
||||
.station-navigation-shell .sidebar-card { padding: 10px 11px; border-radius: 14px; }
|
||||
.station-navigation-shell .status-glass-grid3 { gap: 5px; margin-bottom: 7px; }
|
||||
.station-navigation-shell .glass-cell { padding: 6px; }
|
||||
.station-navigation-shell .rank-glass-row { min-height: 42px; padding: 7px 8px; }
|
||||
}
|
||||
|
||||
@media (max-width: 390px) {
|
||||
.station-navigation-shell .header-left { width: calc(100% - 76px); }
|
||||
.station-navigation-shell .brand-block { flex-basis: 104px; min-width: 88px; }
|
||||
.station-navigation-shell .brand-logo-svg { max-width: 104px; }
|
||||
.station-navigation-shell .cockpit-title { font-size: 13px; }
|
||||
.station-navigation-shell .brand-divider { margin-left: -2px; }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const styles = fs.readFileSync(new URL('../styles.css', import.meta.url), 'utf8');
|
||||
const sandbox = {
|
||||
console,
|
||||
document: { addEventListener() {}, getElementById() { return null; } },
|
||||
@@ -48,7 +49,7 @@ assert.equal(sandbox.stationNodeCount, 3);
|
||||
assert.equal(sandbox.locationOptionCount, 8);
|
||||
vm.runInNewContext(`
|
||||
globalThis.inferredDistrict = stationDistrictName({ province: '浙江省', city: '嘉兴市', address: '嘉兴市海盐县海盐经济开发区' });
|
||||
state.locationFilter = { province: '', city: '', district: '' };
|
||||
state.locationFilters = [];
|
||||
state.query = '广州';
|
||||
globalThis.queryMatches = stationMatchesQuery({ name: '广州合作站' });
|
||||
globalThis.queryMisses = stationMatchesQuery({ name: '杭州合作站' });
|
||||
@@ -56,18 +57,47 @@ vm.runInNewContext(`
|
||||
assert.equal(sandbox.inferredDistrict, '海盐县');
|
||||
assert.equal(sandbox.queryMatches, true);
|
||||
assert.equal(sandbox.queryMisses, false);
|
||||
vm.runInNewContext(`
|
||||
state.query = '';
|
||||
state.locationFilters = [
|
||||
{ level: 'city', province: '广东省', city: '广州市', district: '', label: '广州' },
|
||||
{ level: 'city', province: '浙江省', city: '嘉兴市', district: '', label: '嘉兴' }
|
||||
];
|
||||
globalThis.multiLocationMatches = [
|
||||
stationMatchesLocation({ province: '广东省', city: '广州市', district: '白云区' }),
|
||||
stationMatchesLocation({ province: '浙江省', city: '嘉兴市', district: '海盐县' }),
|
||||
stationMatchesLocation({ province: '广东省', city: '深圳市', district: '南山区' })
|
||||
];
|
||||
`, sandbox);
|
||||
assert.equal(sandbox.multiLocationMatches.join(','), 'true,true,false');
|
||||
assert.ok(Math.abs(sandbox.distanceKm([113, 23], [114, 23]) - 102.4) < 1);
|
||||
assert.match(source, /zoom < 11\) return 'city'/);
|
||||
assert.match(source, /state\.userLocation && left\.kind === 'station'/);
|
||||
assert.doesNotMatch(source, /rankSecondaryTab|setRank\(/);
|
||||
assert.match(source, /stationSearchSuggestions/);
|
||||
assert.match(source, /handleStationSearchInput/);
|
||||
assert.match(source, /locationFilters/);
|
||||
assert.match(source, /renderLocationFilterChips/);
|
||||
assert.match(source, /kpiTotal|kpiPartner/);
|
||||
const index = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||
assert.match(index, /station-summary-strip/);
|
||||
assert.match(index, /station-status-card/);
|
||||
assert.equal((index.match(/id="statusTotal"/g) || []).length, 1);
|
||||
assert.equal((index.match(/id="mobileStatusTotal"/g) || []).length, 1);
|
||||
assert.match(source, /window\.setTimeout\(\(\) => \{ state\.query = value/);
|
||||
assert.match(source, /focusSearchResults\(\)/);
|
||||
assert.match(source, /visibleOnly && !normalize\(state\.query\)/);
|
||||
assert.match(source, /station-list-partner-tag/);
|
||||
assert.match(source, /station-list-partner-tag\$\{node\.cooperative \? '' : ' is-placeholder'\}/);
|
||||
assert.match(source, /node\.kind === 'station' \? '' : `\$\{formatNumber\(node\.count\)\} 座`/);
|
||||
assert.match(source, /node\.cooperative \? 'H₂ · 合作' : ''/);
|
||||
assert.doesNotMatch(source, /在线 \$\{formatNumber\(node\.online\)\}/);
|
||||
assert.doesNotMatch(source, /monthlyHydrogenKg|totalHydrogenKg|vehicle/);
|
||||
assert.match(source, /uri\.amap\.com\/navigation/);
|
||||
assert.equal((source.match(/function navigateToStation\(/g) || []).length, 1);
|
||||
assert.match(styles, /station-summary-strip/);
|
||||
assert.match(styles, /header-kpi-group,\n \.station-navigation-shell \.station-status-card/);
|
||||
assert.match(styles, /theme-switcher-bw \.bw-btn span \{ display: none; \}/);
|
||||
assert.match(styles, /height: clamp\(430px, 58dvh, 540px\)/);
|
||||
assert.match(styles, /map-action-controls \.glass-btn:nth-child\(2\)/);
|
||||
console.log('station navigation app tests: ok');
|
||||
|
||||
@@ -30,6 +30,9 @@ class StationNavigationTest(unittest.TestCase):
|
||||
def test_static_assets_are_fingerprinted(self):
|
||||
self.assertRegex(server.STATIC_ASSET_VERSION, r"^[0-9a-f]{16}$")
|
||||
|
||||
def test_static_assets_resolve_from_the_release_directory(self):
|
||||
self.assertIn('directory=str(ROOT)', Path(__file__).parents[1].joinpath('server.py').read_text(encoding='utf-8'))
|
||||
|
||||
def test_default_cache_window_is_two_minutes(self):
|
||||
self.assertEqual(server.STATION_CACHE_SECONDS, 120)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user