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

17 KiB
Raw Blame History

热门推荐模块前端实现计划

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方法

// ==================== 热门推荐相关接口 ====================

// 批量获取热门分类(页面初始化)
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: 提交
git add frontend/utils/api.js
git commit -m "feat: 新增热门推荐API方法"

Task 2: 创建 HotCategoryBlock.vue 组件

文件:

  • 创建: frontend/pages/square/components/HotCategoryBlock.vue

  • Step 1: 创建组件文件

<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: 提交
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 添加:

{
  "path": "pages/square/hot-category-more",
  "style": {
    "navigationBarTitleText": "热门推荐",
    "navigationBarBackgroundColor": "#1a1a2e",
    "navigationBarTextStyle": "white",
    "backgroundColor": "#1a1a2e"
  }
}
  • Step 1: 创建 hot-category-more.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: 提交
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 中添加引入和状态

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
onMounted(() => {
  const info = uni.getSystemInfoSync()
  screenWidth.value = info.windowWidth
  screenHeight.value = info.windowHeight

  resetSquare()
  loadBannerActivities()
  loadHotCategories()  // 新增
})
  • Step 3: 在模板的 CreationGrid 之前添加4个 HotCategoryBlock
<!-- 热门分类区块 -->
<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: 添加样式
/* 热门分类区块 */
.hot-category-wrapper {
  margin-bottom: 16rpx;
}
  • Step 5: 提交
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 页面配置

{
  "path": "pages/square/hot-category-more",
  "style": {
    "navigationBarTitleText": "热门推荐",
    "navigationBarBackgroundColor": "#1a1a2e",
    "navigationBarTextStyle": "white",
    "backgroundColor": "#1a1a2e"
  }
}
  • Step 2: 提交
git add frontend/pages.json
git commit -m "feat: 添加热门分类查看更多页面路由"

验证清单

  • API 方法添加成功
  • HotCategoryBlock 组件渲染正常
  • 4个分类区块正确显示
  • 刷新按钮旋转动画正常
  • 查看更多跳转正常
  • hot-category-more 页面子标签正常(星卡分类)
  • 分页加载正常
  • 空状态显示正常