Commit 48c9773c authored by prurite's avatar prurite
Browse files

BusTimerV2: Global refresh button, map improvement and bug fixes

parent 5288953e
Loading
Loading
Loading
Loading
+2 −2
Changes for docs/.vuepress/client.js: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -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'
@@ -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)
+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')">
@@ -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>
@@ -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>
@@ -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>
@@ -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' },
})

@@ -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
@@ -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 || []
@@ -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 }
    }
  }))
@@ -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])
+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
@@ -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>

@@ -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

@@ -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
@@ -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) {
@@ -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) {
@@ -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
}

@@ -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']())
@@ -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; }
+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>
+12 −11

File changed.

Preview size limit exceeded, changes collapsed.

Loading