feat: configure hot leagues in match center
This commit is contained in:
@@ -40,6 +40,14 @@
|
||||
:inactive-value="0" @change="handleToggleShow(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="热门联赛" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-if="row.type === 'league' && !row.is_group"
|
||||
v-perms="['match.league/edit']" v-model="row.is_hot" :active-value="1"
|
||||
:inactive-value="0" @change="handleToggleHot(row)" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['match.league/edit']" type="primary" link @click="handleEdit(row)">
|
||||
@@ -68,6 +76,9 @@
|
||||
<el-form-item label="显示">
|
||||
<el-switch v-model="editData.is_show" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editData.type === 'league'" label="热门联赛">
|
||||
<el-switch v-model="editData.is_hot" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEdit = false">取消</el-button>
|
||||
@@ -120,17 +131,34 @@ const editData = reactive({
|
||||
type: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
is_show: 1
|
||||
is_show: 1,
|
||||
is_hot: 0
|
||||
})
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
Object.assign(editData, { id: row.id, label: row.label, type: row.type, icon: row.icon || '', sort: row.sort, is_show: row.is_show })
|
||||
Object.assign(editData, {
|
||||
id: row.id,
|
||||
label: row.label,
|
||||
type: row.type,
|
||||
icon: row.icon || '',
|
||||
sort: row.sort,
|
||||
is_show: row.is_show,
|
||||
is_hot: row.is_hot || 0
|
||||
})
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
const handleToggleShow = async (row: any) => {
|
||||
try {
|
||||
await leagueEdit({ id: row.id, label: row.label, sort: row.sort, is_show: row.is_show })
|
||||
await leagueEdit({ id: row.id, is_show: row.is_show })
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleHot = async (row: any) => {
|
||||
try {
|
||||
await leagueEdit({ id: row.id, is_hot: row.is_hot })
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 联赛管理增加“热门联赛”配置字段,可重复执行。
|
||||
SET @schema_name = DATABASE();
|
||||
SET @has_is_hot = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @schema_name
|
||||
AND TABLE_NAME = 'la_league'
|
||||
AND COLUMN_NAME = 'is_hot'
|
||||
);
|
||||
SET @alter_sql = IF(
|
||||
@has_is_hot = 0,
|
||||
'ALTER TABLE `la_league` ADD COLUMN `is_hot` tinyint unsigned NOT NULL DEFAULT 0 COMMENT ''是否热门联赛: 0=否 1=是'' AFTER `is_show`',
|
||||
'SELECT ''la_league.is_hot already exists'''
|
||||
);
|
||||
PREPARE alter_stmt FROM @alter_sql;
|
||||
EXECUTE alter_stmt;
|
||||
DEALLOCATE PREPARE alter_stmt;
|
||||
@@ -16,6 +16,7 @@ CREATE TABLE `la_league` (
|
||||
`icon` varchar(500) NOT NULL DEFAULT '' COMMENT '联赛图标 URL',
|
||||
`sort` int NOT NULL DEFAULT 0 COMMENT '排序权重(越小越靠前)',
|
||||
`is_show` tinyint unsigned NOT NULL DEFAULT 1 COMMENT '是否显示: 0=隐藏 1=显示',
|
||||
`is_hot` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '是否热门联赛: 0=否 1=是',
|
||||
`extra_data` json DEFAULT NULL COMMENT '额外数据(如 calendar 等)',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
|
||||
@@ -796,6 +796,7 @@ CREATE TABLE `la_league` (
|
||||
`icon` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
|
||||
`sort` int NOT NULL DEFAULT 0 COMMENT '排序权重(越小越靠前)',
|
||||
`is_show` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否显示: 0=隐藏 1=显示',
|
||||
`is_hot` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否热门联赛: 0=否 1=是',
|
||||
`extra_data` json NULL COMMENT '额外数据(如 calendar 等)',
|
||||
`sessionid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Session ID',
|
||||
`create_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
|
||||
@@ -59,6 +59,7 @@ class LeagueLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
'icon' => $sport['icon'] ?? '',
|
||||
'sort' => $sport['sort'],
|
||||
'is_show' => $sport['is_show'],
|
||||
'is_hot' => 0,
|
||||
'is_group' => true,
|
||||
'children' => $children,
|
||||
];
|
||||
@@ -86,6 +87,7 @@ class LeagueLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
'icon' => '',
|
||||
'sort' => '',
|
||||
'is_show' => '',
|
||||
'is_hot' => '',
|
||||
'is_group' => true,
|
||||
'children' => $orphans,
|
||||
];
|
||||
|
||||
@@ -15,13 +15,24 @@ class LeagueLogic extends BaseLogic
|
||||
public static function edit(array $params): bool
|
||||
{
|
||||
try {
|
||||
League::update([
|
||||
'id' => $params['id'],
|
||||
'label' => $params['label'],
|
||||
'icon' => $params['icon'] ?? '',
|
||||
'sort' => $params['sort'] ?? 0,
|
||||
'is_show' => $params['is_show'] ?? 1,
|
||||
]);
|
||||
$league = League::findOrEmpty($params['id']);
|
||||
if ($league->isEmpty()) {
|
||||
self::setError('联赛不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach (['label', 'icon', 'sort', 'is_show'] as $field) {
|
||||
if (array_key_exists($field, $params)) {
|
||||
$data[$field] = $params[$field];
|
||||
}
|
||||
}
|
||||
if (array_key_exists('is_hot', $params)) {
|
||||
$data['is_hot'] = $league->type === 'league' ? (int)$params['is_hot'] : 0;
|
||||
}
|
||||
if (!empty($data)) {
|
||||
$league->save($data);
|
||||
}
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
@@ -8,10 +8,12 @@ class LeagueValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'id' => 'require',
|
||||
'is_hot' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'id.require' => '联赛id不能为空',
|
||||
'is_hot.in' => '热门联赛状态值错误',
|
||||
];
|
||||
|
||||
public function sceneDetail()
|
||||
@@ -21,7 +23,7 @@ class LeagueValidate extends BaseValidate
|
||||
|
||||
public function sceneEdit()
|
||||
{
|
||||
return $this->only(['id']);
|
||||
return $this->only(['id', 'is_hot']);
|
||||
}
|
||||
|
||||
public function sceneDelete()
|
||||
|
||||
@@ -52,7 +52,8 @@ class MatchController extends BaseApiController
|
||||
if ($sportType > 0) {
|
||||
$query->where('sport_type', $sportType);
|
||||
}
|
||||
$leagues = $query->field('id, label, sport_type, icon, sort')
|
||||
$leagues = $query->field('id, label, sport_type, icon, sort, is_hot')
|
||||
->order('is_hot desc')
|
||||
->order('sort asc')
|
||||
->select()
|
||||
->toArray();
|
||||
@@ -82,6 +83,7 @@ class MatchController extends BaseApiController
|
||||
'id' => $item['id'],
|
||||
'league_name' => $item['label'],
|
||||
'icon' => $item['icon'],
|
||||
'is_hot' => (int)$item['is_hot'],
|
||||
'count' => $counts[$item['label']] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,22 +9,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<MatchCenterTabsPanel @open-worldcup="openWorldCupCenter" />
|
||||
<MatchCenterTabsPanel />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import AiAssistantEntry from '@/components/ai-assistant-entry/ai-assistant-entry.vue'
|
||||
import { getArticleCate } from '@/api/news'
|
||||
import MatchCenterTabsPanel from './MatchCenterTabsPanel.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-worldcup', payload: { tab: 'schedule'; cid?: number | null }): void
|
||||
}>()
|
||||
|
||||
const statusBarHeight = ref(0)
|
||||
const worldCupCateId = ref<number | null>(null)
|
||||
|
||||
const initLayout = () => {
|
||||
uni.getSystemInfo({
|
||||
@@ -39,27 +33,6 @@ const initLayout = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const fetchWorldCupCateId = async () => {
|
||||
if (worldCupCateId.value) return worldCupCateId.value
|
||||
try {
|
||||
const cateList = await getArticleCate()
|
||||
const category = (cateList || []).find((item: any) => {
|
||||
const name = item?.name || item?.label || item?.title
|
||||
return String(name) === '世界杯'
|
||||
})
|
||||
worldCupCateId.value = category?.id || null
|
||||
} catch (error) {
|
||||
console.log('获取世界杯分类失败=>', error)
|
||||
worldCupCateId.value = null
|
||||
}
|
||||
return worldCupCateId.value
|
||||
}
|
||||
|
||||
const openWorldCupCenter = async () => {
|
||||
const cid = await fetchWorldCupCateId()
|
||||
emit('open-worldcup', { tab: 'schedule', cid })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initLayout()
|
||||
})
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
<view class="match-center-tabs">
|
||||
<view class="content-tabs">
|
||||
<view v-for="tab in contentTabs" :key="tab.key" class="content-tabs__item"
|
||||
:class="{ 'content-tabs__item--active': activeTab === tab.key }" @tap="switchContentTab(tab.key)">
|
||||
:class="{ 'content-tabs__item--active': activeTab === tab.key && !hotLeagueMode }"
|
||||
@tap="switchContentTab(tab.key)">
|
||||
<text>{{ tab.label }}</text>
|
||||
</view>
|
||||
<view class="content-tabs__item" @tap="emit('open-worldcup')">
|
||||
<view class="content-tabs__item" :class="{ 'content-tabs__item--active': hotLeagueMode }"
|
||||
@tap="openHotLeagues">
|
||||
<text>热门联赛</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -42,7 +44,7 @@
|
||||
<view class="league-tabs__inner">
|
||||
<view class="league-tab" :class="{ 'league-tab--active': currentLeague === '' }"
|
||||
@tap="switchLeague('')">
|
||||
<text>热门</text>
|
||||
<text>{{ hotLeagueMode ? '全部热门' : '全部' }}</text>
|
||||
</view>
|
||||
<view v-for="(league, idx) in leagueTabs" :key="idx" class="league-tab"
|
||||
:class="{ 'league-tab--active': currentLeague === league.league_name }"
|
||||
@@ -76,9 +78,9 @@
|
||||
<u-loading mode="circle" />
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-if="!scheduleLoading && !matchGroups.length"
|
||||
class="content-empty">
|
||||
<u-empty text="暂无赛事" mode="data" />
|
||||
<view v-if="!scheduleLoading && !matchGroups.length" class="content-empty">
|
||||
<u-empty :text="hotLeagueMode && !leagueTabs.length ? '暂无后台配置的热门联赛' : '暂无赛事'"
|
||||
mode="data" />
|
||||
</view>
|
||||
<view v-if="endedNoMore && endedList.length" class="content-nomore">
|
||||
<text>没有更多赛事了</text>
|
||||
@@ -271,10 +273,6 @@ type MatchCenterTab = 'schedule' | 'live' | 'odds'
|
||||
type OddsShowType = '' | 'live' | 'today' | 'early'
|
||||
type OddsPlatformOption = { key: string; label: string }
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-worldcup'): void
|
||||
}>()
|
||||
|
||||
const showStandaloneOddsTab = false
|
||||
|
||||
const allContentTabs: { key: MatchCenterTab; label: string }[] = [
|
||||
@@ -298,6 +296,7 @@ const defaultOddsPlatformOptions: OddsPlatformOption[] = [
|
||||
]
|
||||
|
||||
const activeTab = ref<MatchCenterTab>('schedule')
|
||||
const hotLeagueMode = ref(false)
|
||||
const sportTabs = ref<any[]>([])
|
||||
const currentSport = ref(0)
|
||||
const currentLeague = ref('')
|
||||
@@ -409,21 +408,25 @@ const filterScheduleItems = (items: any[]) => {
|
||||
const dateFiltered = selectedDateKey.value
|
||||
? items.filter((item: any) => getMatchDateKey(item) === selectedDateKey.value)
|
||||
: items
|
||||
if (currentLeague.value) return dateFiltered
|
||||
const hotItems = dateFiltered.filter((item: any) => Number(item?.is_hot) === 1)
|
||||
return hotItems.length ? hotItems : dateFiltered
|
||||
if (!hotLeagueMode.value || currentLeague.value) return dateFiltered
|
||||
const hotLeagueNames = new Set(leagueTabs.value.map((item: any) => String(item?.league_name || '')))
|
||||
return dateFiltered.filter((item: any) => hotLeagueNames.has(String(item?.league_name || '')))
|
||||
}
|
||||
|
||||
const leagueTabs = computed(() => {
|
||||
let leagues: any[] = []
|
||||
if (currentSport.value === 0) {
|
||||
const merged: any[] = []
|
||||
Object.keys(allLeagues.value || {}).forEach((key) => {
|
||||
const items = (allLeagues.value as any)[key]
|
||||
if (Array.isArray(items)) merged.push(...items)
|
||||
if (Array.isArray(items)) leagues.push(...items)
|
||||
})
|
||||
return merged
|
||||
} else {
|
||||
leagues = allLeagues.value[currentSport.value] || []
|
||||
}
|
||||
return allLeagues.value[currentSport.value] || []
|
||||
if (hotLeagueMode.value) {
|
||||
leagues = leagues.filter((item: any) => Number(item?.is_hot) === 1)
|
||||
}
|
||||
return leagues
|
||||
})
|
||||
|
||||
const matchGroups = computed(() => {
|
||||
@@ -604,13 +607,23 @@ const refreshCurrentTab = () => {
|
||||
}
|
||||
|
||||
const switchContentTab = (tab: MatchCenterTab) => {
|
||||
if (activeTab.value === tab) return
|
||||
if (activeTab.value === tab && !hotLeagueMode.value) return
|
||||
hotLeagueMode.value = false
|
||||
activeTab.value = tab
|
||||
if (tab === 'schedule') void fetchMatchList(true)
|
||||
if (tab === 'live') void fetchLiveCards()
|
||||
if (tab === 'odds') void fetchOdds()
|
||||
}
|
||||
|
||||
const openHotLeagues = () => {
|
||||
if (hotLeagueMode.value) return
|
||||
hotLeagueMode.value = true
|
||||
activeTab.value = 'schedule'
|
||||
currentSport.value = 0
|
||||
currentLeague.value = ''
|
||||
void fetchMatchList(true)
|
||||
}
|
||||
|
||||
const switchSport = (value: number) => {
|
||||
if (currentSport.value === value) return
|
||||
currentSport.value = value
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="match-shell-page">
|
||||
<WorldCupPanel v-if="currentView === 'worldcup'" :initial-tab="worldcupState.tab"
|
||||
:initial-cid="worldcupState.cid" @back="showMatchCenter" />
|
||||
<MatchCenterPanel v-else @open-worldcup="openWorldCup" />
|
||||
<MatchCenterPanel v-else />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -40,14 +40,6 @@ const applyState = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openWorldCup = (payload?: { tab?: string; cid?: number | null }) => {
|
||||
worldcupState.value = {
|
||||
tab: payload?.tab || 'live',
|
||||
cid: payload?.cid ?? null
|
||||
}
|
||||
currentView.value = 'worldcup'
|
||||
}
|
||||
|
||||
const showMatchCenter = () => {
|
||||
currentView.value = 'match'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user