Files
sbnews/赛事中心赛程直播赔率组件实施计划.md
T
2026-07-11 11:19:43 +08:00

302 lines
8.1 KiB
Markdown

# 赛事中心赛程、直播、赔率组件实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. 本项目未授权子代理、自动测试、构建、提交或部署。
**Goal:** 在赛事中心当前页面内用一个通用组件切换赛程、直播和赔率内容,不再因点击三个 Tab 切换到世界杯主题组件。
**Architecture:** `MatchCenterPanel.vue` 只保留赛事中心外壳和世界杯专题入口,新建 `MatchCenterTabsPanel.vue` 管理通用筛选、三个 Tab、数据加载和内容渲染。后端新增通用直播与赔率接口,并复用现有直播地址解析、赔率盘口格式化和赛事匹配逻辑;世界杯专题接口保持兼容。
**Tech Stack:** PHP 8 / ThinkPHP、Vue 3 `<script setup>`、TypeScript、uni-app、SCSS。
---
### Task 1: 通用直播查询
**Files:**
- Modify: `server/app/common/service/match/MatchLiveService.php`
- Modify: `server/app/api/controller/MatchController.php`
- [ ] **Step 1: 将世界杯直播查询抽成可传筛选条件的通用方法**
`MatchLiveService` 增加:
```php
public static function getLiveCards(int $sportType = 0, string $leagueName = '', bool $worldCupOnly = false): array
```
查询必须保留:
```php
->join('match m', 'm.id = live.match_id')
->where(['m.is_show' => 1])
->where([['m.status', '<>', 2]])
```
`MatchLive` 模型已启用 `SoftDelete`,查询自动排除已软删除记录,不额外重复拼接 `delete_time` 条件。
条件规则:
```php
if ($sportType > 0) {
$query->where('m.sport_type', $sportType);
}
if ($leagueName !== '') {
$query->where('m.league_name', $leagueName);
}
if ($worldCupOnly) {
$query->where(function ($query) {
$query->where('m.league_name', '世界杯')
->whereOr('m.competition_id', 61);
});
}
```
`getWorldCupLiveCards()` 改为调用:
```php
return self::getLiveCards(0, '', true);
```
- [ ] **Step 2: 新增公开接口**
`MatchController::$notNeedLogin` 增加 `liveCards`,新增:
```php
public function liveCards()
{
$sportType = $this->request->get('sport_type/d', 0);
$leagueName = trim($this->request->get('league_name/s', ''));
return $this->data([
'list' => MatchLiveService::getLiveCards($sportType, $leagueName),
]);
}
```
- [ ] **Step 3: 保持世界杯接口响应兼容**
`worldCupLiveCards()` 继续返回 `season_id``list`,不改变专题页字段结构。
### Task 2: 通用赔率查询
**Files:**
- Modify: `server/app/api/controller/MatchController.php`
- [ ] **Step 1: 抽取赔率列表构建方法**
新增私有方法:
```php
private function getOddsList(
string $showType,
string $sourceSite,
string $keyword,
int $includeSpecial,
int $limit,
int $sportType = 0,
string $leagueName = '',
bool $worldCupEventOnly = false
): array
```
`sport_type` 映射规则:
```php
private function resolveOddsSportType(int $sportType): string
{
$map = [1 => 'FT'];
return $map[$sportType] ?? '';
}
```
`sportType > 0` 且无法映射时直接返回空数组。`leagueName` 非空时对 `MatchOdds.league_name` 做精确筛选。
- [ ] **Step 2: 将赛事匹配扩展为通用匹配**
`buildWorldCupEventMap()` 改为:
```php
private function buildOddsEventMap(int $sportType = 0, string $leagueName = '', bool $worldCupOnly = false): array
```
通用查询以 `MatchEvent::where('is_show', 1)` 为基础;根据参数增加 `sport_type``league_name` 或世界杯条件,继续按标准化主客队键匹配。
- [ ] **Step 3: 新增通用赔率接口并保持专题接口**
`MatchController::$notNeedLogin` 增加 `odds`
```php
public function odds()
{
return $this->data([
'list' => $this->getOddsList(
$this->request->get('show_type/s', ''),
strtolower(trim($this->request->get('source_site/s', ''))),
trim($this->request->get('keyword/s', '')),
$this->request->get('include_special/d', 0),
min(max($this->request->get('limit/d', 120), 1), 300),
$this->request->get('sport_type/d', 0),
trim($this->request->get('league_name/s', ''))
),
]);
}
```
`worldCupOdds()` 调用同一方法并传 `worldCupEventOnly=true`,继续返回 `season_id`
### Task 3: 前端 API 类型与请求
**Files:**
- Modify: `uniapp/src/api/match.ts`
- [ ] **Step 1: 将专题类型改成可复用类型**
新增别名或通用接口:
```ts
export type MatchLiveCardItem = WorldCupLiveCardItem
export type MatchOddsMatchItem = WorldCupOddsMatchItem
```
- [ ] **Step 2: 增加通用请求方法**
```ts
export function getMatchLiveCards(data?: {
sport_type?: number
league_name?: string
}) {
return request.get({ url: '/match/liveCards', data }, { withToken: false })
}
export function getMatchOdds(data?: {
sport_type?: number
league_name?: string
show_type?: 'live' | 'today' | 'early'
source_site?: string
keyword?: string
include_special?: number
limit?: number
}) {
return request.get({ url: '/match/odds', data }, { withToken: false })
}
```
世界杯请求方法保持不变。
### Task 4: 通用赛事中心 Tab 组件
**Files:**
- Create: `uniapp/src/packages_match/components/MatchCenterTabsPanel.vue`
- Modify: `uniapp/src/packages_match/components/MatchCenterPanel.vue`
- [ ] **Step 1: 创建组件状态**
组件内部定义:
```ts
type MatchCenterTab = 'schedule' | 'live' | 'odds'
type OddsShowType = '' | 'live' | 'today' | 'early'
const activeTab = ref<MatchCenterTab>('schedule')
const currentSport = ref(0)
const currentLeague = ref('')
```
组件统一加载 `getSportTypes()``getMatchLeagues()`,运动和联赛分类只在赛程 Tab 显示并刷新赛程数据。
- [ ] **Step 2: 迁移赛程列表**
`MatchCenterPanel.vue` 迁移:
- `liveList``upcomingList``endedList`
- 已结束赛事分页
- `matchGroups`
- 分组折叠
- `fetchMatchList()``loadMore()``goDetail()`
- 当前运动和联赛筛选
- 现有 `match-card` 列表模板与样式
- [ ] **Step 3: 增加直播内容**
调用:
```ts
getMatchLiveCards()
```
直播卡复用世界杯直播卡现有的地址选择、主播信息、观看/关注人数格式化以及 H5/App 播放兼容逻辑,但不渲染世界杯主题头部。
- [ ] **Step 4: 增加赔率内容和筛选**
赔率请求:
```ts
getMatchOdds({
show_type: oddsShowType.value || undefined,
keyword: oddsKeyword.value || undefined,
include_special: 0,
limit: 300
})
```
保留:
- 全部/滚球盘/今日盘/早盘筛选
- 联赛/球队搜索输入框
- 不显示运动分类和联赛分类条
- 筛选、清空
- 独赢、让球、大小球盘口卡
- 匹配到站内赛事时进入比赛详情
- [ ] **Step 5: 精简赛事中心外壳**
`MatchCenterPanel.vue` 只保留:
```vue
<view class="match-header__title">...</view>
<view class="topic-entry" @tap="openWorldCupCenter">...</view>
<MatchCenterTabsPanel />
```
删除原 `topic-tabs` 的世界杯视图切换事件。蓝色专题卡仍调用:
```ts
emit('open-worldcup', { tab: 'schedule', cid })
```
### Task 5: 文档同步与人工验证建议
**Files:**
- Modify: `业务进度管理.md`
- Modify: `设计稿映射.md`
- [ ] **Step 1: 记录完成内容**
在业务进度中增加:
- 赛事中心三个通用 Tab。
- 通用直播和赔率接口。
- 世界杯专题入口与专题页保持兼容。
- 本次未执行构建、测试和部署。
- [ ] **Step 2: 更新设计稿映射**
将赛事中心备注更新为“赛程/直播/赔率在当前页面内容区切换,世界杯专题卡仍进入专题页”。
- [ ] **Step 3: 建议验证命令,不自动执行**
```powershell
php -l server\app\api\controller\MatchController.php
php -l server\app\common\service\match\MatchLiveService.php
cd uniapp
npm run build:h5
```
建议浏览器人工验证:
1. 赛事中心默认显示赛程。
2. 三个 Tab 切换时 URL 和页面外壳不切换为世界杯主题。
3. 直播按运动/联赛刷新。
4. 赔率盘口和关键词筛选有效。
5. 世界杯专题卡仍能进入专题页。