hookehuyr

feat(CheckInList): 添加二级弹框课程选择功能

为CheckInList组件添加二级弹框功能,支持显示课程列表并选择
移除独立的less文件,将样式内联到组件中
通过provide/inject实现父子弹框联动控制
......@@ -20,7 +20,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, provide } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import CheckInList from '@/components/ui/CheckInList.vue'
import { getTaskListAPI } from "@/api/checkin";
......@@ -113,4 +113,20 @@ onMounted(async () => {
await refresh_checkin_list()
}
})
// 向子组件提供父级弹框的联动控制方法
provide('parent_popup_control', {
/**
* @function hideParent
* @description 隐藏父级弹框。
* @returns {void}
*/
hideParent: () => emit('update:show', false),
/**
* @function reopenParent
* @description 重新打开父级弹框。
* @returns {void}
*/
reopenParent: () => emit('update:show', true)
})
</script>
......
.CheckInListWrapper {
// 列表项样式
.CheckInListItem {
// 选中态样式
&.is-active {
border-color: #bbf7d0; // 绿色边框
background-color: rgba(16, 185, 129, 0.1); // 轻微绿色背景
}
// 图标样式
.Icon {
&.is-active {
background-color: #10b981; // 绿色激活背景
color: #ffffff; // 白色图标
}
}
}
// 提交按钮样式
.SubmitBtn {
&:disabled {
opacity: 0.7; // 禁用态透明度
}
}
}
......@@ -32,13 +32,54 @@
<template v-else>提交打卡</template>
</button>
</div>
<!-- 二级弹框(课程列表,使用mock数据) -->
<van-popup
:show="inner_popup_show"
@update:show="(v) => inner_popup_show = v"
round
position="bottom"
teleport="body"
:close-on-click-overlay="false"
:style="{ minHeight: '40%', maxHeight: '80%', width: '100%' }"
>
<div class="p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-medium">课程列表</h3>
<van-icon name="cross" @click="close_inner_popup" />
</div>
<div class="grid grid-cols-2 gap-4">
<div v-for="course in inner_courses" :key="course.id" class="rounded-xl overflow-hidden bg-white/80">
<div class="h-24 relative">
<img
:src="format_cdn_image(course.imageUrl)"
:alt="course.title"
class="w-full h-full object-cover"
/>
<div v-if="course.isPurchased" class="absolute top-0 left-0 bg-amber-500 text-white text-xs px-2 py-1 rounded-br-lg font-medium" style="background-color: rgba(249, 115, 22, 0.85)">
已购
</div>
</div>
<div class="p-2">
<h4 class="text-sm font-medium line-clamp-1">{{ course.title }}</h4>
<p class="text-xs text-gray-500 line-clamp-1">{{ course.subtitle }}</p>
<div class="flex justify-between items-center mt-2">
<span class="text-xs text-green-600">¥{{ course.price }}</span>
<button class="text-xs px-2 py-1 bg-green-600 text-white rounded" @click="select_inner_course(course)">选择</button>
</div>
</div>
</div>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, inject } from 'vue'
import { useRouter } from 'vue-router'
import { checkinTaskAPI } from '@/api/checkin'
import { showToast } from 'vant'
import { courses as mock_courses } from '@/utils/mockData'
/**
* @typedef {Object} CheckInItem
......@@ -56,6 +97,7 @@ const props = defineProps({
items: { type: Array, default: () => [] },
dense: { type: Boolean, default: false },
scroll: { type: Boolean, default: false },
plain: { type: Boolean, default: false },
})
/**
......@@ -67,6 +109,11 @@ const emit = defineEmits(['submit-success'])
const router = useRouter()
const selected_item = ref(null)
const submitting = ref(false)
// 父弹框联动(仅弹框模式下有效)
const parent_popup = inject('parent_popup_control', null)
// 二级弹框与数据
const inner_popup_show = ref(false)
const inner_courses = ref([])
/**
* @function wrapper_class
......@@ -95,10 +142,30 @@ const scroll_style = computed(() => {
* @returns {void}
*/
const handle_select = (item) => {
// TODO: 想要判断是否有二级菜单
const has_submenu = item.children && item.children.length > 0;
// 如果有二级菜单需要特殊处理
if (has_submenu) {
// 不同模式下弹框的显示逻辑是不一样的
if (props.plain) {
// 普通模式:直接弹出本组件的popup
open_inner_popup()
} else {
// 弹框模式:先隐藏父级弹框,再弹出本组件的popup,关闭后重新打开父级弹框
if (parent_popup && typeof parent_popup.hideParent === 'function') {
parent_popup.hideParent()
}
// 略微延迟以确保父弹框状态切换完成
setTimeout(() => open_inner_popup(), 50)
}
return
}
// 点击已完成打卡项提示
if (item.is_gray && item.task_type === 'checkin') {
showToast('您已经完成了今天的打卡')
return
}
// 点击上传项跳转上传页
if (item.task_type === 'upload') {
router.push({
path: '/checkin/index',
......@@ -133,8 +200,97 @@ const handle_submit = async () => {
submitting.value = false
}
}
/**
* @function open_inner_popup
* @description 打开二级弹框并填充课程列表(mock 数据)。
* @returns {void}
*/
const open_inner_popup = () => {
inner_courses.value = build_course_list()
inner_popup_show.value = true
}
/**
* @function close_inner_popup
* @description 关闭二级弹框;若处于弹框模式则重新打开父级弹框。
* @returns {void}
*/
const close_inner_popup = () => {
inner_popup_show.value = false
if (!props.plain && parent_popup && typeof parent_popup.reopenParent === 'function') {
// 略微延迟,避免与二级弹框关闭动画冲突
setTimeout(() => parent_popup.reopenParent(), 150)
}
}
/**
* @function build_course_list
* @description 构造课程列表(来源于 mock 数据)。
* @returns {Array}
*/
const build_course_list = () => {
return (mock_courses || []).map(c => ({
id: c.id,
title: c.title,
subtitle: c.subtitle,
imageUrl: c.imageUrl,
price: c.price,
isPurchased: !!c.isPurchased
}))
}
/**
* @function select_inner_course
* @description 选择二级弹框中的课程(占位行为:提示并关闭二级弹框)。
* @param {Object} course - 课程对象。
* @returns {void}
*/
const select_inner_course = (course) => {
showToast(`已选择课程:${course.title}`)
close_inner_popup()
}
/**
* @function format_cdn_image
* @description 若图片来自 cdn.ipadbiz.cn,则追加压缩参数;否则原样返回。
* @param {string} url - 图片地址。
* @returns {string}
*/
const format_cdn_image = (url) => {
if (!url) return ''
const host = 'cdn.ipadbiz.cn'
if (url.includes(host)) {
return `${url}?imageMogr2/thumbnail/200x/strip/quality/70`
}
return url
}
</script>
<style lang="less" scoped>
@import './CheckInList.less';
.CheckInListWrapper {
// 列表项样式
.CheckInListItem {
// 选中态样式
&.is-active {
border-color: #bbf7d0; // 绿色边框
background-color: rgba(16, 185, 129, 0.1); // 轻微绿色背景
}
// 图标样式
.Icon {
&.is-active {
background-color: #10b981; // 绿色激活背景
color: #ffffff; // 白色图标
}
}
}
// 提交按钮样式
.SubmitBtn {
&:disabled {
opacity: 0.7; // 禁用态透明度
}
}
}
</style>
......
......@@ -80,7 +80,7 @@
<router-link to="/profile" class="text-green-600 text-sm">打卡记录</router-link>
</div>
<template v-if="checkInTypes.length">
<CheckInList :items="checkInTypes" dense scroll @submit-success="handleHomeCheckInSuccess" />
<CheckInList :items="checkInTypes" dense scroll :plain="true" @submit-success="handleHomeCheckInSuccess" />
</template>
<template v-else>
<div class="text-center">
......