Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"hls.js": "^1.6.15",
"leancloud-realtime": "5.0.0-rc.8",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"primeicons": "^7.0.0",
"primevue": "^4.5.4",
"uuid": "^13.0.0",
Expand Down
24 changes: 24 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 31 additions & 14 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import Toast from 'primevue/toast';
import { defineAsyncComponent, onBeforeUnmount, onMounted, ref } from 'vue';
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref } from 'vue';
import TopToolbar from './components/header/TopToolbar.vue';
import LiveStage from './components/layout/LiveStage.vue';
import ScheduleArea from './components/layout/ScheduleArea.vue';
import CurrentMatchPanel from './components/panels/CurrentMatchPanel.vue';
import { bindDanmuRoomReset } from './composables/danmuLifecycle';
import { scheduleDeferredMount } from './composables/deferredMount';
import { requestNotificationPermissionOnLaunch } from './composables/notificationPermissionOnLaunch';
import { useScheduleNotifyPolling } from './composables/scheduleNotifyClient';
import { useDanmuStore } from './stores/danmu';
import { useRmDataStore } from './stores/rmData';
import { useScheduleNotifyStore } from './stores/scheduleNotify';
import { useUiStore } from './stores/ui';
import { markPerformance } from './utils/observability';
import type { DanmuMessage } from './types/api';
import type { TeamSelectPayload } from './types/teamSelect';

Expand All @@ -27,6 +27,7 @@ const scheduleNotifyStore = useScheduleNotifyStore();
useScheduleNotifyPolling();

const { selectedZoneChatRoomId } = storeToRefs(dataStore);
const { runningMatchForSelectedZone, streamLoading, liveGameInfo } = storeToRefs(dataStore);

const dataDialogVisible = ref(false);
const dataDialogTeam = ref<string | null>(null);
Expand All @@ -39,12 +40,20 @@ function onDanmuReceived(msg: DanmuMessage) {
}

function onDanmuReset() {
danmuStore.clearMessages();
// Keep cached danmu visible during reconnect; fresh history/realtime messages will update it.
}

bindDanmuRoomReset(selectedZoneChatRoomId, danmuStore.clearMessages);

const enableSecondaryPanels = ref(false);
const enableSecondaryPanels = ref(true);

const showMatchHero = computed(() => {
if (runningMatchForSelectedZone.value) {
return true;
}

return streamLoading.value || !liveGameInfo.value;
});

