wode-xunlian-moban.vue 8.2 KB
<template>
  <view class="my-template-container">
    <!-- 筛选栏:部位 + 场景 -->
    <view class="filter-bar">
      <!-- 部位筛选 -->
      <view class="filter-wrapper">
        <view class="filter-btn" :class="{ active: activePartId !== '0' }" @click="togglePartDropdown">
          {{ activePart }}
          <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="18"></uni-icons>
        </view>
        <view class="dropdown-menu" v-if="showPartDropdown">
          <view class="dropdown-item" :class="{ selected: activePartId === item.id }" v-for="item in partList"
            :key="item.id" @click="selectPart(item)">
            {{ item.title }}
          </view>
        </view>
      </view>

      <!-- 场景筛选 -->
      <view class="filter-wrapper">
        <view class="filter-btn" :class="{ active: activeSceneId !== '0' }" @click="toggleSceneDropdown">
          {{ activeScene }}
          <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="18"></uni-icons>
        </view>
        <view class="dropdown-menu" v-if="showSceneDropdown">
          <view class="dropdown-item" :class="{ selected: activeSceneId === item.id }" v-for="item in sceneList"
            :key="item.id" @click="selectScene(item)">
            {{ item.title }}
          </view>
        </view>
      </view>
    </view>

    <!-- 我的训练模板列表 -->
    <scroll-view scroll-y class="list-container">
      <view class="grid-container">
        <view class="plan-card" v-for="(item, index) in templateList" :key="index" @click="goToDetail(item)">
          <image :src="item.urlCover || lostImage" mode="aspectFill" class="card-cover" />
          <view class="card-info">
            <text class="card-title">{{ item.name }}</text>
            <view class="card-stats">
              <view class="stat-item">
                <text class="stat-number">{{ item.exerciseCount }}</text>
                <text class="stat-label">动作</text>
              </view>
              <view class="stat-item">
                <text class="stat-number">{{ item.totalSets }}</text>
                <text class="stat-label">组</text>
              </view>
              <view class="stat-item">
                <text class="stat-number">{{ item.totalWeight }}</text>
                <text class="stat-label">kg</text>
              </view>
            </view>
            <text class="tags-text" :maxLines="1">
              {{ item.primaryMuscleNames?.join(' ') || '' }}
            </text>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import TemplatesApi from '@/sheep/api/Template/Templates'

// 模板列表
const templateList = ref([])

// 部位筛选
const activePartId = ref('0')
const activePart = ref('不限')
const showPartDropdown = ref(false)
const partList = ref([])

// 场景筛选
const activeSceneId = ref('0')
const activeScene = ref('不限')
const showSceneDropdown = ref(false)
const sceneList = ref([
  { id: '0', title: '不限' },
  { id: '1', title: '健身房' },
  { id: '2', title: '仅哑铃' },
  { id: '3', title: '仅哑铃+杠铃' },
])

// 缺省图
const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"

// ==================================
// 获取部位分类
// ==================================
const getPartCategories = async () => {
  try {
    const res = await TemplatesApi.getPartAllCategories()
    if (res.code === 0) {
      partList.value = [
        { title: '不限', id: '0' },
        ...res.data.map(item => ({
          title: item.name,
          id: String(item.id)
        }))
      ]
    }
  } catch (e) {
    console.log('部位获取失败', e)
  }
}

// ==================================
// 下拉切换
// ==================================
const togglePartDropdown = () => {
  showPartDropdown.value = !showPartDropdown.value
  showSceneDropdown.value = false
}
const toggleSceneDropdown = () => {
  showSceneDropdown.value = !showSceneDropdown.value
  showPartDropdown.value = false
}

// ==================================
// 选择筛选条件
// ==================================
const selectPart = (item) => {
  activePart.value = item.title
  activePartId.value = item.id
  showPartDropdown.value = false
  doFilter()
}
const selectScene = (item) => {
  activeScene.value = item.title
  activeSceneId.value = item.id
  showSceneDropdown.value = false
  doFilter()
}

