feat: match detail live room matching
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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%,
|
||||
|
||||
Reference in New Issue
Block a user