优化:分类数据优化

This commit is contained in:
ivan 2026-02-28 00:03:37 +08:00
parent af22e1a91c
commit 73cf147348
7 changed files with 124 additions and 25 deletions

View File

@ -17,7 +17,24 @@
| 类型 | 说明 | | 类型 | 说明 |
|------|------| |------|------|
| `PmTagMainItem` | 接口返回的 PmTag 结构,含 children | | `PmTagMainItem` | 接口返回的 PmTag 结构,含 children |
| `CategoryTreeNode` | 前端使用的树节点,含 id、label、slug、icon、sectionTitle、children | | `CategoryTreeNode` | 前端使用的树节点,含 id、label、slug、icon、sectionTitle、children、tagIds |
### CategoryTreeNode.tagIds
从后端 `PmTagCatalogItem.tagId` 字段提取的标签 ID 列表,用于事件筛选:
```typescript
interface CategoryTreeNode {
// ... 其他字段
/** 关联的标签 ID 列表,用于事件筛选 */
tagIds?: number[]
}
```
**提取逻辑**
- 如果 `tagId` 是数字数组,直接使用(`[1351, 1368]`
- 如果 `tagId` 是单个数字,包装为数组(`1351``[1351]`
- 如果 `tagId` 是对象或其他类型,忽略
## 使用方式 ## 使用方式

View File

@ -49,3 +49,18 @@ clearEventListCache()
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入 1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存 2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存
3. **多选项展示**`mapEventItemToCard` 已支持 multi 类型,可扩展 `EventCardOutcome` 字段 3. **多选项展示**`mapEventItemToCard` 已支持 multi 类型,可扩展 `EventCardOutcome` 字段
## 参数传递方式
### tagIds 参数(数组)
`tagIds` 使用传统数组方式传递,不再是逗号分隔的字符串:
```typescript
// 正确方式 - 直接传递数组
const res = await getPmEventPublic({
page: 1,
pageSize: 10,
tagIds: [1, 2, 3] // 会作为多个同名参数传递:?tagIds=1&tagIds=2&tagIds=3
})
```

View File

@ -1,27 +1,69 @@
# Home.vue # Home.vue
**路径**`src/views/Home.vue` **路径**`src/views/Home.vue`
**路由**`/`name: `home`
## 功能用途 ## 功能用途
首页,展示分类 Tab、搜索、事件卡片列表。支持三层分类、下拉刷新、无限滚动、搜索历史卡片支持单一/多选项展示 首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多
## 核心能力 ## 核心能力
- 分类:第一层 Tab、第二层图标、第三层 Tab`getPmTagMain` 获取 - **分类导航**:三层级分类选择(一级 Tab、二级图标、三级 Tab
- 搜索:展开浮层、历史记录、`useSearchHistory` - **事件列表**:卡片式展示,支持下拉刷新、触底加载
- 列表:`getPmEventPublic` 分页、`mapEventItemToCard` 映射、`MarketCard` 渲染 - **搜索**:可按关键词搜索事件
- 缓存:`eventListCache` 切换页面时复用,下拉刷新时清空 - **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
- Keep-alive`Home` 被 include切换回来时保留状态
## 数据流
```
PmTagCatalogItem.tagId = [1351]
↓ mapCatalogToTreeNode
CategoryTreeNode.tagIds = [1351]
↓ 用户选择分类(如:政治 → 特朗普)
activeTagIds = [1351, 1368] // 合并所有选中层级的 tagIds
↓ getPmEventPublic
?tagIds=1351&tagIds=1368
```
## 核心计算属性
### activeTagIds
收集所有选中层级节点的 `tagIds`(含父级),用于事件筛选:
```typescript
const activeTagIds = computed(() => {
const activeIds = layerActiveValues.value
const tagIdSet = new Set<number>()
// 遍历每一层选中的节点,收集所有 tagIds含父级
let currentNodes = filterVisible(categoryTree.value)
for (let i = 0; i < activeIds.length; i++) {
const selectedId = activeIds[i]
if (!selectedId) continue
const node = currentNodes.find((n) => n.id === selectedId)
if (node?.tagIds && node.tagIds.length > 0) {
node.tagIds.forEach((id) => tagIdSet.add(id))
}
currentNodes = filterVisible(node?.children)
}
return Array.from(tagIdSet) // 去重后的数组
})
```
**示例**
- 选中「政治」tagIds: [1351])→ activeTagIds = [1351]
- 选中「政治 → 特朗普」tagIds: [1351] + [1368])→ activeTagIds = [1351, 1368]
## 使用方式 ## 使用方式
- 访问 `/` 即可进入 无需手动调用,路由 `/` 自动加载。
- 分类切换、搜索、下拉刷新、滚动加载均自动工作
## 扩展方式 ## 扩展方式
1. **新增筛选**:在搜索浮层旁增加筛选按钮,修改 `getPmEventPublic` 的 params 1. **新增分类层级**:修改 `MAX_LAYER` 常量,调整模板渲染逻辑
2. **骨架屏**:在 loading 时展示 `v-skeleton-loader` 2. **自定义筛选逻辑**:修改 `activeTagIds` 计算属性
3. **空状态**:列表为空时展示空状态插画与文案 3. **列表缓存策略**:调整 `getEventListCache` / `setEventListCache`

View File

@ -50,6 +50,8 @@ export interface CategoryTreeNode {
updatedAt?: string updatedAt?: string
/** 排序值,有则按从小到大排序 */ /** 排序值,有则按从小到大排序 */
sort?: number sort?: number
/** 关联的标签 ID 列表,用于事件筛选 */
tagIds?: number[]
children?: CategoryTreeNode[] children?: CategoryTreeNode[]
} }
@ -204,6 +206,14 @@ function mapCatalogToTreeNode(item: PmTagCatalogItem): CategoryTreeNode {
? item.children.map(mapCatalogToTreeNode) ? item.children.map(mapCatalogToTreeNode)
: undefined : undefined
const icon = item.icon ?? resolveCategoryIcon({ label, slug }) const icon = item.icon ?? resolveCategoryIcon({ label, slug })
// 提取 tagIds优先使用数组形式的 tagId
const tagIds = Array.isArray(item.tagId)
? item.tagId.filter((v): v is number => typeof v === 'number')
: typeof item.tagId === 'number'
? [item.tagId]
: undefined
return { return {
id, id,
label, label,
@ -211,6 +221,7 @@ function mapCatalogToTreeNode(item: PmTagCatalogItem): CategoryTreeNode {
icon, icon,
sectionTitle: item.sectionTitle, sectionTitle: item.sectionTitle,
sort: item.sort, sort: item.sort,
tagIds,
children: children?.length ? children : undefined, children: children?.length ? children : undefined,
} }
} }

