Commit 0673e425 authored by Ziqiang Li's avatar Ziqiang Li
Browse files

Merge master into poster-update-2026-09-09T14-30-08

parents e6ee2bf7 1e946635
Loading
Loading
Loading
Loading
+6 −1
Changes for docs/.vuepress/client.js: 6 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -17,14 +17,19 @@ 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 { 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'

export default defineClientConfig({
  enhance({ app }) {
  enhance({ app, router }) {
    // MapLibre 6 的独立 Worker 需要由 Vite 打包并显式指定地址。
    setWorkerUrl(maplibreWorkerUrl)

    router.afterEach((to) => {
      const page = busPageForPath(to.path)
      if (page) reportBusVisit(page)
    })
    app.component("BusTable", BusTable)
    app.component("BusTable_img", BusTable_img)
    app.component("BusAnnouncement", BusAnnouncement)
+5 −1
Changes for docs/.vuepress/components/bus-v2/BusHomeV2.vue: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@
      <section class="panel notices">
        <h3>{{ busText('announcements') }}</h3>
        <p v-if="!notices.length" class="muted">{{ busText('empty') }}</p>
        <details v-for="notice in notices" :key="notice.id" :open="noticeScope(notice) === 'global'">
        <details v-for="notice in notices" :key="notice.id" :class="{ 'bus-notice--important': isImportantNotice(notice) }" :open="isNoticeOpen(notice, noticeScope(notice) === 'global')" @toggle="rememberNoticeOpen(notice, $event)">
          <summary>
            <span>{{ noticeTitle(notice) }}</span>
            <small>{{ busText(noticeScope(notice)) }}<template v-if="noticeTime(notice)"> · <time :datetime="notice.starts_at">{{ noticeTime(notice) }}</time></template></small>
@@ -102,6 +102,7 @@ import { displayName, displayStopName, formatDistance, formatLocalDateTime, have
import { favoriteRouteIds, favoriteStopIds, loadFavorites } from './favorites.mjs'
import { busLanguage, busText, setBusLanguage } from './i18n.mjs'
import { renderNoticeMarkdown } from './markdown.mjs'
import { isImportantNotice, loadNoticeOpenStates, noticeIsOpen, saveNoticeOpenState } from './notice-state.mjs'
import BusVehicleLegendV2 from './BusVehicleLegendV2.vue'
import BusVehiclesV2 from './BusVehiclesV2.vue'

@@ -117,6 +118,7 @@ const error = ref('')
const routes = ref([])
const stops = ref([])
const notices = ref([])
const noticeOpenStates = ref(loadNoticeOpenStates())
const vehicles = ref([])
const vehiclesLoading = ref(true)
const vehiclesError = ref(false)
@@ -167,6 +169,8 @@ const openFirstResult = () => { if (searchResults.value[0]) openResult(searchRes
const noticeScope = (notice) => notice.route_id ? 'route' : notice.stop_id ? 'stop' : 'global'
const noticeTitle = (notice) => notice[busLanguage.value === 'en' ? 'title_en' : 'title_zh'] || notice.title_zh || notice.title_en
const noticeTime = (notice) => formatLocalDateTime(notice.starts_at)
const isNoticeOpen = (notice, defaultOpen = false) => noticeIsOpen(noticeOpenStates.value, notice.id, defaultOpen)
const rememberNoticeOpen = (notice, event) => saveNoticeOpenState(noticeOpenStates.value, notice.id, event.target.open)
const arrivalName = (arrival) => `${arrival[busLanguage.value === 'en' ? 'route_name_en' : 'route_name_zh'] || arrival.route_name_zh || arrival.route_name_en || ''} · ${arrival[busLanguage.value === 'en' ? 'direction_name_en' : 'direction_name_zh'] || arrival.direction_name_zh || arrival.direction_name_en || ''}`
const vehicleIcon = (arrival) => String(arrival.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png'
const vehicleLabel = (arrival) => String(arrival.vehicle_type).toUpperCase() === 'SHUTTLE' ? (busLanguage.value === 'zh' ? '电瓶车' : 'EV Shuttle') : (busLanguage.value === 'zh' ? '巴士' : 'Bus')
+73 −28
Changes for docs/.vuepress/components/bus-v2/BusMapV2.vue: 73 added lines, 28 removed lines.
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ import { createApp, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import BusVehicleDetailV2 from './BusVehicleDetailV2.vue'
import BusVehicleLegendV2 from './BusVehicleLegendV2.vue'
import { parseGeometry } from './bus-v2-helpers.mjs'
import { displayName, displayStopName } from './core.mjs'
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'
@@ -37,7 +37,6 @@ const loading = ref(true)
const mapError = ref('')
let map
let maplibregl
let mediaQuery
let themeChangeHandler
let themeObserver
let activePopup
@@ -45,18 +44,23 @@ let vehicleDetailMarker
let vehicleDetailApp
let loaded = false
let protocolInUse = false
let vehicleMarkers = []
let vehicleMarkers = new Map()
let mapEventsBound = false

const activeRoutes = () => props.routes.filter((route) => !props.routeId || route.id === props.routeId)
const activeVehicles = () => props.vehicles.filter((vehicle) => (!props.routeId || vehicle.route_id === props.routeId) && Number.isFinite(+vehicle.longitude) && Number.isFinite(+vehicle.latitude))
const routeFor = (id) => props.routes.find((route) => route.id === id)
const darkTheme = () => document.documentElement.getAttribute('data-theme') === 'dark' || mediaQuery?.matches
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 sourceData = (features) => ({ type: 'FeatureCollection', features })

function vehicleBearing(vehicle) {
  const direction = routeFor(vehicle.route_id)?.directions?.find((item) => item.id === vehicle.route_direction_id)
  return vehicleBearingAt(parseGeometry(direction?.geometry_json), direction?.stops, vehicle)
}

function routeFeatures() {
  return activeRoutes().flatMap((route) => (route.directions || []).map((direction) => ({
    type: 'Feature',
@@ -95,8 +99,12 @@ function fitToStops() {
}

function clearVehicleMarkers() {
  vehicleMarkers.forEach((marker) => marker.remove())
  vehicleMarkers = []
  vehicleMarkers.forEach((record) => { cancelAnimationFrame(record.frame); record.marker.remove() })
  vehicleMarkers = new Map()
}

function settleVehicleMarkers() {
  vehicleMarkers.forEach((record) => { cancelAnimationFrame(record.frame); record.element.style.transition = 'none'; record.element.style.transform = '' })
}

function closeVehiclePopup() {
@@ -118,23 +126,58 @@ function showVehiclePopup(vehicle) {
  vehicleDetailMarker = new maplibregl.Marker({ element: content, anchor: 'bottom', offset: [0, -18] }).setLngLat([+vehicle.longitude, +vehicle.latitude]).addTo(map)
}

function refreshVehicleMarkers() {
  clearVehicleMarkers()
  const visibleVehicles = activeVehicles()
  const selected = selectedVehicle.value && visibleVehicles.find((vehicle) => vehicle.id === selectedVehicle.value.id)
  if (!selected && selectedVehicle.value) closeVehiclePopup()
  vehicleMarkers = visibleVehicles.map((vehicle) => {
    const element = document.createElement('button')
function updateVehicleMarker(record, vehicle) {
  record.vehicle = vehicle
  const route = routeFor(vehicle.route_id)
  record.element.className = `bus-map__vehicle status-${vehicle.data_status || 'offline'}`
  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`)
}

function moveVehicleMarker(record, longitude, latitude) {
  cancelAnimationFrame(record.frame)
  const start = record.marker.getLngLat()
  const target = [+longitude, +latitude]
  if (start.lng === target[0] && start.lat === target[1]) return
  const startPoint = map.project(start), targetPoint = map.project(target)
  record.marker.setLngLat(target)
  record.element.style.transition = 'none'
  record.element.style.transform = `translate(${startPoint.x - targetPoint.x}px, ${startPoint.y - targetPoint.y}px)`
  record.frame = requestAnimationFrame(() => { record.element.style.transition = 'transform 1s linear'; record.element.style.transform = '' })
}

function createVehicleMarker(vehicle) {
  const record = { vehicle, marker: null, frame: 0, element: null }
  const markerElement = document.createElement('div')
  const element = document.createElement('button')
  const arrow = document.createElement('span')
  const image = document.createElement('img')
  arrow.className = 'bus-map__vehicle-arrow'
  markerElement.className = 'bus-map__vehicle-marker'
  image.src = String(vehicle.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png'
  image.alt = ''
  element.type = 'button'
    element.className = `bus-map__vehicle status-${vehicle.data_status || 'offline'}`
    element.style.backgroundImage = `url(${String(vehicle.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png'})`
    element.title = displayName(vehicle, props.language) || vehicle.id
    element.setAttribute('aria-label', element.title)
    element.style.setProperty('--route-color', route?.color || '#2878c8')
    element.addEventListener('click', (event) => { event.stopPropagation(); showVehiclePopup(vehicle) })
    return new maplibregl.Marker({ element, anchor: 'center' }).setLngLat([+vehicle.longitude, +vehicle.latitude]).addTo(map)
  element.append(arrow, image)
  markerElement.append(element)
  element.addEventListener('click', (event) => { event.stopPropagation(); showVehiclePopup(record.vehicle) })
  record.element = element
  updateVehicleMarker(record, vehicle)
  record.marker = new maplibregl.Marker({ element: markerElement, anchor: 'center' }).setLngLat([+vehicle.longitude, +vehicle.latitude]).addTo(map)
  return record
}

function refreshVehicleMarkers() {
  const visibleVehicles = activeVehicles(), visibleIds = new Set(visibleVehicles.map((vehicle) => vehicle.id))
  vehicleMarkers.forEach((record, id) => { if (!visibleIds.has(id)) { cancelAnimationFrame(record.frame); record.marker.remove(); vehicleMarkers.delete(id) } })
  visibleVehicles.forEach((vehicle) => {
    const record = vehicleMarkers.get(vehicle.id)
    if (!record) vehicleMarkers.set(vehicle.id, createVehicleMarker(vehicle))
    else { updateVehicleMarker(record, vehicle); moveVehicleMarker(record, vehicle.longitude, vehicle.latitude) }
  })
  const selected = selectedVehicle.value && visibleVehicles.find((vehicle) => vehicle.id === selectedVehicle.value.id)
  if (!selected && selectedVehicle.value) closeVehiclePopup()
  if (selected) showVehiclePopup(selected)
}

@@ -175,6 +218,7 @@ function addLayers() {
  })
  if (!mapEventsBound) {
    map.on('click', 'bus-v2-stops', showStopPopup)
    map.on('click', (event) => { if (!map.queryRenderedFeatures(event.point, { layers: ['bus-v2-stops'] }).length) closeVehiclePopup() })
    map.on('mouseenter', 'bus-v2-stops', () => { map.getCanvas().style.cursor = 'pointer' })
    map.on('mouseleave', 'bus-v2-stops', () => { map.getCanvas().style.cursor = '' })
    mapEventsBound = true
@@ -242,7 +286,6 @@ async function initialise() {
  if (typeof window === 'undefined' || !mapElement.value) return
  try {
    maplibregl = await import('maplibre-gl')
    mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    await acquireProtocol()
    map = new maplibregl.Map({ container: mapElement.value, style: styleUrl(), center: CAMPUS_CENTER, zoom: 14, minZoom: 12, attributionControl: true })
    map.addControl(new maplibregl.NavigationControl(), 'top-left')
@@ -251,10 +294,10 @@ async function initialise() {
    map.addControl(new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, trackUserLocation: true, showUserHeading: true }), 'top-right')
    map.on('style.load', restoreOverlays)
    themeChangeHandler = () => { if (!props.styleUrl) reloadStyle() }
    mediaQuery.addEventListener('change', themeChangeHandler)
    themeObserver = new MutationObserver(themeChangeHandler)
    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
    map.on('load', () => { loaded = true; addLayers(); fitToStops(); refresh(); loading.value = false })
    map.on('movestart', settleVehicleMarkers)
    map.on('load', () => { loaded = true; addLayers(); fitToStops(); refresh(); requestAnimationFrame(() => mapElement.value?.querySelector('.maplibregl-ctrl-attrib')?.classList.remove('maplibregl-compact-show')); loading.value = false })
    map.on('error', (event) => {
      if (!loaded && event.error) {
        mapError.value = `${props.language === 'zh' ? '地图不可用:' : 'Map unavailable: '}${event.error.message}`
@@ -276,7 +319,6 @@ onBeforeUnmount(() => {
  clearVehicleMarkers()
  closeVehiclePopup()
  activePopup?.remove()
  mediaQuery?.removeEventListener('change', themeChangeHandler)
  themeObserver?.disconnect()
  if (map) map.remove()
  map = null
@@ -295,10 +337,13 @@ defineExpose({ refresh, refreshVehicleMarkers })
.bus-map { position: relative; min-height: 22rem; overflow: hidden; border: 1px solid #d9e2ec; border-radius: .6rem; background: #eef4f8; }
.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: 1.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 :deep(.bus-map__vehicle) { width: 1.4rem; height: 1.4rem; border: 2px solid var(--route-color); border-radius: 50%; padding: 0; background-color: var(--route-color); background-position: center; background-repeat: no-repeat; background-size: contain; cursor: pointer; box-shadow: 0 0 0 2px var(--route-color), 0 1px 4px rgba(0, 0, 0, .35); }
.bus-map :deep(.bus-map__vehicle.status-delayed) { background-color: #f7a600; }
.bus-map :deep(.bus-map__vehicle.status-offline) { background-color: #9aa4b2; }
.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 :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; }
.bus-map :deep(.bus-map__vehicle-arrow) { position: absolute; z-index: 2; inset: 0; pointer-events: none; transform: rotate(var(--bearing)); }
.bus-map :deep(.bus-map__vehicle-arrow)::before { content: ''; position: absolute; top: -.57rem; left: 50%; transform: translateX(-50%); border-right: .44rem solid transparent; border-bottom: .62rem solid #fff; border-left: .44rem solid transparent; }
.bus-map :deep(.bus-map__vehicle-arrow)::after { content: ''; position: absolute; top: -.45rem; left: 50%; transform: translateX(-50%); border-right: .32rem solid transparent; border-bottom: .5rem solid var(--route-color); border-left: .32rem solid transparent; }
.bus-map :deep(.bus-map__interaction-lock), .bus-map :deep(.bus-map__interaction-allow) { background-image: none; font-size: 1rem; }
.bus-map :deep(.bus-map__interaction-lock)::before { content: '🔒'; }
.bus-map :deep(.bus-map__interaction-allow)::before { content: '🖐'; }
+8 −2
Changes for docs/.vuepress/components/bus-v2/BusRouteV2.vue: 8 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@
      <section class="panel notices">
        <h3>{{ busText('announcements') }}</h3>
        <p v-if="!routeNotices.length" class="muted">{{ busText('empty') }}</p>
        <details v-for="notice in routeNotices" :key="notice.id"><summary><span>{{ noticeTitle(notice) }}</span><time v-if="noticeTime(notice)" :datetime="notice.starts_at">{{ noticeTime(notice) }}</time></summary><div class="markdown" v-html="renderNoticeMarkdown(notice.body_markdown)" /></details>
        <details v-for="notice in routeNotices" :key="notice.id" :class="{ 'bus-notice--important': isImportantNotice(notice) }" :open="isNoticeOpen(notice)" @toggle="rememberNoticeOpen(notice, $event)"><summary><span>{{ noticeTitle(notice) }}</span><time v-if="noticeTime(notice)" :datetime="notice.starts_at">{{ noticeTime(notice) }}</time></summary><div class="markdown" v-html="renderNoticeMarkdown(notice.body_markdown)" /></details>
      </section>

      <section v-if="selectedStop" class="panel arrivals" aria-live="polite">
@@ -59,7 +59,7 @@
        <p v-if="!currentDirection?.stops?.length" class="muted">{{ busText('empty') }}</p>
        <ol v-else>
          <li v-for="stop in currentDirection.stops" :key="stop.id" :class="{ selected: stop.id === selectedStopId }">
            <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(atStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(atStop(stop))">S</em><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(betweenStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(betweenStop(stop))">S</em><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b></button></div></div>
            <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(atStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(betweenStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div>
            <div class="stop-main"><a :href="`${stopHref}${encodeURIComponent(stop.id)}`" @click.prevent="selectStop(stop.id)"><strong>{{ routeStopName(stop) }}</strong></a></div>
            <div v-if="expandedVehicleKey === `at-${stop.id}` || expandedVehicleKey === `between-${stop.id}`" class="vehicle-details">
              <BusVehicleDetailV2 v-for="vehicle in expandedVehicleKey === `at-${stop.id}` ? atStop(stop) : betweenStop(stop)" :key="vehicle.id" :vehicle="vehicle" :routes="[route]" :stops="stops" :language="busLanguage" />
@@ -85,6 +85,7 @@ import { groupSchedules } from './bus-v2-helpers.mjs'
import { isFavorite, loadFavorites, toggleFavorite } from './favorites.mjs'
import { busLanguage, busText, setBusLanguage } from './i18n.mjs'
import { renderNoticeMarkdown } from './markdown.mjs'
import { isImportantNotice, loadNoticeOpenStates, noticeIsOpen, saveNoticeOpenState } from './notice-state.mjs'
import BusMapV2 from './BusMapV2.vue'
import BusScheduleRowsV2 from './BusScheduleRowsV2.vue'
import BusVehicleDetailV2 from './BusVehicleDetailV2.vue'
@@ -93,6 +94,7 @@ import BusVehicleLegendV2 from './BusVehicleLegendV2.vue'
const props = defineProps({ id: { type: String, default: '' }, direction: { type: String, default: '' }, stopHref: { type: String, default: '/transport/bustimer_v2_stop.html?id=' } })
const route = ref(null), loading = ref(true), error = ref(''), directionId = ref(''), selectedStopId = ref('')
const notices = ref([]), vehicles = ref([]), stops = ref([]), schedules = ref([]), showSchedules = ref(false), scheduleLoading = ref(false), scheduleError = ref(false)
const noticeOpenStates = ref(loadNoticeOpenStates())
const arrivalState = ref({ loading: false, error: false, items: [] }), allArrivals = ref(false), expandedVehicleKey = ref(''), now = ref(new Date())
const refreshRemaining = ref(30)
let refreshTimer, arrivalRequest = 0
@@ -114,6 +116,8 @@ const labels = { zh: { route: '线路', home: '返回首页', saved: '已收藏'
const label = (key, values = {}) => (labels[busLanguage.value][key] || key).replace(/\{(\w+)\}/g, (_, name) => values[name] ?? '')
const noticeTitle = (notice) => notice[busLanguage.value === 'en' ? 'title_en' : 'title_zh'] || notice.title_zh || notice.title_en
const noticeTime = (notice) => formatLocalDateTime(notice.starts_at)
const isNoticeOpen = (notice) => noticeIsOpen(noticeOpenStates.value, notice.id)
const rememberNoticeOpen = (notice, event) => saveNoticeOpenState(noticeOpenStates.value, notice.id, event.target.open)
const statusText = (value) => value === 'SUSPENDED' ? label('suspended') : label('normal')
const arrivalKey = (item) => `${item.route_direction_id}-${item.trip_id || item.planned_arrival_at || item.updated_at || item.eta_minutes}`
function time(value) { return value ? new Date(value).toLocaleTimeString(busLanguage.value === 'zh' ? 'zh-CN' : 'en', { hour: '2-digit', minute: '2-digit', hour12: false }) : '' }
@@ -153,6 +157,8 @@ defineExpose({ load, refresh, selectDirection, selectStop })
.route-stops .vehicle-marker.at { top: 1rem; }
.route-stops .vehicle-marker.between { top: -0.1rem; background: color-mix(in srgb, #f7a600 18%, var(--bus-v2-bg)); }
.route-stops .vehicle-marker { gap: .1rem; padding: 0; background: transparent; }
.route-stops .vehicle-marker { display: flex; align-items: center; }
.route-stops .vehicle-marker b { margin: 0 .15rem 0 0; }
.route-stops .vehicle-marker.between { background: transparent; }
.route-stops .vehicle-icon { z-index: auto; display: block; width: 1.5rem; height: 1.5rem; margin: 0; border-radius: 50%; background: #fff url('/bus.png') center / calc(100% - 2px) no-repeat; }
.route-stops .vehicle-icon.shuttle { background-image: url('/sev.png'); }
+6 −2

File changed.

Preview size limit exceeded, changes collapsed.

Loading