Loading docs/.vuepress/client.js +2 −2 Changes for docs/.vuepress/client.js: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ import TalksTodayNav from './components/TalksTodayNav.vue' import { BusHomeV2 } from './components/bus-v2/index.mjs' import BusRouteV2 from './components/bus-v2/BusRouteV2.vue' import BusStopV2 from './components/bus-v2/BusStopV2.vue' import BusSchedulesV2 from './components/bus-v2/BusSchedulesV2.vue' import BusRefreshButton from './components/bus-v2/BusRefreshButton.vue' import { busPageForPath, reportBusVisit } from './components/bus-v2/analytics.mjs' import { setWorkerUrl } from 'maplibre-gl' import maplibreWorkerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url' Loading Loading @@ -45,7 +45,7 @@ export default defineClientConfig({ app.component("BusHomeV2", BusHomeV2) app.component("BusRouteV2", BusRouteV2) app.component("BusStopV2", BusStopV2) app.component("BusSchedulesV2", BusSchedulesV2) app.component("BusRefreshButton", BusRefreshButton) // 含有echart的组件,注意需要用non-ssr模式 app.component("BusChartVue", BusChartVue) Loading docs/.vuepress/components/bus-v2/BusHomeV2.vue +17 −16 Changes for docs/.vuepress/components/bus-v2/BusHomeV2.vue: 17 added lines, 16 removed lines. Original line number Diff line number Diff line <template> <main class="bus-home" :lang="busLanguage === 'zh' ? 'zh-CN' : 'en'"> <BusRefreshButton :remaining="refreshRemaining" :label="busLanguage === 'zh' ? '立即刷新' : 'Refresh now'" @refresh="refresh" /> <header class="bus-home__header"> <div> <h1>{{ busLanguage === 'zh' ? '校园巴士' : 'Campus bus' }}</h1> <p>{{ busLanguage === 'zh' ? '实时到站与出行信息。所有到站时间均为预测,仅供参考。' : 'Live arrivals and travel information. All arrival times are estimates and for reference only.' }}</p> </div> <div><button class="text-button" type="button" :aria-label="busLanguage === 'zh' ? '立即刷新' : 'Refresh now'" @click="refresh">🔄{{ refreshRemaining }}s</button><button class="text-button" type="button" @click="setBusLanguage(busLanguage === 'zh' ? 'en' : 'zh')">{{ busText('language') }}</button></div> <div><button class="text-button" type="button" @click="setBusLanguage(busLanguage === 'zh' ? 'en' : 'zh')">{{ busText('language') }}</button></div> </header> <section class="panel search" :aria-label="busText('search')"> Loading Loading @@ -47,9 +48,9 @@ <BusVehicleLegendV2 v-if="favoriteStopCards.length" :language="busLanguage" /> <article v-for="stop in favoriteStopCards" :key="stop.id" class="nearby-stop"> <div class="nearby-stop__head"><button type="button" class="link-title" @click="openStop(stop.id)">{{ displayStopName(stop, busLanguage) }}</button><span v-if="stop.distance != null">{{ formatDistance(stop.distance) }}</span></div> <p v-if="favoriteArrivals[stop.id]?.loading" class="muted">{{ busText('loading') }}</p> <p v-else-if="favoriteArrivals[stop.id]?.error" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"><li v-for="arrival in favoriteArrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul> <p v-if="favoriteArrivals[stop.id]?.loading && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="favoriteArrivals[stop.id]?.error && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul> </article> <p v-if="!favoriteRoutes.length && !favoriteStops.length" class="muted">{{ busText('noFavorites') }}</p> </section> Loading @@ -68,10 +69,10 @@ <button type="button" class="link-title" @click="openStop(stop.id)">{{ displayStopName(stop, busLanguage) }}</button> <span>{{ formatDistance(stop.distance) }}</span> </div> <p v-if="arrivals[stop.id]?.loading" class="muted">{{ busText('loading') }}</p> <p v-else-if="arrivals[stop.id]?.error" class="muted">{{ busText('unavailable') }}</p> <p v-if="arrivals[stop.id]?.loading && !arrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="arrivals[stop.id]?.error && !arrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"> <li v-for="arrival in arrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`"> <li v-for="(arrival, index) in arrivals[stop.id]?.items" :key="index"> <i :style="{ background: arrival.route_color || '#2878c8' }" /> <a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a> <span>{{ arrivalText(arrival) }}</span> Loading @@ -87,10 +88,9 @@ <BusVehiclesV2 :routes="routes" :stops="stops" :vehicles="vehicles" :loading="vehiclesLoading" :error="vehiclesError" :show-all-vehicles="showAllVehicles" @update:show-all-vehicles="setShowAllVehicles" /> </section> <nav class="feature-links" :aria-label="busLanguage === 'zh' ? '巴士功能' : 'Bus features'"> <a :href="schedulesHref"><span>{{ busText('schedules') }}</span></a> <a :href="filesHref"><span>{{ busText('files') }}</span></a> </nav> <section id="schedules" class="panel schedules-card"><BusSchedulesV2 /></section> <nav class="feature-links" :aria-label="busLanguage === 'zh' ? '巴士功能' : 'Bus features'"><a :href="filesHref"><span>{{ busText('files') }}</span></a></nav> </template> </main> </template> Loading @@ -105,11 +105,12 @@ import { renderNoticeMarkdown } from './markdown.mjs' import { isImportantNotice, loadNoticeOpenStates, noticeIsOpen, saveNoticeOpenState } from './notice-state.mjs' import BusVehicleLegendV2 from './BusVehicleLegendV2.vue' import BusVehiclesV2 from './BusVehiclesV2.vue' import BusSchedulesV2 from './BusSchedulesV2.vue' import BusRefreshButton from './BusRefreshButton.vue' const props = defineProps({ routeHref: { type: String, default: '/transport/bustimer_v2_route.html?id=' }, stopHref: { type: String, default: '/transport/bustimer_v2_stop.html?id=' }, schedulesHref: { type: String, default: '/transport/bustimer_v2_schedules.html' }, filesHref: { type: String, default: '/transport/bustimer_v2_files.html' }, }) Loading @@ -131,7 +132,7 @@ const locationErrorKey = ref('') const nearbyStops = ref([]) const location = ref(null) const allNearbyStops = ref(false) const refreshRemaining = ref(30) const refreshRemaining = ref(10) const locationUpdatedAt = ref(0) let nearbyCandidates = [] let refreshTimer Loading Loading @@ -190,7 +191,7 @@ function arrivalText(arrival) { async function loadArrivals(stopList, target = arrivals) { const results = await Promise.all(stopList.map(async (stop) => { target.value = { ...target.value, [stop.id]: { loading: true, items: [] } } target.value = { ...target.value, [stop.id]: { ...target.value[stop.id], loading: true, items: target.value[stop.id]?.items || [] } } try { const result = await busApi.arrivals(stop.id) const rawItems = result.arrivals || [] Loading @@ -198,7 +199,7 @@ async function loadArrivals(stopList, target = arrivals) { target.value = { ...target.value, [stop.id]: { loading: false, items } } return { stop, hidden: rawItems.length > 0 && !items.length } } catch { target.value = { ...target.value, [stop.id]: { loading: false, error: true, items: [] } } target.value = { ...target.value, [stop.id]: { loading: false, error: true, items: target.value[stop.id]?.items || [] } } return { stop, hidden: false } } })) Loading @@ -208,7 +209,7 @@ async function loadArrivals(stopList, target = arrivals) { async function refresh() { const position = typeof window === 'undefined' ? null : [window.scrollX, window.scrollY] refreshRemaining.value = 30 refreshRemaining.value = 10 await load(true) if (nearbyCandidates.length) await loadArrivals(nearbyCandidates) if (position) window.scrollTo(position[0], position[1]) Loading docs/.vuepress/components/bus-v2/BusMapV2.vue +11 −3 Changes for docs/.vuepress/components/bus-v2/BusMapV2.vue: 11 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ <p v-if="mapError" class="bus-map__message" role="alert">{{ mapError }}</p> <p v-else-if="loading" class="bus-map__message">{{ language === 'zh' ? '正在加载地图…' : 'Loading map…' }}</p> <BusVehicleLegendV2 class="bus-map__legend" :language="language" /> <label class="bus-map__overlap"><input v-model="avoidOverlap" type="checkbox" @change="refreshVehicleMarkers" /> {{ language === 'zh' ? '避免重叠' : 'Avoid overlap' }}</label> </section> </template> Loading @@ -17,6 +18,7 @@ import { displayName, displayStopName, vehicleBearingAt } from './core.mjs' const LIGHT_STYLE = 'https://bus.sustcra.com/static/protomaps/pmtiles-style/pmtiles-light.json' const DARK_STYLE = 'https://bus.sustcra.com/static/protomaps/pmtiles-style/pmtiles-dark.json' const CAMPUS_CENTER = [113.99373, 22.60308] const CACHE_BUST = Date.now() let protocolUsers = 0 let protocol Loading @@ -35,6 +37,7 @@ const mapElement = ref(null) const selectedVehicle = ref(null) const loading = ref(true) const mapError = ref('') const avoidOverlap = ref(true) let map let maplibregl let themeChangeHandler Loading @@ -53,7 +56,7 @@ const routeFor = (id) => props.routes.find((route) => route.id === id) const darkTheme = () => document.documentElement.getAttribute('data-theme') === 'dark' const neutralRouteColor = () => darkTheme() ? '#aaa' : '#666' const neutralStopColor = () => darkTheme() ? '#ccc' : '#444' const styleUrl = () => props.styleUrl || (darkTheme() ? DARK_STYLE : LIGHT_STYLE) const styleUrl = () => `${props.styleUrl || (darkTheme() ? DARK_STYLE : LIGHT_STYLE)}?v=${CACHE_BUST}` const sourceData = (features) => ({ type: 'FeatureCollection', features }) function vehicleBearing(vehicle) { Loading Loading @@ -133,7 +136,10 @@ function updateVehicleMarker(record, vehicle) { record.element.title = displayName(vehicle, props.language) || vehicle.id record.element.setAttribute('aria-label', record.element.title) record.element.style.setProperty('--route-color', route?.color || '#2878c8') record.element.style.setProperty('--bearing', `${vehicleBearing(vehicle)}deg`) const bearing = vehicleBearing(vehicle) record.element.style.setProperty('--bearing', `${bearing}deg`) const rightBearing = (bearing + 90) * Math.PI / 180 record.marker?.setOffset(avoidOverlap.value ? [Math.sin(rightBearing) * 10, -Math.cos(rightBearing) * 10] : [0, 0]) } function moveVehicleMarker(record, longitude, latitude) { Loading Loading @@ -165,6 +171,7 @@ function createVehicleMarker(vehicle) { record.element = element updateVehicleMarker(record, vehicle) record.marker = new maplibregl.Marker({ element: markerElement, anchor: 'center' }).setLngLat([+vehicle.longitude, +vehicle.latitude]).addTo(map) updateVehicleMarker(record, vehicle) return record } Loading Loading @@ -230,7 +237,7 @@ function createInteractionLockControl() { onAdd(currentMap) { const container = document.createElement('div') const button = document.createElement('button') let enabled = false let enabled = true container.className = 'maplibregl-ctrl maplibregl-ctrl-group' const update = () => { ;['dragPan', 'boxZoom', 'doubleClickZoom', 'touchZoomRotate', 'scrollZoom'].forEach((key) => currentMap[key]?.[enabled ? 'enable' : 'disable']()) Loading Loading @@ -338,6 +345,7 @@ defineExpose({ refresh, refreshVehicleMarkers }) .bus-map__canvas { width: 100%; height: 28rem; } .bus-map__message { position: absolute; top: .75rem; left: .75rem; z-index: 1; margin: 0; padding: .45rem .65rem; border-radius: .35rem; background: rgba(255, 255, 255, .9); color: #526172; } .bus-map__legend { position: absolute; z-index: 1; bottom: .75rem; left: .75rem; margin: 0; padding: .4rem .55rem; border-radius: .35rem; background: color-mix(in srgb, var(--bus-v2-bg, #fff) 90%, transparent); } .bus-map__overlap { position: absolute; z-index: 1; top: .75rem; left: 3rem; padding: .35rem .5rem; border-radius: .35rem; background: color-mix(in srgb, var(--bus-v2-bg, #fff) 90%, transparent); color: var(--bus-v2-text, #333); font-size: .8rem; } .bus-map :deep(.bus-map__vehicle-marker) { display: block; width: 1.6rem; min-width: 1.6rem; max-width: 1.6rem; height: 1.6rem; min-height: 1.6rem; max-height: 1.6rem; line-height: 0; } .bus-map :deep(.bus-map__vehicle) { box-sizing: border-box; display: block; position: relative; width: 1.6rem; min-width: 1.6rem; max-width: 1.6rem; height: 1.6rem; min-height: 1.6rem; max-height: 1.6rem; aspect-ratio: 1; border: 2px solid #fff; border-radius: 50%; padding: 1px; background: var(--route-color); cursor: pointer; } .bus-map :deep(.bus-map__vehicle img) { position: absolute; inset: 0; width: 80%; height: 80%; margin: auto; object-fit: contain; } Loading docs/.vuepress/components/bus-v2/BusRefreshButton.vue 0 → 100644 +17 −0 Changes for docs/.vuepress/components/bus-v2/BusRefreshButton.vue: 17 added lines, 0 removed lines. Original line number Diff line number Diff line <template> <button class="bus-refresh-button" type="button" :aria-label="label" :title="label" @click="handleClick"> <svg viewBox="0 0 40 40" aria-hidden="true" class="bus-refresh-button__progress"><circle class="track" cx="20" cy="20" r="18" /><circle class="value" cx="20" cy="20" r="18" :style="{ strokeDashoffset: 113.1 * (1 - remaining / total) }" /></svg> <svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg> </button> </template> <script setup> const props = defineProps({ remaining: { type: Number, default: 10 }, total: { type: Number, default: 10 }, label: { type: String, default: 'Refresh' }, reload: Boolean }) const emit = defineEmits(['refresh']) function handleClick() { if (props.reload && typeof window !== 'undefined') window.location.reload(); else emit('refresh') } </script> <style scoped> .bus-refresh-button { position: fixed; right: 1rem; bottom: calc(4rem + 48px + .75rem); z-index: 20; display: grid; place-items: center; width: 3rem; height: 3rem; padding: 0; border: 0; border-radius: 50%; background: var(--bus-v2-bg, #fff); color: var(--bus-v2-link, #2878c8); box-shadow: 0 2px 10px #0003; cursor: pointer; } .bus-refresh-button__progress { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; } circle { fill: none; stroke-width: 2; } .track { stroke: var(--bus-v2-border, #d9e2ec); } .value { stroke: currentColor; stroke-dasharray: 113.1; transition: stroke-dashoffset .2s linear; } @media (max-width: 959px) { .bus-refresh-button { right: 1rem; bottom: calc(4rem + 38.4px + .75rem + env(safe-area-inset-bottom)); } } </style> docs/.vuepress/components/bus-v2/BusRouteV2.vue +12 −11 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
docs/.vuepress/client.js +2 −2 Changes for docs/.vuepress/client.js: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ import TalksTodayNav from './components/TalksTodayNav.vue' import { BusHomeV2 } from './components/bus-v2/index.mjs' import BusRouteV2 from './components/bus-v2/BusRouteV2.vue' import BusStopV2 from './components/bus-v2/BusStopV2.vue' import BusSchedulesV2 from './components/bus-v2/BusSchedulesV2.vue' import BusRefreshButton from './components/bus-v2/BusRefreshButton.vue' import { busPageForPath, reportBusVisit } from './components/bus-v2/analytics.mjs' import { setWorkerUrl } from 'maplibre-gl' import maplibreWorkerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url' Loading Loading @@ -45,7 +45,7 @@ export default defineClientConfig({ app.component("BusHomeV2", BusHomeV2) app.component("BusRouteV2", BusRouteV2) app.component("BusStopV2", BusStopV2) app.component("BusSchedulesV2", BusSchedulesV2) app.component("BusRefreshButton", BusRefreshButton) // 含有echart的组件,注意需要用non-ssr模式 app.component("BusChartVue", BusChartVue) Loading
docs/.vuepress/components/bus-v2/BusHomeV2.vue +17 −16 Changes for docs/.vuepress/components/bus-v2/BusHomeV2.vue: 17 added lines, 16 removed lines. Original line number Diff line number Diff line <template> <main class="bus-home" :lang="busLanguage === 'zh' ? 'zh-CN' : 'en'"> <BusRefreshButton :remaining="refreshRemaining" :label="busLanguage === 'zh' ? '立即刷新' : 'Refresh now'" @refresh="refresh" /> <header class="bus-home__header"> <div> <h1>{{ busLanguage === 'zh' ? '校园巴士' : 'Campus bus' }}</h1> <p>{{ busLanguage === 'zh' ? '实时到站与出行信息。所有到站时间均为预测,仅供参考。' : 'Live arrivals and travel information. All arrival times are estimates and for reference only.' }}</p> </div> <div><button class="text-button" type="button" :aria-label="busLanguage === 'zh' ? '立即刷新' : 'Refresh now'" @click="refresh">🔄{{ refreshRemaining }}s</button><button class="text-button" type="button" @click="setBusLanguage(busLanguage === 'zh' ? 'en' : 'zh')">{{ busText('language') }}</button></div> <div><button class="text-button" type="button" @click="setBusLanguage(busLanguage === 'zh' ? 'en' : 'zh')">{{ busText('language') }}</button></div> </header> <section class="panel search" :aria-label="busText('search')"> Loading Loading @@ -47,9 +48,9 @@ <BusVehicleLegendV2 v-if="favoriteStopCards.length" :language="busLanguage" /> <article v-for="stop in favoriteStopCards" :key="stop.id" class="nearby-stop"> <div class="nearby-stop__head"><button type="button" class="link-title" @click="openStop(stop.id)">{{ displayStopName(stop, busLanguage) }}</button><span v-if="stop.distance != null">{{ formatDistance(stop.distance) }}</span></div> <p v-if="favoriteArrivals[stop.id]?.loading" class="muted">{{ busText('loading') }}</p> <p v-else-if="favoriteArrivals[stop.id]?.error" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"><li v-for="arrival in favoriteArrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul> <p v-if="favoriteArrivals[stop.id]?.loading && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="favoriteArrivals[stop.id]?.error && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul> </article> <p v-if="!favoriteRoutes.length && !favoriteStops.length" class="muted">{{ busText('noFavorites') }}</p> </section> Loading @@ -68,10 +69,10 @@ <button type="button" class="link-title" @click="openStop(stop.id)">{{ displayStopName(stop, busLanguage) }}</button> <span>{{ formatDistance(stop.distance) }}</span> </div> <p v-if="arrivals[stop.id]?.loading" class="muted">{{ busText('loading') }}</p> <p v-else-if="arrivals[stop.id]?.error" class="muted">{{ busText('unavailable') }}</p> <p v-if="arrivals[stop.id]?.loading && !arrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="arrivals[stop.id]?.error && !arrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"> <li v-for="arrival in arrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`"> <li v-for="(arrival, index) in arrivals[stop.id]?.items" :key="index"> <i :style="{ background: arrival.route_color || '#2878c8' }" /> <a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a> <span>{{ arrivalText(arrival) }}</span> Loading @@ -87,10 +88,9 @@ <BusVehiclesV2 :routes="routes" :stops="stops" :vehicles="vehicles" :loading="vehiclesLoading" :error="vehiclesError" :show-all-vehicles="showAllVehicles" @update:show-all-vehicles="setShowAllVehicles" /> </section> <nav class="feature-links" :aria-label="busLanguage === 'zh' ? '巴士功能' : 'Bus features'"> <a :href="schedulesHref"><span>{{ busText('schedules') }}</span></a> <a :href="filesHref"><span>{{ busText('files') }}</span></a> </nav> <section id="schedules" class="panel schedules-card"><BusSchedulesV2 /></section> <nav class="feature-links" :aria-label="busLanguage === 'zh' ? '巴士功能' : 'Bus features'"><a :href="filesHref"><span>{{ busText('files') }}</span></a></nav> </template> </main> </template> Loading @@ -105,11 +105,12 @@ import { renderNoticeMarkdown } from './markdown.mjs' import { isImportantNotice, loadNoticeOpenStates, noticeIsOpen, saveNoticeOpenState } from './notice-state.mjs' import BusVehicleLegendV2 from './BusVehicleLegendV2.vue' import BusVehiclesV2 from './BusVehiclesV2.vue' import BusSchedulesV2 from './BusSchedulesV2.vue' import BusRefreshButton from './BusRefreshButton.vue' const props = defineProps({ routeHref: { type: String, default: '/transport/bustimer_v2_route.html?id=' }, stopHref: { type: String, default: '/transport/bustimer_v2_stop.html?id=' }, schedulesHref: { type: String, default: '/transport/bustimer_v2_schedules.html' }, filesHref: { type: String, default: '/transport/bustimer_v2_files.html' }, }) Loading @@ -131,7 +132,7 @@ const locationErrorKey = ref('') const nearbyStops = ref([]) const location = ref(null) const allNearbyStops = ref(false) const refreshRemaining = ref(30) const refreshRemaining = ref(10) const locationUpdatedAt = ref(0) let nearbyCandidates = [] let refreshTimer Loading Loading @@ -190,7 +191,7 @@ function arrivalText(arrival) { async function loadArrivals(stopList, target = arrivals) { const results = await Promise.all(stopList.map(async (stop) => { target.value = { ...target.value, [stop.id]: { loading: true, items: [] } } target.value = { ...target.value, [stop.id]: { ...target.value[stop.id], loading: true, items: target.value[stop.id]?.items || [] } } try { const result = await busApi.arrivals(stop.id) const rawItems = result.arrivals || [] Loading @@ -198,7 +199,7 @@ async function loadArrivals(stopList, target = arrivals) { target.value = { ...target.value, [stop.id]: { loading: false, items } } return { stop, hidden: rawItems.length > 0 && !items.length } } catch { target.value = { ...target.value, [stop.id]: { loading: false, error: true, items: [] } } target.value = { ...target.value, [stop.id]: { loading: false, error: true, items: target.value[stop.id]?.items || [] } } return { stop, hidden: false } } })) Loading @@ -208,7 +209,7 @@ async function loadArrivals(stopList, target = arrivals) { async function refresh() { const position = typeof window === 'undefined' ? null : [window.scrollX, window.scrollY] refreshRemaining.value = 30 refreshRemaining.value = 10 await load(true) if (nearbyCandidates.length) await loadArrivals(nearbyCandidates) if (position) window.scrollTo(position[0], position[1]) Loading
docs/.vuepress/components/bus-v2/BusMapV2.vue +11 −3 Changes for docs/.vuepress/components/bus-v2/BusMapV2.vue: 11 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ <p v-if="mapError" class="bus-map__message" role="alert">{{ mapError }}</p> <p v-else-if="loading" class="bus-map__message">{{ language === 'zh' ? '正在加载地图…' : 'Loading map…' }}</p> <BusVehicleLegendV2 class="bus-map__legend" :language="language" /> <label class="bus-map__overlap"><input v-model="avoidOverlap" type="checkbox" @change="refreshVehicleMarkers" /> {{ language === 'zh' ? '避免重叠' : 'Avoid overlap' }}</label> </section> </template> Loading @@ -17,6 +18,7 @@ import { displayName, displayStopName, vehicleBearingAt } from './core.mjs' const LIGHT_STYLE = 'https://bus.sustcra.com/static/protomaps/pmtiles-style/pmtiles-light.json' const DARK_STYLE = 'https://bus.sustcra.com/static/protomaps/pmtiles-style/pmtiles-dark.json' const CAMPUS_CENTER = [113.99373, 22.60308] const CACHE_BUST = Date.now() let protocolUsers = 0 let protocol Loading @@ -35,6 +37,7 @@ const mapElement = ref(null) const selectedVehicle = ref(null) const loading = ref(true) const mapError = ref('') const avoidOverlap = ref(true) let map let maplibregl let themeChangeHandler Loading @@ -53,7 +56,7 @@ const routeFor = (id) => props.routes.find((route) => route.id === id) const darkTheme = () => document.documentElement.getAttribute('data-theme') === 'dark' const neutralRouteColor = () => darkTheme() ? '#aaa' : '#666' const neutralStopColor = () => darkTheme() ? '#ccc' : '#444' const styleUrl = () => props.styleUrl || (darkTheme() ? DARK_STYLE : LIGHT_STYLE) const styleUrl = () => `${props.styleUrl || (darkTheme() ? DARK_STYLE : LIGHT_STYLE)}?v=${CACHE_BUST}` const sourceData = (features) => ({ type: 'FeatureCollection', features }) function vehicleBearing(vehicle) { Loading Loading @@ -133,7 +136,10 @@ function updateVehicleMarker(record, vehicle) { record.element.title = displayName(vehicle, props.language) || vehicle.id record.element.setAttribute('aria-label', record.element.title) record.element.style.setProperty('--route-color', route?.color || '#2878c8') record.element.style.setProperty('--bearing', `${vehicleBearing(vehicle)}deg`) const bearing = vehicleBearing(vehicle) record.element.style.setProperty('--bearing', `${bearing}deg`) const rightBearing = (bearing + 90) * Math.PI / 180 record.marker?.setOffset(avoidOverlap.value ? [Math.sin(rightBearing) * 10, -Math.cos(rightBearing) * 10] : [0, 0]) } function moveVehicleMarker(record, longitude, latitude) { Loading Loading @@ -165,6 +171,7 @@ function createVehicleMarker(vehicle) { record.element = element updateVehicleMarker(record, vehicle) record.marker = new maplibregl.Marker({ element: markerElement, anchor: 'center' }).setLngLat([+vehicle.longitude, +vehicle.latitude]).addTo(map) updateVehicleMarker(record, vehicle) return record } Loading Loading @@ -230,7 +237,7 @@ function createInteractionLockControl() { onAdd(currentMap) { const container = document.createElement('div') const button = document.createElement('button') let enabled = false let enabled = true container.className = 'maplibregl-ctrl maplibregl-ctrl-group' const update = () => { ;['dragPan', 'boxZoom', 'doubleClickZoom', 'touchZoomRotate', 'scrollZoom'].forEach((key) => currentMap[key]?.[enabled ? 'enable' : 'disable']()) Loading Loading @@ -338,6 +345,7 @@ defineExpose({ refresh, refreshVehicleMarkers }) .bus-map__canvas { width: 100%; height: 28rem; } .bus-map__message { position: absolute; top: .75rem; left: .75rem; z-index: 1; margin: 0; padding: .45rem .65rem; border-radius: .35rem; background: rgba(255, 255, 255, .9); color: #526172; } .bus-map__legend { position: absolute; z-index: 1; bottom: .75rem; left: .75rem; margin: 0; padding: .4rem .55rem; border-radius: .35rem; background: color-mix(in srgb, var(--bus-v2-bg, #fff) 90%, transparent); } .bus-map__overlap { position: absolute; z-index: 1; top: .75rem; left: 3rem; padding: .35rem .5rem; border-radius: .35rem; background: color-mix(in srgb, var(--bus-v2-bg, #fff) 90%, transparent); color: var(--bus-v2-text, #333); font-size: .8rem; } .bus-map :deep(.bus-map__vehicle-marker) { display: block; width: 1.6rem; min-width: 1.6rem; max-width: 1.6rem; height: 1.6rem; min-height: 1.6rem; max-height: 1.6rem; line-height: 0; } .bus-map :deep(.bus-map__vehicle) { box-sizing: border-box; display: block; position: relative; width: 1.6rem; min-width: 1.6rem; max-width: 1.6rem; height: 1.6rem; min-height: 1.6rem; max-height: 1.6rem; aspect-ratio: 1; border: 2px solid #fff; border-radius: 50%; padding: 1px; background: var(--route-color); cursor: pointer; } .bus-map :deep(.bus-map__vehicle img) { position: absolute; inset: 0; width: 80%; height: 80%; margin: auto; object-fit: contain; } Loading
docs/.vuepress/components/bus-v2/BusRefreshButton.vue 0 → 100644 +17 −0 Changes for docs/.vuepress/components/bus-v2/BusRefreshButton.vue: 17 added lines, 0 removed lines. Original line number Diff line number Diff line <template> <button class="bus-refresh-button" type="button" :aria-label="label" :title="label" @click="handleClick"> <svg viewBox="0 0 40 40" aria-hidden="true" class="bus-refresh-button__progress"><circle class="track" cx="20" cy="20" r="18" /><circle class="value" cx="20" cy="20" r="18" :style="{ strokeDashoffset: 113.1 * (1 - remaining / total) }" /></svg> <svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg> </button> </template> <script setup> const props = defineProps({ remaining: { type: Number, default: 10 }, total: { type: Number, default: 10 }, label: { type: String, default: 'Refresh' }, reload: Boolean }) const emit = defineEmits(['refresh']) function handleClick() { if (props.reload && typeof window !== 'undefined') window.location.reload(); else emit('refresh') } </script> <style scoped> .bus-refresh-button { position: fixed; right: 1rem; bottom: calc(4rem + 48px + .75rem); z-index: 20; display: grid; place-items: center; width: 3rem; height: 3rem; padding: 0; border: 0; border-radius: 50%; background: var(--bus-v2-bg, #fff); color: var(--bus-v2-link, #2878c8); box-shadow: 0 2px 10px #0003; cursor: pointer; } .bus-refresh-button__progress { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; } circle { fill: none; stroke-width: 2; } .track { stroke: var(--bus-v2-border, #d9e2ec); } .value { stroke: currentColor; stroke-dasharray: 113.1; transition: stroke-dashoffset .2s linear; } @media (max-width: 959px) { .bus-refresh-button { right: 1rem; bottom: calc(4rem + 38.4px + .75rem + env(safe-area-inset-bottom)); } } </style>
docs/.vuepress/components/bus-v2/BusRouteV2.vue +12 −11 File changed.Preview size limit exceeded, changes collapsed. Show changes