diff --git a/server/app/api/controller/MatchController.php b/server/app/api/controller/MatchController.php index 5222914..b1e73a3 100644 --- a/server/app/api/controller/MatchController.php +++ b/server/app/api/controller/MatchController.php @@ -281,7 +281,7 @@ class MatchController extends BaseApiController return $this->fail('赛事不存在'); } $data = $match->toArray(); - $data['live_list'] = MatchLiveService::getLiveListForApi((int) $data['id']); + $data['live_list'] = MatchLiveService::getMatchedLiveListForApi($data); if (empty($data['live_url']) && !empty($data['live_list'][0]['source_url'])) { $data['live_url'] = (string) $data['live_list'][0]['source_url']; } diff --git a/server/app/common/service/match/MatchLiveService.php b/server/app/common/service/match/MatchLiveService.php index 77bc2bf..cd67634 100644 --- a/server/app/common/service/match/MatchLiveService.php +++ b/server/app/common/service/match/MatchLiveService.php @@ -28,6 +28,47 @@ class MatchLiveService return $liveMap[$matchId] ?? []; } + /** + * 获取赛事可用的直播间:优先返回已绑定赛事,未绑定的房间则按标题中的主客队名称补充匹配。 + */ + public static function getMatchedLiveListForApi(array $match): array + { + $matchId = (int) ($match['id'] ?? 0); + $liveList = $matchId > 0 ? self::getLiveListForApi($matchId) : []; + $knownLiveIds = array_flip(array_filter(array_map( + static fn(array $item): int => (int) ($item['id'] ?? 0), + $liveList + ))); + + $homeTeam = trim((string) ($match['home_team'] ?? '')); + $awayTeam = trim((string) ($match['away_team'] ?? '')); + if ($homeTeam === '' || $awayTeam === '') { + return $liveList; + } + + $homeKeyword = '%' . addcslashes($homeTeam, '\\%_') . '%'; + $awayKeyword = '%' . addcslashes($awayTeam, '\\%_') . '%'; + $matchedRows = MatchLive::where('fetch_status', 1) + ->whereNull('delete_time') + ->where('title', 'like', $homeKeyword) + ->where('title', 'like', $awayKeyword) + ->order('create_time', 'asc') + ->order('id', 'asc') + ->select() + ->toArray(); + + foreach ($matchedRows as $row) { + $liveId = (int) ($row['id'] ?? 0); + if ($liveId <= 0 || isset($knownLiveIds[$liveId])) { + continue; + } + $liveList[] = self::decorateLiveRow($row); + $knownLiveIds[$liveId] = true; + } + + return $liveList; + } + public static function getLiveMapForApi(array $matchIds): array { $matchIds = array_values(array_unique(array_filter(array_map('intval', $matchIds)))); @@ -44,8 +85,7 @@ class MatchLiveService $result = []; foreach ($rows as &$row) { - $row['stream_options'] = self::buildStreamOptions($row); - $row['default_play_url'] = self::resolveDefaultPlayUrl($row); + $row = self::decorateLiveRow($row); $matchId = (int) ($row['match_id'] ?? 0); if ($matchId <= 0) { continue; @@ -57,6 +97,14 @@ class MatchLiveService return $result; } + protected static function decorateLiveRow(array $row): array + { + $row['stream_options'] = self::buildStreamOptions($row); + $row['default_play_url'] = self::resolveDefaultPlayUrl($row); + + return $row; + } + public static function buildStreamOptions(array $row): array { $definitions = [ diff --git a/uniapp/src/api/match.ts b/uniapp/src/api/match.ts index ba746cb..a14bd9a 100644 --- a/uniapp/src/api/match.ts +++ b/uniapp/src/api/match.ts @@ -36,6 +36,11 @@ export interface MatchLiveLineItem { update_time?: string | number updated_at?: string | number source_url?: string + anchor_name?: string + anchor_avatar?: string + room_view_count?: string | number + room_focus_count?: string | number + room_notice?: string default_play_url?: string play_url_m3u8?: string play_url_hd_m3u8?: string diff --git a/uniapp/src/components/match-detail-header/match-detail-header.vue b/uniapp/src/components/match-detail-header/match-detail-header.vue index 60bbddc..f98b9d7 100644 --- a/uniapp/src/components/match-detail-header/match-detail-header.vue +++ b/uniapp/src/components/match-detail-header/match-detail-header.vue @@ -6,7 +6,7 @@ {{ title }} - {{ subtitle }} + {{ subtitle }} @@ -32,29 +32,28 @@ defineEmits<{ .detail-header { position: relative; overflow: hidden; - padding: 0 24rpx 16rpx; + padding: 0 24rpx; background: #fff; color: #333; - box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); + border-bottom: 1rpx solid #eef0f5; &__nav { position: relative; z-index: 1; display: flex; align-items: center; - gap: 12rpx; - height: 88rpx; + height: 96rpx; } &__back, &__share { - width: 64rpx; - height: 64rpx; + width: 72rpx; + height: 72rpx; display: flex; align-items: center; justify-content: center; - border-radius: 20rpx; - background: #f5f5f5; + border-radius: 50%; + background: transparent; } &__title-wrap { @@ -64,14 +63,18 @@ defineEmits<{ flex-direction: column; align-items: center; justify-content: center; - gap: 4rpx; + gap: 2rpx; } &__title { - font-size: 32rpx; - font-weight: 700; - color: #333; + max-width: 480rpx; + font-size: 38rpx; + font-weight: 800; + color: #0b1020; line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &__subtitle { diff --git a/uniapp/src/pages/match_detail/match_detail.vue b/uniapp/src/pages/match_detail/match_detail.vue index 38db071..a8e3a98 100644 --- a/uniapp/src/pages/match_detail/match_detail.vue +++ b/uniapp/src/pages/match_detail/match_detail.vue @@ -2072,6 +2072,193 @@ onUnload(() => { } } +/* 直播主画面:播放器沉浸展示,房间切换保持在同一视觉层级。 */ +.live-entrance { + margin: 0; + gap: 0; + + &__player-card { + gap: 0; + padding: 0; + border-radius: 0; + background: #070a12; + box-shadow: none; + } + + &__player-wrap { + border-radius: 0; + height: 430rpx; + } + + &__player { + height: 430rpx; + object-fit: cover; + } + + &__live-status { + position: absolute; + top: 24rpx; + left: 24rpx; + display: flex; + align-items: center; + overflow: hidden; + border-radius: 12rpx; + background: rgba(3, 12, 7, 0.82); + color: #fff; + z-index: 3; + + text:first-child { + padding: 12rpx 18rpx; + background: #10b54b; + font-size: 28rpx; + font-weight: 700; + } + } + + &__live-minute { + padding: 0 18rpx; + font-size: 26rpx; + font-weight: 700; + font-variant-numeric: tabular-nums; + } + + &__fullscreen { + position: absolute; + right: 24rpx; + bottom: 22rpx; + display: flex; + align-items: center; + gap: 8rpx; + padding: 12rpx 16rpx; + border-radius: 12rpx; + background: rgba(3, 9, 17, 0.72); + color: #fff; + font-size: 24rpx; + font-weight: 600; + z-index: 3; + } + + &__fullscreen-icon { + font-size: 34rpx; + line-height: 1; + } + + &__player-header { + align-items: center; + padding: 20rpx 28rpx 22rpx; + background: #fff; + } + + &__player-title-wrap { + gap: 4rpx; + } + + &__eyebrow { + font-size: 20rpx; + color: #8c95a6; + line-height: 1.2; + } + + &__player-title { + font-size: 29rpx; + font-weight: 700; + color: #141a27; + line-height: 1.35; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__player-meta { + font-size: 21rpx; + line-height: 1.3; + color: #8c95a6; + } + + &__source-action { + display: flex; + align-items: center; + gap: 4rpx; + padding: 10rpx 14rpx; + border: 1rpx solid #d7e2f7; + border-radius: 999rpx; + color: #2376f5; + font-size: 22rpx; + font-weight: 600; + line-height: 1; + flex-shrink: 0; + } + + &__room-tabs { + width: 100%; + white-space: nowrap; + background: #fff; + border-top: 1rpx solid #eff2f7; + } + + &__room-tabs-inner { + display: inline-flex; + min-width: 100%; + gap: 14rpx; + padding: 16rpx 28rpx; + box-sizing: border-box; + } + + &__room-tab { + display: inline-flex; + align-items: center; + max-width: 240rpx; + gap: 8rpx; + padding: 12rpx 16rpx; + border: 1rpx solid #e5eaf2; + border-radius: 999rpx; + background: #f7f9fc; + color: #657086; + flex: 0 0 auto; + + &--active { + border-color: #1468f5; + background: #1468f5; + color: #fff; + } + } + + &__room-index { + min-width: 28rpx; + height: 28rpx; + border-radius: 50%; + background: rgba(20, 104, 245, 0.12); + color: #1468f5; + font-size: 19rpx; + font-weight: 700; + line-height: 28rpx; + text-align: center; + } + + &__room-tab--active &__room-index { + background: rgba(255, 255, 255, 0.2); + color: #fff; + } + + &__room-name { + overflow: hidden; + font-size: 22rpx; + font-weight: 600; + line-height: 1.3; + text-overflow: ellipsis; + } + + &__player-empty { + min-height: 430rpx; + border-radius: 0; + } + + &__player-error { + padding: 12rpx 28rpx; + background: #fff3f2; + } +} + @keyframes live-pulse { 0%,