// ==================================
// 筛选我的模板
// ==================================
const doFilter = async () => {
  const muscleId = activePartId.value
  const sceneId = activeSceneId.value

  if (muscleId === '0' && sceneId === '0') {
    getMyTemplates()
    return
  }

  try {
    const res = await TemplatesApi.queryCustTemplate(0, muscleId, sceneId)
    templateList.value = res.data || []
  } catch (e) {
    console.log('筛选失败', e)
  }
}

// ==================================
// 获取我的模板
// ==================================
const getMyTemplates = async () => {
  try {
    // 打开注释即可使用真实接口
    // const res = await TemplatesApi.individualTemplates()
    // templateList.value = res.data || []

    // 模拟数据
    templateList.value = [
      { id: 1001, name: "胸部基础训练", urlCover: "https://picsum.photos/400/300?1", exerciseCount: 6, totalSets: 18, totalWeight: 50, primaryMuscleNames: ["胸肌", "三头肌"] },
      { id: 1002, name: "腿部力量训练", urlCover: "https://picsum.photos/400/300?2", exerciseCount: 5, totalSets: 15, totalWeight: 120, primaryMuscleNames: ["股四头", "臀部"] },
      { id: 1003, name: "背部塑形计划", urlCover: "https://picsum.photos/400/300?3", exerciseCount: 7, totalSets: 21, totalWeight: 70, primaryMuscleNames: ["背阔肌"] },
    ]
  } catch (e) {
    console.log('我的模板加载失败', e)
  }
}

// ==================================
// 跳转详情
// ==================================
const goToDetail = (item) => {
  uni.navigateTo({
    url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${item.id}`
  })
}

// ==================================
// 初始化
// ==================================
onMounted(() => {
  getPartCategories()
  getMyTemplates()
})
</script>

<style scoped lang="scss">
.my-template-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  background: #f6f6f6;
}

/* 筛选栏 */
.filter-bar {
  display: flex;
  gap: 20rpx;
  padding: 20rpx 30rpx;
  background: #fff;
  border-bottom: 1rpx solid #f0f0f0;
}

.filter-wrapper {
  position: relative;
  z-index: 999;
}

.filter-btn {
  display: flex;
  align-items: center;
  gap: 8rpx;
  padding: 12rpx 24rpx;
  border: 1rpx solid #ddd;
  border-radius: 40rpx;
  font-size: 28rpx;
  color: #333;

  &.active {
    border-color: #000;
    color: #000;
    font-weight: 500;
  }
}

.dropdown-menu {
  position: absolute;
  top: calc(100% + 8rpx);
  left: 0;
  background: #fff;
  border-radius: 16rpx;
  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  padding: 20rpx;
  min-width: 220rpx;
  max-height: 400rpx;
  overflow-y: auto;
}

.dropdown-item {
  padding: 16rpx 24rpx;
  font-size: 28rpx;
  border-radius: 8rpx;
  color: #333;

  &.selected {
    background: #ffd100;
    color: #000;
    font-weight: bold;
  }
}

/* 列表 */
.list-container {
  flex: 1;
  padding: 20rpx 0;
}

.grid-container {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20rpx;
  padding: 0 20rpx;
}

.plan-card {
  border-radius: 16rpx;
  overflow: hidden;
  position: relative;
  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}

.card-cover {
  width: 100%;
  height: 300rpx;
}

.card-info {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 20rpx;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
}

.card-title {
  color: #fff;
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 10rpx;
}

.card-stats {
  display: flex;
  flex-direction: column;
  gap: 4rpx;
}

.stat-item {
  display: flex;
  align-items: center;
  gap: 6rpx;
}

.stat-number {
  color: #fff;
  font-size: 26rpx;
  font-weight: bold;
}

.stat-label {
  color: rgba(255, 255, 255, 0.8);
  font-size: 22rpx;
}

.tags-text {
  color: rgba(255, 255, 255, 0.7);
  font-size: 22rpx;
  margin-top: 6rpx;
}
</style>