index.vue 7 KB
<template>
  <div class="min-h-screen bg-[#f9fafb] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
    <!-- Navigation Header -->
    <NavHeader :title="pageTitle" />

    <!-- Loading State -->
    <div v-if="loading" class="px-[40rpx] mt-[80rpx] flex items-center justify-center">
      <text class="text-[#9CA3AF] text-[28rpx]">加载中...</text>
    </div>

    <!-- Content List -->
    <div v-else-if="filteredChildren.length > 0" class="px-[40rpx] mt-[40rpx] relative z-10">
      <template v-for="item in filteredChildren" :key="item.id">
        <!-- 有子分类:显示 SectionCard -->
        <SectionCard
          v-if="item.max_depth > 1"
          :title="item.category_name"
          :items="convertToItems(item.children)"
          @item-click="handleItemClick"
        />

        <!-- 无子分类且有文章:直接显示 ArticleCard 列表 -->
        <template v-else-if="item.max_depth === 1 && item.list?.length">
          <view class="bg-white rounded-[32rpx] mb-[32rpx] overflow-hidden shadow-sm">
            <!-- 标题区域 - 与 SectionCard 一致 -->
            <view class="px-[40rpx] py-[32rpx]" style="background: linear-gradient(90deg, #EFF6FF 0%, #DBEAFE 100%)">
              <text class="text-[#1f2937] text-[32rpx] font-normal">{{ item.category_name }}</text>
            </view>

            <!-- 文章列表 -->
            <view class="px-[32rpx] pt-[24rpx] pb-[32rpx]">
              <view v-for="(article, index) in item.list" :key="article.id">
                <ArticleCard
                  :id="article.id"
                  :title="article.post_title"
                  :excerpt="article.post_excerpt"
                  :date="article.formatted_post_date || article.post_date"
                  :collected="article.is_favorite === 1"
                  :show-cover="false"
                  @collect-changed="handleCollectChanged"
                />
                <!-- 卡片间距(最后一项不显示) -->
                <view v-if="index < item.list.length - 1" class="h-[16rpx]"></view>
              </view>
            </view>
          </view>
        </template>
      </template>
    </div>

    <!-- Empty State -->
    <div v-else class="px-[40rpx] mt-[80rpx] flex items-center justify-center">
      <view class="flex flex-col items-center">
        <view class="text-[#9CA3AF] text-[120rpx] mb-[24rpx]">📂</view>
        <text class="text-gray-600 text-[28rpx]">暂无分类</text>
      </view>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useLoad } from '@tarojs/taro'
import NavHeader from '@/components/navigation/NavHeader.vue'
import SectionCard from '@/components/list/SectionCard.vue'
import ArticleCard from '@/components/cards/ArticleCard.vue'
import { listAPI } from '@/api/article'
import { mockArticleListAPI } from '@/utils/mockData'
import { useGo } from '@/hooks/useGo'
import Taro from '@tarojs/taro'
import { USE_MOCK_DATA } from '@/config/app'

const go = useGo()

/**
 * 页面状态
 */
const loading = ref(false)
const data = ref({})

/**
 * 页面标题
 */
const pageTitle = ref('分类列表')

/**
 * 过滤后的子分类列表
 * @description 过滤掉 max_depth === 1 且 list 为空的项
 */
const filteredChildren = computed(() => {
  const children = data.value?.children || []
  return children.filter(item => {
    // 保留有子分类的项
    if (item.max_depth > 1) return true
    // 保留有文章列表的项
    if (item.max_depth === 1 && item.list?.length > 0) return true
    // 过滤掉空项
    return false
  })
})

/**
 * 获取文章分类列表
 * @param {Object} options - 页面参数
 * @param {string} options.cid - 分类ID(首次进入)
 * @param {string} options.id - 子分类ID(后续层级)
 * @param {string} options.title - 页面标题
 */
const fetchCategoryList = async (options) => {
  try {
    loading.value = true

    // 构建请求参数
    const params = {}
    if (options.cid) {
      params.cid = options.cid // 首次进入使用 cid
    } else if (options.id) {
      params.cid = options.id // 后续层级使用 id
    }

    console.log('[Category List] 请求参数:', params)
    console.log('[Category List] 使用 Mock 数据:', USE_MOCK_DATA)

    // 调用文章列表接口(支持分类结构)
    const res = USE_MOCK_DATA
      ? await mockArticleListAPI(params)
      : await listAPI(params)

    if (res.code === 1 && res.data) {
      data.value = res.data
      console.log('[Category List] 分类数据:', res.data)
    } else {
      Taro.showToast({
        title: res.msg || '获取分类列表失败',
        icon: 'none',
        duration: 2000
      })
    }
  } catch (error) {
    console.error('[Category List] 获取分类列表失败:', error)
    throw error
  } finally {
    loading.value = false
  }
}

/**
 * 处理分类点击事件
 * @description 根据该分类的层级和是否有子分类决定跳转
 *
 * 数据结构说明:
 * - level=1: 第一层(大标题),如"入职前"、"入职中"、"入职后"
 * - level=2: 第二层(小标题),如"考试报名"、"资格考试报名入口"
 * - max_level: 总的最大层级数
 * - max_depth: 当前分支的最大深度
 *
 * 跳转规则:
 * - 如果当前是第二层(level=2),直接跳转到 material-list(最终层)
 * - category-list 只显示两层结构
 *
 * @param {Object} item - 被点击的项目数据(第二层分类)
 */
const handleItemClickWithNav = (item, go) => {
  console.log('[Category List] 点击分类:', item)
  console.log('[Category List] 分类层级:', item.level)
  console.log('[Category List] 最大深度:', item.maxDepth)

  // 当前点击的是第二层(level=2),直接跳转到文章列表
  console.log('[Category List] 跳转到文章列表')
  go('/pages/material-list/index', {
    id: item.id,
    title: item.title
  })
}

// 导出 handleItemClick 供 SectionCard 使用
const handleItemClick = (item) => handleItemClickWithNav(item, go)

/**
 * 将子分类数组转换为 SectionCard items 格式
 * @param {Array} children - 子分类数组
 * @returns {Array} SectionCard items 格式
 */
const convertToItems = (children) => {
  if (!children || children.length === 0) return []
  return children.map(category => ({
    id: category.id,
    title: category.category_name,
    subtitle: category.list?.length ? `${category.list.length} 篇文章` : '',
    icon: category.icon || '',
    level: category.level,
    maxDepth: category.max_depth,
    _raw: category
  }))
}

/**
 * 处理收藏状态变化
 * @param {Object} payload - 收藏事件数据
 */
const handleCollectChanged = (payload) => {
  console.log('[Category List] 收藏状态变化:', payload)
  // TODO: 更新本地状态或刷新列表
}

/**
 * 页面加载时接收参数并初始化
 */
useLoad((options) => {
  console.log('[Category List] 页面参数:', options)

  // 设置页面标题
  if (options.title) {
    pageTitle.value = options.title
  }

  // 获取分类列表
  fetchCategoryList(options)
})
</script>

<script>
export default {
  name: 'CategoryListIndex'
}
</script>