function onOpenTeamData(payload: string | TeamSelectPayload) {
const teamName = typeof payload === 'string' ? payload : payload.teamName;
Expand All @@ -60,22 +69,16 @@ function onOpenTeamData(payload: string | TeamSelectPayload) {
dataDialogVisible.value = true;
}

let stopDeferredMount: (() => void) | null = null;

onMounted(() => {
markPerformance('rm-app-on-mounted');
requestNotificationPermissionOnLaunch();
void scheduleNotifyStore.syncPrefsToIdb();
uiStore.initializeUi();
dataStore.startPolling();
stopDeferredMount = scheduleDeferredMount(() => {
enableSecondaryPanels.value = true;
});
markPerformance('rm-data-start-dispatched');
});

onBeforeUnmount(() => {
if (stopDeferredMount) {
stopDeferredMount();
}
uiStore.teardownUi();
dataStore.stopPolling();
});
Expand All @@ -86,7 +89,7 @@ onBeforeUnmount(() => {
<Toast position="top-right" />
<TopToolbar />

<section class="match-hero">
<section v-if="showMatchHero" class="match-hero" :class="{ reserving: !runningMatchForSelectedZone }">
<CurrentMatchPanel :key="dataStore.selectedZoneId ?? 'zone-empty'" @team-select="onOpenTeamData" />
</section>

Expand All @@ -107,7 +110,7 @@ onBeforeUnmount(() => {

<style scoped>
.app-shell {
max-width: 1280px;
max-width: 1440px;
margin: 0 auto;
padding: 1rem;
box-sizing: border-box;
Expand All @@ -118,9 +121,23 @@ onBeforeUnmount(() => {
margin-bottom: 1rem;
}

.match-hero.reserving {
min-height: 7.5rem;
}

@media (max-width: 768px) {
.app-shell {
padding: 0.65rem;
}

.match-hero.reserving {
min-height: 6rem;
}
}
</style>

<style>
html {
scrollbar-gutter: stable both-edges;
}
</style>
11 changes: 4 additions & 7 deletions src/components/common/ScheduleItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { TeamSelectPayload } from '@/types/teamSelect';
import type { MatchView } from '@/utils/matchView';
import { DeferredContent } from 'primevue';
import Button from 'primevue/button';
import Card from 'primevue/card';
import Tag from 'primevue/tag';
Expand Down Expand Up @@ -110,9 +109,8 @@ const slug = computed(() => {
</script>

<template>
<DeferredContent>
<Card class="schedule-item" :class="{ compact: props.compact }">
<template #content>
<Card class="schedule-item" :class="{ compact: props.compact }">
<template #content>
<header class="item-header">
<span class="event-title">{{ item.eventTitle || '赛事' }}</span>
<div class="header-center">
Expand Down Expand Up @@ -208,9 +206,8 @@ const slug = computed(() => {
:video-url="item.replayVideo.url"
:cover-url="item.replayVideo.coverUrl"
/>
</template>
</Card>
</DeferredContent>
</template>
</Card>
</template>

<style scoped>
Expand Down
37 changes: 34 additions & 3 deletions src/components/common/TeamLogo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ const props = withDefaults(defineProps<Props>(), {

const logoUrl = computed(() => (props.logo ? buildImageUrl(props.logo) : ''));

const placeholderText = computed(() => {
const value = String(props.teamName ?? '').trim();
if (!value) {
return '';
}

return [...value].slice(0, 2).join('');
});

const wrapperStyle = computed(() => {
if (props.customSize) {
return {
Expand All @@ -42,14 +51,16 @@ const wrapperStyle = computed(() => {

<template>
<div
v-if="logoUrl"
class="team-logo-wrapper"
:class="{ rounded: rounded, square: !rounded }"
:class="{ rounded: rounded, square: !rounded, placeholder: !logoUrl }"
:style="{
...wrapperStyle,
}"
:aria-label="teamName"
role="img"
>
<img :src="logoUrl" :alt="teamName" class="team-logo-img" />
<img v-if="logoUrl" :src="logoUrl" :alt="teamName" class="team-logo-img" decoding="async" />
<span v-else class="team-logo-placeholder">{{ placeholderText }}</span>
</div>
</template>

Expand All @@ -61,6 +72,10 @@ const wrapperStyle = computed(() => {
background-color: white;
flex-shrink: 0;
overflow: hidden;
color: var(--text-color-secondary);
font-weight: 700;
font-size: 0.72em;
line-height: 1;
}

.team-logo-wrapper.rounded {
Expand All @@ -76,4 +91,20 @@ const wrapperStyle = computed(() => {
height: 100%;
object-fit: cover;
}

.team-logo-wrapper.placeholder {
background: linear-gradient(135deg, rgba(148, 163, 184, 0.18), rgba(148, 163, 184, 0.08));
border: 1px solid rgba(148, 163, 184, 0.28);
}

.team-logo-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
text-transform: uppercase;
letter-spacing: 0.02em;
user-select: none;
}
</style>
44 changes: 22 additions & 22 deletions src/components/header/TopToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,16 @@ const settingsVisible = ref(false);
margin-bottom: 1rem;
}

.top-toolbar :deep(.p-toolbar) {
min-height: 3.2rem;
row-gap: 0.45rem;
}

.toolbar-brand {
display: flex;
align-items: center;
gap: 0.65rem;
min-width: 0;
}

.brand-logo {
Expand Down Expand Up @@ -239,23 +245,29 @@ const settingsVisible = ref(false);
font-size: 0.78rem;
}

.toolbar-actions {
display: flex;
align-items: center;
gap: 0.5rem;
.zone-select {
min-width: 9rem;
}

.zone-select-button-wrap {
width: 100%;
min-width: 0;
max-width: 100%;
}

.toolbar-actions > * {
flex-shrink: 0;
.zone-select-button-wrap :deep(.p-selectbutton) {
width: 100%;
min-width: 0;
display: flex;
flex-wrap: wrap;
gap: 0.28rem;
}

.zone-select {
min-width: 9rem;
.zone-select-button-wrap :deep(.p-togglebutton) {
flex: 0 0 auto;
}

.zone-select-button-wrap {
flex: 1;
.zone-select-button-wrap :deep(.p-togglebutton .p-button-label) {
min-width: 0;
}

Expand Down Expand Up @@ -304,14 +316,6 @@ const settingsVisible = ref(false);
padding: 0.25rem;
}

.settings-entry-logo {
display: block;
width: 1.35rem;
height: 1.35rem;
object-fit: contain;
opacity: 0.92;
}

@media (width <= 768px) {
.toolbar-brand-meta h1 {
font-size: 0.92rem;
Expand All @@ -321,10 +325,6 @@ const settingsVisible = ref(false);
display: none;
}

.toolbar-actions {
gap: 0.35rem;
}

.zone-select {
min-width: 0;
flex: 1;
Expand Down
Loading
Loading