index.vue 8.83 KB
<!--
 * @Date: 2025-08-27 17:47:26
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-09-13 16:22:18
 * @FilePath: /lls_program/src/pages/Rewards/index.vue
 * @Description: 文件描述
-->
<template>
  <view class="min-h-screen flex flex-col bg-white">
    <!-- Blue header background -->
    <view class="bg-blue-500 h-48 absolute w-full top-0 left-0 z-0"></view>
    <!-- Content -->
    <view class="relative z-10 flex-1 pb-20">
      <!-- Points display -->
      <view class="pt-8 pb-8 flex flex-col items-center">
        <h2 class="text-4xl font-bold text-white mb-1">{{ totalPoints }}分</h2>
        <p class="text-white text-opacity-80">我的积分</p>
      </view>
      <!-- Main content -->
      <view class="bg-white rounded-t-3xl px-4 pt-5">
        <!-- Search bar -->
        <view class="mb-6">
          <view class="relative">
            <input type="text" v-model="searchQuery" placeholder="搜索商户名称"
              class=" bg-gray-100 rounded-lg py-3 pl-10 pr-4 border border-transparent focus:bg-white focus:border-blue-500 focus:outline-none" />
            <view class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
              <Search2 size="20" class="text-gray-400" />
            </view>
          </view>
        </view>

        <view class="flex gap-3 mb-6">
          <view v-for="option in quickExchangeOptions" :key="option.index" @click="selectedPoints = selectedPoints === option.range ? null : option.range"
            :class="[
              'flex-1 py-3 rounded-lg border text-center',
              selectedPoints === option.range
                ? 'border-blue-500 bg-blue-50 text-blue-500'
                : 'border-gray-200 text-gray-700'
            ]">
            <view class="text-center text-xs">
              <view>{{ option.label }}</view>
              <view>可兑</view>
            </view>
          </view>
        </view>
        <view class="flex justify-between items-center mb-4">
          <h3 class="text-lg font-medium">可兑换列表</h3>
          <view class="flex items-center text-gray-500 text-sm" @click="toggleSortOrder">
            <span class="mr-1">{{ sortOrder === 'desc' ? '从高到低排列' : '从低到高排列' }}</span>
            <ScreenLittle />
          </view>
        </view>
        <!-- Rewards list -->
        <view class="space-y-4">
          <!-- 加载状态 -->
          <view v-if="loading && couponList.length === 0" class="text-center py-8 text-gray-500">
            加载中...
          </view>

          <!-- 空状态 -->
          <view v-else-if="!loading && sortedRewardItems.length === 0" class="text-center py-8 text-gray-500">
            暂无可兑换的商品
          </view>

          <!-- 商品列表 -->
          <template v-else>
            <view v-for="reward in sortedRewardItems" :key="reward.id"
              class="bg-white rounded-xl p-4 flex items-center shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
              <image :src="reward.thumbnail" class="w-16 h-16 rounded-lg mr-4 flex-shrink-0" mode="aspectFill" />
              <view class="flex-1 min-w-0">
                <view class="font-medium text-base">{{ reward.title }}</view>
                <view class="text-gray-500 text-sm mt-1">{{ reward.merchant_name || '商户' }}</view>
              </view>
              <view class="ml-4 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm flex-shrink-0"
                @click="goToRewardDetail(reward)">
                {{ reward.points_cost + '分兑换' }}
              </view>
            </view>

            <!-- 加载更多 -->
            <view v-if="hasMore && !loading && sortedRewardItems.length > 0"
                  class="text-center py-4 text-blue-500"
                  @click="fetchCouponList()">
              加载更多
            </view>

            <!-- 没有更多数据 -->
            <view v-if="!hasMore && sortedRewardItems.length > 0"
                  class="text-center py-4 text-gray-500">
              没有更多数据了
            </view>
          </template>
        </view>
      </view>
    </view>
    <BottomNav />
  </view>
</template>

<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import Taro, { useDidShow } from '@tarojs/taro';
import { ScreenLittle, Search2, My, ArrowUp, ArrowDown } from '@nutui/icons-vue-taro';
import BottomNav from '../../components/BottomNav.vue';
// 导入接口
import { getUserProfileAPI } from '@/api/user';
import { getFamilyDashboardAPI } from '@/api/family';
import { getPointRangesAPI, getCouponListAPI } from '@/api/coupon';

const searchQuery = ref('');
const selectedPoints = ref(null);
const sortOrder = ref('desc'); // 'asc' or 'desc'

