CheckInDialog.vue 7 KB
<template>
  <van-popup
    :show="show"
    @update:show="$emit('update:show', $event)"
    round
    position="bottom"
    :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }"
  >
    <div class="p-4">
      <div class="flex justify-between items-center mb-3">
        <h3 class="font-medium">
            <span :class="{ 'text-green-500' : active_tab === 'today' }" @click="active_tab = 'today'">今日打卡</span>&nbsp;&nbsp;&nbsp;&nbsp;
            <span v-if="has_history" :class="{ 'text-green-500' : active_tab === 'history' }" @click="active_tab = 'history'">历史打卡</span>
        </h3>
        <van-icon name="cross" @click="handleClose" />
      </div>

      <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
        <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4>
        <!-- <p class="text-green-600 text-sm">+5 积分已添加到您的账户</p> -->
      </div>
      <template v-else>
        <div class="grid grid-cols-2 gap-4 py-2"> <!-- grid-cols-2 强制每行2列,gap控制间距 -->
          <button
            v-for="checkInType in active_list"
            :key="checkInType.id"
            class="flex flex-col items-center p-2 rounded-lg border transition-colors
                  bg-white/70 border-gray-100 hover:bg-white"
            :class="{
              'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id
            }"
            @click="handleCheckInSelect(checkInType)"
          >
            <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors
                        bg-gray-100 text-gray-500"
                :class="{
                  'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id
                }"
            >
              <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" />
              <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" />
            </div>
            <span :class="['text-xs', checkInType.is_gray ? 'text-gray-500' : '']">{{ checkInType.name }}</span>
          </button>
        </div>

        <div v-if="selectedCheckIn" class="mt-3">
          <!-- <textarea
            :placeholder="`请输入${selectedCheckIn.name}内容...`"
            v-model="checkInContent"
            class="w-full p-3 border border-gray-200 rounded-lg text-sm resize-none h-24"
          /> -->
          <button
            class="mt-2 w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg flex items-center justify-center"
            @click="handleCheckInSubmit"
            :disabled="isCheckingIn"
          >
            <template v-if="isCheckingIn">
              <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
              提交中...
            </template>
            <template v-else>提交打卡</template>
          </button>
        </div>
      </template>
    </div>
  </van-popup>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { showToast } from 'vant'
import { useRoute, useRouter } from 'vue-router'
import { getTaskListAPI, checkinTaskAPI } from "@/api/checkin";

// 签到列表
const checkInTypes = ref([]);

const route = useRoute()
const router = useRouter()

const props = defineProps({
    /** 弹窗显隐 */
    show: { type: Boolean, required: true, default: false },
    /** 今日打卡任务(外部传入,可选) */
    items_today: { type: Array, default: () => [] },
    /** 历史打卡任务(外部传入,可选) */
    items_history: { type: Array, default: () => [] }
})

const emit = defineEmits(['update:show', 'check-in-success', 'check-in-data'])

const selectedCheckIn = ref(null)
const checkInContent = ref('')
const isCheckingIn = ref(false)
const checkInSuccess = ref(false)

/**
 * @var {import('vue').Ref<'today'|'history'>} active_tab
 * @description 当前选中的任务标签页:今日或历史。
 */
const active_tab = ref('today')

/**
 * @function has_history
 * @description 是否存在历史任务(用于显示“历史打卡”标签)。
 * @returns {boolean}
 */
const has_history = computed(() => {
    const list = Array.isArray(props.items_history) ? props.items_history : []
    return list.length > 0
})

/**
 * @function active_list
 * @description 当前展示的任务列表:优先使用外部传入,未传时回退为组件内部获取的列表。
 * @returns {Array}
 */
const active_list = computed(() => {
    const today = Array.isArray(props.items_today) ? props.items_today : []
    const history = Array.isArray(props.items_history) ? props.items_history : []
    if (active_tab.value === 'today') {
        return today.length ? today : checkInTypes.value
    }
    return history
})

const handleCheckInSelect = (type) => {
  if (type.is_gray && type.task_type === 'checkin') {
    showToast('您已经完成了今天的打卡')
    return
  }
  if (type.task_type === 'upload') {
    router.push({
      path: '/checkin/index',
      query: {
        id: type.id
      }
    })
  } else {
    selectedCheckIn.value = type;
  }
}

const handleCheckInSubmit = async () => {
  if (!selectedCheckIn.value) {
    showToast('请选择打卡项目')
    return
  }
  // if (!checkInContent.value.trim()) {
  //   showToast('请输入打卡内容')
  //   return
  // }

  isCheckingIn.value = true
  try {
    // API调用
    const { code, data } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id });
    if (code) {
      checkInSuccess.value = true
      // 重置表单
      setTimeout(() => {
        checkInSuccess.value = false
        selectedCheckIn.value = null
        checkInContent.value = ''
        emit('update:show', false)
      }, 1500)
      emit('check-in-success')
    }
  } catch (error) {
    // showToast('打卡失败,请重试')
  } finally {
    isCheckingIn.value = false
  }
}

const handleClose = () => {
  selectedCheckIn.value = null
  checkInContent.value = ''
  checkInSuccess.value = false
  emit('update:show', false)
}

onMounted(async () => {
    // 当未从外部传入“今日任务”时,回退为组件内部获取的通用任务列表
    if (!Array.isArray(props.items_today) || props.items_today.length === 0) {
        const task = await getTaskListAPI()
        if (task.code) {
            emit('check-in-data', task.data)
            task.data.forEach(item => {
                checkInTypes.value.push({
                    id: item.id,
                    name: item.title,
                    task_type: item.task_type,
                    is_gray: item.is_gray
                })
            })
        }
    }
})
</script>