topfans/docs/superpowers/plans/2026-05-28-热门推荐模块前端实现计划.md
zheng020 19bfce1b65 docs: 添加热门推荐模块前端实现计划
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:46:08 +08:00

802 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 热门推荐模块前端实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在广场页面集成4个热门分类区块每个区块显示8张高点赞作品支持刷新和查看更多
**Architecture:** 使用 Vue 3 Composition API + uni-app通过 API 批量获取4个分类数据单独分类刷新查看更多跳转新页面
**Tech Stack:** uni-app + Vue 3 + SCSS
---
## 文件结构
```
frontend/pages/square/
├── square.vue # 修改集成4个HotCategoryBlock
├── components/
│ └── HotCategoryBlock.vue # 新增:单个热门分类区块组件
└── hot-category-more.vue # 新增:热门分类查看更多页面
frontend/utils/
└── api.js # 修改新增3个API方法
```
---
## API 变更
**新增 3 个 API**(在 `api.js` 末尾添加):
1. `getHotInspirationFlowBatchApi()` — 批量获取4个分类
2. `getHotInspirationFlowApi(type)` — 单个分类刷新
3. `getHotInspirationFlowMoreApi(type, cursor, limit)` — 查看更多分页
---
## 实现步骤
### Task 1: 添加 API 方法
**文件:**
- 修改: `frontend/utils/api.js`
- [ ] **Step 1: 在 api.js 末尾添加3个API方法**
```javascript
// ==================== 热门推荐相关接口 ====================
// 批量获取热门分类(页面初始化)
export function getHotInspirationFlowBatchApi() {
return request({
url: '/api/v1/inspiration-flow/hot/batch',
method: 'GET'
})
}
// 单个分类刷新
export function getHotInspirationFlowApi(type) {
return request({
url: '/api/v1/inspiration-flow/hot',
method: 'GET',
data: { type }
})
}
// 查看更多分页
export function getHotInspirationFlowMoreApi(type, cursor = '', limit = 20) {
return request({
url: '/api/v1/inspiration-flow/hot/more',
method: 'GET',
data: { type, cursor, limit }
})
}
```
- [ ] **Step 2: 提交**
```bash
git add frontend/utils/api.js
git commit -m "feat: 新增热门推荐API方法"
```
---
### Task 2: 创建 HotCategoryBlock.vue 组件
**文件:**
- 创建: `frontend/pages/square/components/HotCategoryBlock.vue`
- [ ] **Step 1: 创建组件文件**
```vue
<template>
<view class="hot-category-block">
<!-- 标题 -->
<text class="block-title">{{ title }}</text>
<!-- 网格区域 -->
<view v-if="loading" class="block-grid">
<view v-for="i in 8" :key="i" class="skeleton-card">
<view class="skeleton-shimmer"></view>
</view>
</view>
<view v-else-if="items.length === 0" class="empty-state">
<text class="empty-text">暂无{{ title }}作品</text>
</view>
<view v-else class="block-grid">
<view
v-for="(item, index) in items"
:key="item.asset_id || index"
class="hot-card"
@click="handleCardClick(item)"
>
<image class="hot-card-image" :src="item.cover_url" mode="aspectFill" />
<view class="hot-card-overlay">
<view class="hot-card-user">
<image class="user-avatar" :src="item.owner_avatar" mode="aspectFill" />
<text class="user-name">{{ item.owner_nickname }}</text>
</view>
<view class="hot-card-likes">
<image class="like-icon" src="/static/icon/heart-icon.png" mode="aspectFit" />
<text class="like-count">{{ formatCount(item.likes) }}</text>
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="block-actions">
<view
class="refresh-btn"
:class="{ spinning: refreshing }"
:disabled="refreshing || loading"
@click="handleRefresh"
>
<text class="action-icon">↻</text>
</view>
<view class="more-btn" @click="handleViewMore">
<text class="more-text">查看更多</text>
<text class="more-arrow"></text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { getHotInspirationFlowApi } from '@/utils/api.js'
const props = defineProps({
categoryType: { type: String, required: true },
title: { type: String, required: true }
})
const emit = defineEmits(['cardClick'])
const items = ref([])
const loading = ref(false)
const refreshing = ref(false)
const formatCount = (count) => {
if (!count) return '0'
if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
if (count >= 1000) return (count / 1000).toFixed(1) + 'k'
return count.toString()
}
const handleCardClick = (item) => {
emit('cardClick', item)
}
const handleRefresh = async () => {
if (refreshing.value || loading.value) return
refreshing.value = true
try {
const res = await getHotInspirationFlowApi(props.categoryType)
if (res.code === 200 && res.data?.items) {
items.value = res.data.items
}
} catch (e) {
console.error('[HotCategoryBlock] 刷新失败', e)
} finally {
refreshing.value = false
}
}
const handleViewMore = () => {
uni.navigateTo({
url: `/pages/square/hot-category-more?type=${props.categoryType}&title=${encodeURIComponent(props.title)}`
})
}
// 外部调用:设置数据
const setItems = (newItems) => {
items.value = newItems
}
// 外部调用:设置加载状态
const setLoading = (status) => {
loading.value = status
}
defineExpose({ setItems, setLoading })
</script>
<style scoped>
.hot-category-block {
padding: 0 24rpx;
margin-bottom: 32rpx;
}
.block-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #fff;
padding-bottom: 16rpx;
}
.block-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.hot-card {
width: 48%;
aspect-ratio: 1 / 1.3;
border-radius: 16rpx;
overflow: hidden;
position: relative;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
margin-bottom: 16rpx;
}
.hot-card-image {
width: 100%;
height: 100%;
display: block;
}
.hot-card-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 64rpx;
background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0 12rpx 12rpx;
}
.hot-card-user {
display: flex;
align-items: center;
}
.user-avatar {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
margin-right: 6rpx;
}
.user-name {
font-size: 20rpx;
color: #fff;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hot-card-likes {
display: flex;
align-items: center;
}
.like-icon {
width: 20rpx;
height: 20rpx;
margin-right: 4rpx;
}
.like-count {
font-size: 20rpx;
color: #fff;
}
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 300rpx;
}
.empty-text {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
}
/* 骨架屏 */
.skeleton-card {
width: 48%;
aspect-ratio: 1 / 1.3;
border-radius: 16rpx;
background: #3a3a4a;
overflow: hidden;
position: relative;
margin-bottom: 16rpx;
}
.skeleton-shimmer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* 操作按钮 */
.block-actions {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: 8rpx;
gap: 24rpx;
}
.refresh-btn {
display: flex;
align-items: center;
}
.refresh-btn[disabled] {
opacity: 0.5;
}
.refresh-btn .action-icon {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
}
.refresh-btn.spinning .action-icon {
animation: spin 0.8s linear;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.more-btn {
display: flex;
align-items: center;
}
.more-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
}
.more-arrow {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
margin-left: 4rpx;
}
</style>
```
- [ ] **Step 2: 提交**
```bash
git add frontend/pages/square/components/HotCategoryBlock.vue
git commit -m "feat: 新增HotCategoryBlock组件"
```
---
### Task 3: 创建 hot-category-more.vue 页面
**文件:**
- 创建: `frontend/pages/square/hot-category-more.vue`
- 创建: `frontend/pages/square/hot-category-more.vue` 的 json 配置
**pages.json 添加:**
```json
{
"path": "pages/square/hot-category-more",
"style": {
"navigationBarTitleText": "热门推荐",
"navigationBarBackgroundColor": "#1a1a2e",
"navigationBarTextStyle": "white",
"backgroundColor": "#1a1a2e"
}
}
```
- [ ] **Step 1: 创建 hot-category-more.vue 页面**
```vue
<template>
<view class="hot-more-page">
<!-- 子标签仅星卡显示 -->
<view v-if="isStarCard" class="sub-tabs">
<scroll-view scroll-x :show-scrollbar="false">
<view class="sub-tab-list">
<view
v-for="tab in starCardSubTabs"
:key="tab.value"
class="sub-tab-item"
:class="{ active: activeSubTab === tab.value }"
@click="handleSubTabChange(tab.value)"
>
<text class="sub-tab-text">{{ tab.label }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 网格列表 -->
<view class="more-grid">
<view
v-for="(item, index) in items"
:key="item.asset_id || index"
class="more-card"
@click="handleCardClick(item)"
>
<image class="more-card-image" :src="item.cover_url" mode="aspectFill" />
<view class="more-card-overlay">
<view class="more-card-user">
<image class="user-avatar" :src="item.owner_avatar" mode="aspectFill" />
<text class="user-name">{{ item.owner_nickname }}</text>
</view>
<view class="more-card-likes">
<image class="like-icon" src="/static/icon/heart-icon.png" mode="aspectFit" />
<text class="like-count">{{ formatCount(item.likes) }}</text>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="noMore && items.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getHotInspirationFlowMoreApi } from '@/utils/api.js'
const type = ref('')
const title = ref('')
const activeSubTab = ref('all')
const items = ref([])
const cursor = ref('')
const loading = ref(false)
const noMore = ref(false)
const starCardSubTabs = [
{ label: '全部', value: 'all' },
{ label: '光栅卡', value: 'raster' },
{ label: '镭射卡', value: 'holographic' },
{ label: '撕拉卡', value: 'tear_off' },
{ label: '拍立得', value: 'polaroid' }
]
const isStarCard = computed(() => type.value === 'hot_star_card')
onLoad((options) => {
type.value = options.type || ''
title.value = decodeURIComponent(options.title || '热门推荐')
uni.setNavigationBarTitle({ title: title.value })
loadData()
})
const formatCount = (count) => {
if (!count) return '0'
if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
if (count >= 1000) return (count / 1000).toFixed(1) + 'k'
return count.toString()
}
const loadData = async () => {
if (loading.value || noMore.value) return
loading.value = true
try {
const res = await getHotInspirationFlowMoreApi(type.value, cursor.value)
if (res.code === 200 && res.data?.items) {
const newItems = res.data.items.map(item => ({
...item,
id: item.asset_id
}))
items.value = [...items.value, ...newItems]
cursor.value = res.data.cursor || ''
noMore.value = !res.data.has_more
}
} catch (e) {
console.error('[hot-category-more] 加载失败', e)
} finally {
loading.value = false
}
}
const loadMore = () => {
loadData()
}
const handleSubTabChange = (tab) => {
activeSubTab.value = tab
items.value = []
cursor.value = ''
noMore.value = false
loadData()
}
const handleCardClick = (item) => {
uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}`
})
}
</script>
<style scoped>
.hot-more-page {
min-height: 100vh;
background: #1a1a2e;
padding: 24rpx;
}
.sub-tabs {
margin-bottom: 24rpx;
}
.sub-tab-list {
display: flex;
gap: 16rpx;
padding: 0 24rpx;
}
.sub-tab-item {
padding: 12rpx 24rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 32rpx;
}
.sub-tab-item.active {
background: linear-gradient(135deg, #F0E4B1, #F08399);
}
.sub-tab-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
}
.sub-tab-item.active .sub-tab-text {
color: #fff;
font-weight: 600;
}
.more-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding-bottom: 120rpx;
}
.more-card {
width: 48%;
margin-bottom: 24rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
overflow: hidden;
backdrop-filter: blur(10rpx);
}
.more-card-image {
width: 100%;
height: 340rpx;
}
.more-card-overlay {
padding: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.more-card-user {
display: flex;
align-items: center;
}
.user-avatar {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.user-name {
font-size: 22rpx;
color: #fff;
}
.more-card-likes {
display: flex;
align-items: center;
}
.like-icon {
width: 22rpx;
height: 22rpx;
margin-right: 4rpx;
}
.like-count {
font-size: 22rpx;
color: #fff;
}
.loading-more,
.no-more {
text-align: center;
padding: 32rpx 0;
}
.loading-text,
.no-more-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
</style>
```
- [ ] **Step 2: 提交**
```bash
git add frontend/pages/square/hot-category-more.vue
git commit -m "feat: 新增热门分类查看更多页面"
```
---
### Task 4: 修改 square.vue 集成 HotCategoryBlock
**文件:**
- 修改: `frontend/pages/square/square.vue`
- [ ] **Step 1: 在 script setup 中添加引入和状态**
```javascript
import HotCategoryBlock from './components/HotCategoryBlock.vue'
import { getHotInspirationFlowBatchApi } from '@/utils/api.js'
// 热门分类状态
const hotCategories = ref([])
const hotCategoryRefs = ref({})
// 批量加载热门分类
const loadHotCategories = async () => {
try {
const res = await getHotInspirationFlowBatchApi()
if (res.code === 200 && res.data?.categories) {
hotCategories.value = res.data.categories
}
} catch (e) {
console.error('[square] 加载热门分类失败', e)
}
}
// 刷新单个分类
const handleHotCategoryRefresh = async (categoryType) => {
const ref = hotCategoryRefs.value[categoryType]
if (ref) {
await ref.handleRefresh()
}
}
// 点击热门卡片
const handleHotCardClick = (item) => {
uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}`
})
}
```
- [ ] **Step 2: 在 onMounted 中调用 loadHotCategories**
```javascript
onMounted(() => {
const info = uni.getSystemInfoSync()
screenWidth.value = info.windowWidth
screenHeight.value = info.windowHeight
resetSquare()
loadBannerActivities()
loadHotCategories() // 新增
})
```
- [ ] **Step 3: 在模板的 CreationGrid 之前添加4个 HotCategoryBlock**
```html
<!-- 热门分类区块 -->
<view
v-for="category in hotCategories"
:key="category.type"
class="hot-category-wrapper"
>
<HotCategoryBlock
:ref="el => { if(el) hotCategoryRefs[category.type] = el }"
:categoryType="category.type"
:title="category.title"
@cardClick="handleHotCardClick"
/>
</view>
```
- [ ] **Step 4: 添加样式**
```css
/* 热门分类区块 */
.hot-category-wrapper {
margin-bottom: 16rpx;
}
```
- [ ] **Step 5: 提交**
```bash
git add frontend/pages/square/square.vue
git commit -m "feat: 集成4个热门分类区块到广场页面"
```
---
### Task 5: 添加 pages.json 路由配置
**文件:**
- 修改: `frontend/pages.json`
- [ ] **Step 1: 在 pages.json 添加 hot-category-more 页面配置**
```json
{
"path": "pages/square/hot-category-more",
"style": {
"navigationBarTitleText": "热门推荐",
"navigationBarBackgroundColor": "#1a1a2e",
"navigationBarTextStyle": "white",
"backgroundColor": "#1a1a2e"
}
}
```
- [ ] **Step 2: 提交**
```bash
git add frontend/pages.json
git commit -m "feat: 添加热门分类查看更多页面路由"
```
---
## 验证清单
- [ ] API 方法添加成功
- [ ] HotCategoryBlock 组件渲染正常
- [ ] 4个分类区块正确显示
- [ ] 刷新按钮旋转动画正常
- [ ] 查看更多跳转正常
- [ ] hot-category-more 页面子标签正常(星卡分类)
- [ ] 分页加载正常
- [ ] 空状态显示正常