topfans/frontend/pages/components/TaskModal.vue
2026-04-07 23:08:49 +08:00

386 lines
7.6 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view v-if="visible" class="modal-wrapper" @touchmove.stop.prevent @click.stop>
<transition name="fade">
<view v-if="visible" class="modal-mask" @touchstart.stop="handleMaskTouchStart" @click.stop>
</view>
</transition>
<transition name="scale">
<view v-if="visible" class="modal-container" @click.stop>
<!-- 背景图片 -->
<image class="modal-background" src="/static/background/friend-list-bg.png" mode="aspectFill"></image>
<!-- 蒙层 -->
<view class="modal-overlay"></view>
<!-- 关闭按钮 -->
<view class="close-button" @touchstart.stop="handleCloseTouchStart" @touchend.stop="handleCloseTouchEnd" @click="handleCloseClick">
<text class="close-icon">×</text>
</view>
<!-- 内容区域 -->
<view class="modal-content" @touchstart.stop @touchend.stop @click.stop>
<!-- 标题 -->
<text class="modal-title">每日任务</text>
<!-- 任务列表 -->
<view class="task-list">
<view
v-for="task in tasks"
:key="task.id"
class="task-item"
>
<!-- 任务名称 -->
<text class="task-name">{{ task.name }}</text>
<!-- 奖励金额 -->
<text class="task-reward">+{{ task.reward }}</text>
<!-- 状态按钮 -->
<view
class="task-status-btn"
:class="{ 'completed': task.completed }"
>
<text class="status-text">{{ task.completed ? '已完成' : '未完成' }}</text>
<image v-if="task.completed" class="check-icon" src="/static/icon/check.png" mode="aspectFit"></image>
</view>
</view>
</view>
<!-- 底部进度区域 -->
<view class="progress-section">
<!-- 进度条 -->
<view class="progress-bar">
<view
v-for="(milestone, index) in milestones"
:key="index"
class="progress-node"
:class="{ 'active': index < completedCount }"
>
<view class="node-circle"></view>
<text class="node-label">{{ milestone }}</text>
</view>
</view>
</view>
</view>
</view>
</transition>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['close']);
// 任务数据(硬编码)
const tasks = ref([
{ id: 1, name: '登录一次', reward: 5, completed: true },
{ id: 2, name: '拜访好友小屋一次', reward: 10, completed: true },
{ id: 3, name: '抢占好友展位3次', reward: 10, completed: true },
{ id: 4, name: '抢占好友展位5次', reward: 20, completed: false },
]);
// 进度里程碑
const milestones = [20, 40, 60, 80, 100];
// 计算已完成任务数
const completedCount = computed(() => {
return tasks.value.filter(task => task.completed).length;
});
// 遮罩层触摸处理
let maskTouchStartTime = 0;
const handleMaskTouchStart = (e) => {
maskTouchStartTime = Date.now();
e.preventDefault();
e.stopPropagation();
};
const handleMaskTouchEnd = () => {
const touchDuration = Date.now() - maskTouchStartTime;
// 快速点击小于300ms才关闭弹窗
if (touchDuration < 300) {
emit('close');
}
};
// 关闭按钮触摸处理
let closeTouchStartTime = 0;
let closeTouchLocked = false;
const handleCloseTouchStart = (e) => {
closeTouchStartTime = Date.now();
closeTouchLocked = false;
};
const handleCloseTouchEnd = (e) => {
if (closeTouchLocked) return;
closeTouchLocked = true;
const touchDuration = Date.now() - closeTouchStartTime;
// 快速点击小于300ms才关闭弹窗
if (touchDuration < 300) {
e.preventDefault();
e.stopPropagation();
emit('close');
}
};
const handleCloseClick = (e) => {
e.preventDefault();
e.stopPropagation();
emit('close');
};
</script>
<style scoped>
.modal-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1;
}
.modal-container {
position: relative;
width: 580rpx;
max-height: 85vh;
border-radius: 40rpx;
overflow: hidden;
z-index: 2;
}
.modal-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
object-fit: cover;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
}
.close-button {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
cursor: pointer;
}
.close-icon {
font-size: 50rpx;
color: #e6e6e6;
line-height: 1;
font-weight: 300;
}
.modal-content {
position: relative;
z-index: 2;
padding: 60rpx 40rpx 40rpx;
display: flex;
flex-direction: column;
}
.modal-title {
font-size: 40rpx;
font-weight: bold;
color: #e6e6e6;
text-align: center;
margin-bottom: 40rpx;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.task-list {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 40rpx;
}
.task-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
gap: 20rpx;
}
.task-name {
flex: 1;
font-size: 28rpx;
color: #e6e6e6;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.task-reward {
font-size: 32rpx;
font-weight: bold;
color: #FFB800;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
margin-right: 10rpx;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.task-status-btn {
min-width: 140rpx;
height: 50rpx;
border-radius: 25rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 0 20rpx;
background: rgba(255, 255, 255, 0.3);
}
.task-status-btn.completed {
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
}
.status-text {
font-size: 24rpx;
min-height: 30rpx;
color: #e6e6e6;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.check-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.progress-section {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20rpx;
}
.progress-bar {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
padding: 0 10rpx;
}
.progress-bar::before {
content: '';
position: absolute;
left: 10rpx;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
height: 4rpx;
background: rgba(255, 255, 255, 0.3);
z-index: 0;
}
.progress-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
position: relative;
z-index: 1;
}
.node-circle {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
border: 3rpx solid rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
}
.progress-node.active .node-circle {
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-color: #e6e6e6;
box-shadow: 0 0 10rpx rgba(240, 131, 153, 0.6);
}
.node-label {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.6);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
.progress-node.active .node-label {
color: #e6e6e6;
font-weight: bold;
}
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.scale-enter-active,
.scale-leave-active {
transition: all 0.3s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style>