View File

@ -120,9 +120,9 @@ export interface GetPmEventListParams {
page?: number page?: number
pageSize?: number pageSize?: number
keyword?: string keyword?: string
/** 创建时间范围,如 ['2025-01-01', '2025-12-31']collectionFormat: csv */ /** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
createdAtRange?: string[] createdAtRange?: string[]
/** 标签 ID 列表,按分类筛选,collectionFormat: csv */ /** 标签 ID 列表,按分类筛选,传统数组方式传递 */
tagIds?: number[] tagIds?: number[]
} }
@ -137,14 +137,14 @@ export async function getPmEventPublic(
params: GetPmEventListParams = {}, params: GetPmEventListParams = {},
): Promise<PmEventListResponse> { ): Promise<PmEventListResponse> {
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
const query: Record<string, string | number | string[] | undefined> = { const query: Record<string, string | number | number[] | string[] | undefined> = {
page, page,
pageSize, pageSize,
} }
if (keyword != null && keyword !== '') query.keyword = keyword if (keyword != null && keyword !== '') query.keyword = keyword
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
if (tagIds != null && tagIds.length > 0) { if (tagIds != null && tagIds.length > 0) {
query.tagIds = tagIds.join(',') query.tagIds = tagIds
} }
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query) return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
} }

View File

@ -35,7 +35,7 @@ export interface RequestConfig {
*/ */
export async function get<T = unknown>( export async function get<T = unknown>(
path: string, path: string,
params?: Record<string, string | number | string[] | undefined>, params?: Record<string, string | number | string[] | number[] | undefined>,
config?: RequestConfig, config?: RequestConfig,
): Promise<T> { ): Promise<T> {
const url = new URL(path, BASE_URL || window.location.origin) const url = new URL(path, BASE_URL || window.location.origin)

View File

@ -415,14 +415,28 @@ function findNodeById(nodes: CategoryTreeNode[], id: string): CategoryTreeNode |
return undefined return undefined
} }
/** 当前选中分类的 tagIds[第一层, 第二层, 第三层],仅包含数字 ID */ /** 当前选中分类的 tagIds收集所有选中层级节点的 tagId 数组(含父级),用于事件筛选 */
const activeTagIds = computed(() => { const activeTagIds = computed(() => {
const ids = layerActiveValues.value const activeIds = layerActiveValues.value
const tagIds: number[] = [] const tagIdSet = new Set<number>()
for (const id of ids) {
if (id && /^\d+$/.test(id)) tagIds.push(parseInt(id, 10)) // tagIds
let currentNodes = filterVisible(categoryTree.value)
for (let i = 0; i < activeIds.length; i++) {
const selectedId = activeIds[i]
if (!selectedId) continue
const node = currentNodes.find((n) => n.id === selectedId)
if (node?.tagIds && node.tagIds.length > 0) {
// tagIds
node.tagIds.forEach((id) => tagIdSet.add(id))
}
//
currentNodes = filterVisible(node?.children)
} }
return tagIds
return Array.from(tagIdSet)
}) })
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */ /** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */