feat: match detail live room matching

This commit is contained in:
hajimi
2026-08-03 02:47:57 +08:00
parent 369ee15044
commit 56b34414ac
5 changed files with 259 additions and 16 deletions
@@ -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'];
}
@@ -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 = [
+5
View File
@@ -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
@@ -6,7 +6,7 @@
</view>
<view class="detail-header__title-wrap">
<text class="detail-header__title">{{ title }}</text>
<text class="detail-header__subtitle">{{ subtitle }}</text>
<text v-if="subtitle" class="detail-header__subtitle">{{ subtitle }}</text>
</view>
<view class="detail-header__share" @tap="$emit('share')">
<u-icon name="share" size="36" color="#333"></u-icon>
@@ -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 {
@@ -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%,