const totalPoints = ref(0);

// 页面参数
const pageParams = ref({
  category: '',
  id: ''
});

// API数据状态
const pointRanges = ref([]);
const couponList = ref([]);
const loading = ref(false);
const currentPage = ref(0);
const hasMore = ref(true);

const sortedRewardItems = computed(() => {
  // 确保couponList.value是数组类型
  const couponArray = Array.isArray(couponList.value) ? couponList.value : [];
  let items = [...couponArray];

  // Filter by search query
  if (searchQuery.value) {
    items = items.filter(item =>
      item.title && item.title.toLowerCase().includes(searchQuery.value.toLowerCase())
    );
  }

  // 积分范围筛选现在由API处理,这里不需要前端筛选
  // 排序也由API处理,这里保持原始顺序
  return items;
});

const toggleSortOrder = () => {
  sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
};

// 快速兑换选项基于API返回的积分范围
const quickExchangeOptions = computed(() => {
  // 确保pointRanges.value是数组类型
  const rangesArray = Array.isArray(pointRanges.value) ? pointRanges.value : [];
  return rangesArray.map((range, index) => {
    // 分拆显示逻辑:将"1-100分"分为"1-100"和"分"两部分
    let displayLine1 = range;
    let displayLine2 = '';

    if (range && range.includes('分')) {
      const parts = range.split('分');
      displayLine1 = parts[0]; // "1-100"
      displayLine2 = '分'; // "分"
    }

    return {
      points: range,
      label: range,
      range: range, // 保持原始数据用于传值
      displayLine1: displayLine1, // 第一行显示内容
      displayLine2: displayLine2, // 第二行显示内容
      index: index
    };
  });
});

const goToRewardDetail = (reward) => {
  Taro.navigateTo({
    url: `/pages/RewardDetail/index?id=${reward.id}`
  });
};

/**
 * 处理查看全部积分攻略
 */
const handleViewAll = () => {
  Taro.navigateTo({
    url: '/pages/PointsList/index'
  });
};

/**
 * 切换积分攻略展开收起状态
 */
// const toggleStrategyExpand = () => {
//   isStrategyExpanded.value = !isStrategyExpanded.value;
// };

/**
 * 获取积分范围
 */
const fetchPointRanges = async () => {
  try {
    const { code, data } = await getPointRangesAPI();
    if (code) {
      pointRanges.value = data || [];
    }
  } catch (error) {
    console.error('获取积分范围失败:', error);
  }
};

/**
 * 获取优惠券列表
 */
const fetchCouponList = async (reset = false) => {
  if (loading.value || (!hasMore.value && !reset)) return;

  loading.value = true;

  try {
    const params = {
      keyword: searchQuery.value || undefined,
      point_range: selectedPoints.value || undefined,
      sort: sortOrder.value.toUpperCase(),
      page: reset ? 0 : currentPage.value,
      limit: 10,
      category_id: pageParams.value.id || undefined
    };

    const { code, data } = await getCouponListAPI(params);
    if (code) {
      const coupons = data?.coupons || [];
      if (reset) {
        couponList.value = coupons;
        totalPoints.value = data?.user_points || 0;
        currentPage.value = 0;
      } else {
        couponList.value = [...couponList.value, ...coupons];
      }

      // 如果返回的数据少于limit,说明没有更多数据了
      hasMore.value = coupons.length >= 10;
      currentPage.value += 1;
    }
  } catch (error) {
    console.error('获取优惠券列表失败:', error);
  } finally {
    loading.value = false;
  }
};

const initData = async () => {
  // 获取页面参数
  const instance = Taro.getCurrentInstance();
  const params = instance.router?.params || {};

  pageParams.value = {
    category: params.category || '',
    id: params.id || ''
  };

  // 获取积分兑换相关数据
  await Promise.all([
    fetchPointRanges(),
    fetchCouponList(true)
  ]);
  console.warn('初始化数据')
}

// 监听搜索和筛选条件变化
watch([searchQuery, selectedPoints, sortOrder], () => {
  fetchCouponList(true);
}, { deep: true });

useDidShow(() => {
  initData();
})
</script>

<style scoped>
/* 过渡动画 */
.transition-all {
  transition: all 0.3s ease-in-out;
}

.duration-300 {
  transition-duration: 300ms;
}

.cursor-pointer {
  cursor: pointer;
}

.gap-3 {
  gap: 0.75rem;
}
</style>