deploy: auto commit repo-root changes 2026-07-14 09:42:08
This commit is contained in:
@@ -96,7 +96,7 @@ class MatchController extends BaseApiController
|
|||||||
$endedPageSize = $this->request->get('ended_page_size/d', 20);
|
$endedPageSize = $this->request->get('ended_page_size/d', 20);
|
||||||
$endedOnly = $this->request->get('ended_only/d', 0);
|
$endedOnly = $this->request->get('ended_only/d', 0);
|
||||||
|
|
||||||
$field = 'id,league_name,league_icon,home_team,home_icon,home_score,away_team,away_icon,away_score,sport_type,status,match_time,current_minute,half_score,home_odds,draw_odds,away_odds,home_corner,away_corner,home_yellow,away_yellow,home_red,away_red,is_hot,round_name';
|
$field = 'id,competition_id,league_name,league_icon,home_team,home_icon,home_score,away_team,away_icon,away_score,sport_type,status,match_time,current_minute,half_score,home_odds,draw_odds,away_odds,home_corner,away_corner,home_yellow,away_yellow,home_red,away_red,is_hot,round_name';
|
||||||
|
|
||||||
// 基础筛选
|
// 基础筛选
|
||||||
$baseWhere = [['is_show', '=', 1]];
|
$baseWhere = [['is_show', '=', 1]];
|
||||||
@@ -121,6 +121,7 @@ class MatchController extends BaseApiController
|
|||||||
|
|
||||||
// 加载更多时只返回已结束数据
|
// 加载更多时只返回已结束数据
|
||||||
if ($endedOnly) {
|
if ($endedOnly) {
|
||||||
|
$ended = $this->attachMoneylineOdds($ended);
|
||||||
return $this->data([
|
return $this->data([
|
||||||
'ended' => [
|
'ended' => [
|
||||||
'lists' => $ended,
|
'lists' => $ended,
|
||||||
@@ -139,6 +140,13 @@ class MatchController extends BaseApiController
|
|||||||
$upcoming = MatchEvent::where($baseWhere)->where('status', 0)
|
$upcoming = MatchEvent::where($baseWhere)->where('status', 0)
|
||||||
->field($field)->order('match_time asc')->limit(20)->select()->toArray();
|
->field($field)->order('match_time asc')->limit(20)->select()->toArray();
|
||||||
|
|
||||||
|
$liveCount = count($live);
|
||||||
|
$upcomingCount = count($upcoming);
|
||||||
|
$matches = $this->attachMoneylineOdds(array_merge($live, $upcoming, $ended));
|
||||||
|
$live = array_slice($matches, 0, $liveCount);
|
||||||
|
$upcoming = array_slice($matches, $liveCount, $upcomingCount);
|
||||||
|
$ended = array_slice($matches, $liveCount + $upcomingCount);
|
||||||
|
|
||||||
return $this->data([
|
return $this->data([
|
||||||
'live' => $live,
|
'live' => $live,
|
||||||
'upcoming' => $upcoming,
|
'upcoming' => $upcoming,
|
||||||
@@ -151,6 +159,106 @@ class MatchController extends BaseApiController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function attachMoneylineOdds(array $matches): array
|
||||||
|
{
|
||||||
|
if (empty($matches)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$homeTeams = [];
|
||||||
|
$awayTeams = [];
|
||||||
|
$matchKeys = [];
|
||||||
|
foreach ($matches as $index => &$match) {
|
||||||
|
$match['home_odds'] = $this->normalizeMoneylineOdds($match['home_odds'] ?? '');
|
||||||
|
$match['draw_odds'] = $this->normalizeMoneylineOdds($match['draw_odds'] ?? '');
|
||||||
|
$match['away_odds'] = $this->normalizeMoneylineOdds($match['away_odds'] ?? '');
|
||||||
|
|
||||||
|
if ((int)($match['sport_type'] ?? 0) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$key = $this->buildEventOddsMatchKey($match);
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$matchKeys[$index] = $key;
|
||||||
|
$homeTeams[] = trim((string)($match['home_team'] ?? ''));
|
||||||
|
$awayTeams[] = trim((string)($match['away_team'] ?? ''));
|
||||||
|
}
|
||||||
|
unset($match);
|
||||||
|
|
||||||
|
$homeTeams = array_values(array_unique(array_filter($homeTeams)));
|
||||||
|
$awayTeams = array_values(array_unique(array_filter($awayTeams)));
|
||||||
|
if (empty($matchKeys) || empty($homeTeams) || empty($awayTeams)) {
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = MatchOdds::where('sport_type', 'FT')
|
||||||
|
->whereIn('home_team', $homeTeams)
|
||||||
|
->whereIn('away_team', $awayTeams)
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->where('home_win_odds', '<>', '')
|
||||||
|
->whereOr('draw_odds', '<>', '')
|
||||||
|
->whereOr('away_win_odds', '<>', '');
|
||||||
|
})
|
||||||
|
->field('home_team,away_team,match_time_text,home_win_odds,draw_odds,away_win_odds,update_time')
|
||||||
|
->order('update_time desc')
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$oddsMap = [];
|
||||||
|
$scoreMap = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$key = $this->buildOddsMatchKey($row);
|
||||||
|
if (!in_array($key, $matchKeys, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$odds = [
|
||||||
|
'home_odds' => $this->normalizeMoneylineOdds($row['home_win_odds'] ?? ''),
|
||||||
|
'draw_odds' => $this->normalizeMoneylineOdds($row['draw_odds'] ?? ''),
|
||||||
|
'away_odds' => $this->normalizeMoneylineOdds($row['away_win_odds'] ?? ''),
|
||||||
|
];
|
||||||
|
$score = count(array_filter($odds, static fn($value) => $value !== ''));
|
||||||
|
if ($score === 0 || $score <= ($scoreMap[$key] ?? -1)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$oddsMap[$key] = $odds;
|
||||||
|
$scoreMap[$key] = $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($matchKeys as $index => $key) {
|
||||||
|
if (!isset($oddsMap[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$matches[$index] = array_merge($matches[$index], $oddsMap[$key]);
|
||||||
|
}
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventOddsMatchKey(array $match): string
|
||||||
|
{
|
||||||
|
$teamPairKey = $this->buildTeamPairKey(
|
||||||
|
(string)($match['home_team'] ?? ''),
|
||||||
|
(string)($match['away_team'] ?? '')
|
||||||
|
);
|
||||||
|
$timestamp = (int)($match['match_time'] ?? 0);
|
||||||
|
if ($teamPairKey === '' || $timestamp <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if ($this->isWorldCupMatch($match)) {
|
||||||
|
$timestamp += 8 * 60 * 60;
|
||||||
|
}
|
||||||
|
return $teamPairKey . '|' . date('m-d', $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMoneylineOdds($value): string
|
||||||
|
{
|
||||||
|
$value = trim((string)$value);
|
||||||
|
if ($value === '' || !is_numeric($value) || (float)$value <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
public function detail()
|
public function detail()
|
||||||
{
|
{
|
||||||
$id = $this->request->get('id/d');
|
$id = $this->request->get('id/d');
|
||||||
|
|||||||
@@ -53,18 +53,18 @@
|
|||||||
<text class="mc__teng">{{ match.home_team_en || '' }}</text>
|
<text class="mc__teng">{{ match.home_team_en || '' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="mc__odds-item">
|
<view v-if="homeWinOdds" class="mc__odds-item">
|
||||||
<text class="mc__odds-label">主胜</text>
|
<text class="mc__odds-label">主胜</text>
|
||||||
<text class="mc__odds-val">{{ match.home_odds || '0.00' }}</text>
|
<text class="mc__odds-val">{{ homeWinOdds }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="mc__row mc__row--mid">
|
<view class="mc__row mc__row--mid">
|
||||||
<view class="mc__vs-center">
|
<view class="mc__vs-center">
|
||||||
<text class="mc__vs-text">VS</text>
|
<text class="mc__vs-text">VS</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="mc__odds-item">
|
<view v-if="drawOdds" class="mc__odds-item">
|
||||||
<text class="mc__odds-label">平局</text>
|
<text class="mc__odds-label">平</text>
|
||||||
<text class="mc__odds-val">{{ match.draw_odds || '0.00' }}</text>
|
<text class="mc__odds-val">{{ drawOdds }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="mc__row">
|
<view class="mc__row">
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
<text class="mc__teng">{{ match.away_team_en || '' }}</text>
|
<text class="mc__teng">{{ match.away_team_en || '' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="mc__odds-item">
|
<view v-if="awayWinOdds" class="mc__odds-item">
|
||||||
<text class="mc__odds-label">客胜</text>
|
<text class="mc__odds-label">客胜</text>
|
||||||
<text class="mc__odds-val">{{ match.away_odds || '0.00' }}</text>
|
<text class="mc__odds-val">{{ awayWinOdds }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -142,6 +142,16 @@ const liveStyle = computed(() => {
|
|||||||
|
|
||||||
const cardType = computed(() => statusTypeMap[props.match.status] || 'other')
|
const cardType = computed(() => statusTypeMap[props.match.status] || 'other')
|
||||||
|
|
||||||
|
const normalizeMoneylineOdds = (value: unknown) => {
|
||||||
|
const text = String(value ?? '').trim()
|
||||||
|
const number = Number(text)
|
||||||
|
return text && Number.isFinite(number) && number > 0 ? text : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeWinOdds = computed(() => normalizeMoneylineOdds(props.match?.home_odds))
|
||||||
|
const drawOdds = computed(() => normalizeMoneylineOdds(props.match?.draw_odds))
|
||||||
|
const awayWinOdds = computed(() => normalizeMoneylineOdds(props.match?.away_odds))
|
||||||
|
|
||||||
const canShowLiveAi = computed(() => {
|
const canShowLiveAi = computed(() => {
|
||||||
return !!props.showLiveAi && ['live', 'upcoming'].includes(cardType.value) && !!props.match?.id
|
return !!props.showLiveAi && ['live', 'upcoming'].includes(cardType.value) && !!props.match?.id
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -230,30 +230,42 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.match-odds-detail__tabs-inner {
|
.match-odds-detail__tabs-inner {
|
||||||
display: inline-flex;
|
width: 100%;
|
||||||
gap: 12rpx;
|
min-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-odds-detail__tab {
|
.match-odds-detail__tab {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 0 112rpx;
|
||||||
min-width: 112rpx;
|
min-width: 112rpx;
|
||||||
height: 58rpx;
|
height: 64rpx;
|
||||||
padding: 0 22rpx;
|
padding: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1rpx solid #e2e8f0;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-odds-detail__tab--active {
|
.match-odds-detail__tab--active {
|
||||||
border-color: #2563eb;
|
color: #2563eb;
|
||||||
background: #2563eb;
|
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
background: #2563eb;
|
||||||
|
content: '';
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-odds-detail__state {
|
.match-odds-detail__state {
|
||||||
|
|||||||
@@ -136,13 +136,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="detail-tabs">
|
<scroll-view scroll-x class="detail-tabs" show-scrollbar="false">
|
||||||
<view v-for="tab in tabList" :key="tab.key" class="detail-tabs__item"
|
<view class="detail-tabs__inner">
|
||||||
:class="{ 'detail-tabs__item--active': activeTab === tab.key }" @tap="activeTab = tab.key">
|
<view v-for="tab in tabList" :key="tab.key" class="detail-tabs__item"
|
||||||
<text>{{ tab.label }}</text>
|
:class="{ 'detail-tabs__item--active': activeTab === tab.key }" @tap="activeTab = tab.key">
|
||||||
<view v-if="tab.key === 'live' && match.status === 1" class="detail-tabs__dot"></view>
|
<text>{{ tab.label }}</text>
|
||||||
|
<view v-if="tab.key === 'live' && match.status === 1" class="detail-tabs__dot"></view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- Tab 内容区:固定高度可滚动 -->
|
<!-- Tab 内容区:固定高度可滚动 -->
|
||||||
<view class="detail-tab-body">
|
<view class="detail-tab-body">
|
||||||
@@ -2147,21 +2149,32 @@ onUnload(() => {
|
|||||||
|
|
||||||
/* ====== Tab 导航栏 ====== */
|
/* ====== Tab 导航栏 ====== */
|
||||||
.detail-tabs {
|
.detail-tabs {
|
||||||
display: flex;
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
padding: 0 12rpx;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
min-width: 100%;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
flex: 1;
|
position: relative;
|
||||||
display: flex;
|
flex: 0 0 auto;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6rpx;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
position: relative;
|
white-space: nowrap;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
@@ -2183,6 +2196,9 @@ onUnload(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__dot {
|
&__dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 18rpx;
|
||||||
|
right: -14rpx;
|
||||||
width: 10rpx;
|
width: 10rpx;
|
||||||
height: 10rpx;
|
height: 10rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
Reference in New Issue
Block a user