---
date: 2026-05-14
type: frontend-feature
module: admin-product-v2-itinerary-activity-picker
priority: medium
backend_pr: "#2234"
backend_issue: "#2232"
related: "14_frontend_feature_admin_scenic-season-filter-and-tags.md"
notify: ["@mmg"]
status: pending
---

# 游玩项目按产品线季节过滤 + 节点展示季节标签

## 需求

紧接景区 (#2227 / PR #2229),「从资源库添加活动」弹窗第二个 tab「**游玩项目**」也要按产品线 `seasons` 过滤 + 缩略图按季节切换 + 节点季节标签。**行为与景区完全对齐**。

## 后端已就位 (PR #2234 已合并并部署测试服)

### 数据基础

**新建** `activity_season` 子表(Flyway V20260514_001,字段对照 `scenic_season` 复刻)。运营进后台逐个游玩项目补配季节数据后生效;未配的游玩项目 `vo.seasons` 返 `[]`。

老 `activity.best_season` 自由文本字段保留不动(老 UI 兼容),**不**自动迁移到新结构化子表。

### 接口变更

#### `GET /admin/activity/items` — 游玩项目列表

**新增 query 参数**(与景区接口同款):

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `seasons` | `List<String>` | 否 | 多选 OR 过滤,枚举 `spring/summer/autumn/winter`。传任一交集即返回。空/不传 = 不过滤 |

请求格式(数组多参):
```http
GET /admin/activity/items?seasons=spring&seasons=autumn&pageSize=20
```

**`ActivityListVO` 新增字段:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `seasons` | `List<String>` | 该游玩项目在 `activity_season` 配置的全部季节(distinct,枚举 `spring/summer/autumn/winter`)。**节点标签直接读这个**。 |

> ⚠️ **已有的 `coverUrl` 字段含义变更**:当 query 含 `seasons` 时,`coverUrl` 返回 **query.seasons[0] 对应的 `activity_season.cover_material_id`** 物化 URL;该季节未配/物化失败回落到主表 `coverMaterialId`。无 query.seasons 时维持主表 cover(行为不变)。

#### 真实响应示例

**请求**: `GET /admin/activity/items?pageSize=2`

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "total": 56,
    "records": [
      {
        "activityId": 3002000000000000000,
        "name": "满洲里草原篝火晚会",
        "subtitle": "...",
        "categoryCode": "...",
        "billingType": "...",
        "city": "...",
        "coverUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/...",
        "seasons": [],
        "tags": [...],
        "longitude": ...,
        "latitude": ...
      }
    ]
  },
  "success": true
}
```

> 当前 `activity_season` 表是空的(刚建),所有游玩项目 `seasons` 都返 `[]`。**运营录数据后**,过滤会立即生效;封面切换也立即生效,行为与景区完全一致。

## 前端实现建议

### 弹窗「游玩项目」tab

参考景区 tab 的实现完全对照:

1. 弹窗打开时调列表接口透传产品线 `seasons`:
   ```js
   const params = new URLSearchParams()
   productLineSeasons.forEach(s => params.append('seasons', s))
   await api.get(`/admin/activity/items?${params}&pageSize=20`)
   ```
2. 列表行的**缩略图**直接用 VO 返回的 `coverUrl`(后端已切换好)
3. 列表行的**季节标签**渲染 `vo.seasons` 数组,**4 色与景区一致**:
   - `spring` → 春绿 `#67C23A`
   - `summer` → 夏蓝 `#409EFF`
   - `autumn` → 秋橙 `#E6A23C`
   - `winter` → 冬蓝灰 `#909399`

### 已选节点(行程卡片上)

行程节点 ACTIVITY 类型的 `InternalProductDetailVO.NodeItem.seasons` 已透传 — 在游玩项目节点卡片上渲染同款季节标签(可多个)。**与景区节点同款组件即可复用**。

### 兜底

- 老游玩项目没配 `activity_season` 的,`vo.seasons` 返 `[]`,前端不渲染标签即可
- `coverUrl` 任何时候都不会因为新逻辑变 null(已做回落)

## 工单 & PR

- Backend Issue: #2232
- Backend PR: #2234 (squash merged dev,已部署测试服)
- 测试服真测通过: 基线 total=56 ✅ / VO seasons 字段 ✅ / seasons=autumn 过滤 total=0(数据空) / 反例 seasons=xxx 总=0 ✅ / coverUrl 物化 ✅

## 关联

景区同款实施: `14_frontend_feature_admin_scenic-season-filter-and-tags.md`(PR #2229 / Issue #2227)

弹窗的剩余两个 tab「餐厅」「服务」**暂未实施季节过滤** — 业务侧确认需要再开工单。
