|
|
@ -1,30 +1,14 @@ |
|
|
<script setup lang="ts"> |
|
|
<script setup lang="ts"> |
|
|
import { ref, computed, onMounted } from 'vue' |
|
|
import { ref, computed, onMounted, defineComponent, type PropType, h } from 'vue' |
|
|
import { useRouter } from 'vue-router' |
|
|
import { useRouter } from 'vue-router' |
|
|
import { dailyMissionApi, type DailyMission, DailyMissionStatus, RewardType } from '@/api/dailyMission' |
|
|
import { dailyMissionApi, type DailyMission, DailyMissionStatus, RewardType, MissionType, MissionDifficulty, ProgressTargetType } from '@/api/dailyMission' |
|
|
import Particles from '@/components/Particles/Particles.vue' |
|
|
import Particles from '@/components/Particles/Particles.vue' |
|
|
import MissionCarousel from '@/components/MissionCarousel/MissionCarousel.vue' |
|
|
import ElectricBorder from '@/components/ElectricBorder/ElectricBorder.vue' |
|
|
import Carousel from '@/components/Carousel/Carousel.vue' |
|
|
import collectionIcon from '@/assets/images/collection.svg?raw' |
|
|
import type { CarouselItem } from '@/components/Carousel/Carousel.vue' |
|
|
import huntingNormalIcon from '@/assets/images/hunting.svg?raw' |
|
|
|
|
|
|
|
|
const router = useRouter() |
|
|
const router = useRouter() |
|
|
|
|
|
|
|
|
const carouselItems: CarouselItem[] = [ |
|
|
|
|
|
{ |
|
|
|
|
|
title: "Custom Item", |
|
|
|
|
|
description: "A custom carousel item.", |
|
|
|
|
|
id: 1, |
|
|
|
|
|
icon: "circle", |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
title: "Another Item", |
|
|
|
|
|
description: "Another carousel item.", |
|
|
|
|
|
id: 2, |
|
|
|
|
|
icon: "layers", |
|
|
|
|
|
}, |
|
|
|
|
|
// Add more items as needed |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
const missions = ref<DailyMission[]>([]) |
|
|
const missions = ref<DailyMission[]>([]) |
|
|
const todayStats = ref({ claimed: 0, total: 0 }) |
|
|
const todayStats = ref({ claimed: 0, total: 0 }) |
|
|
const loading = ref(false) |
|
|
const loading = ref(false) |
|
|
@ -39,71 +23,6 @@ const showMsg = (text: string, type: 'success' | 'error' = 'success') => { |
|
|
}, 2000) |
|
|
}, 2000) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const getStatusText = (status: DailyMissionStatus) => { |
|
|
|
|
|
switch (status) { |
|
|
|
|
|
case DailyMissionStatus.Pending: |
|
|
|
|
|
return '待接取' |
|
|
|
|
|
case DailyMissionStatus.InProgress: |
|
|
|
|
|
return '进行中' |
|
|
|
|
|
case DailyMissionStatus.Completed: |
|
|
|
|
|
return '待领取' |
|
|
|
|
|
case DailyMissionStatus.Claimed: |
|
|
|
|
|
return '已领取' |
|
|
|
|
|
default: |
|
|
|
|
|
return '未知' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getStatusClass = (status: DailyMissionStatus) => { |
|
|
|
|
|
switch (status) { |
|
|
|
|
|
case DailyMissionStatus.Pending: |
|
|
|
|
|
return 'status-pending' |
|
|
|
|
|
case DailyMissionStatus.InProgress: |
|
|
|
|
|
return 'status-progress' |
|
|
|
|
|
case DailyMissionStatus.Completed: |
|
|
|
|
|
return 'status-completed' |
|
|
|
|
|
case DailyMissionStatus.Claimed: |
|
|
|
|
|
return 'status-claimed' |
|
|
|
|
|
default: |
|
|
|
|
|
return '' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getRewardIcon = (type: RewardType) => { |
|
|
|
|
|
switch (type) { |
|
|
|
|
|
case RewardType.Money: |
|
|
|
|
|
return '💰' |
|
|
|
|
|
case RewardType.Pill: |
|
|
|
|
|
return '💊' |
|
|
|
|
|
case RewardType.Equipment: |
|
|
|
|
|
return '⚔️' |
|
|
|
|
|
default: |
|
|
|
|
|
return '🎁' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const formatRemainingTime = (endTime?: string) => { |
|
|
|
|
|
if (!endTime) return '' |
|
|
|
|
|
const end = new Date(endTime) |
|
|
|
|
|
const now = new Date() |
|
|
|
|
|
const diff = end.getTime() - now.getTime() |
|
|
|
|
|
if (diff <= 0) return '已完成' |
|
|
|
|
|
const minutes = Math.floor(diff / 60000) |
|
|
|
|
|
const seconds = Math.floor((diff % 60000) / 1000) |
|
|
|
|
|
return `${minutes}分${seconds}秒` |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const remainingTimes = ref<Record<number, string>>({}) |
|
|
|
|
|
let timer: number | null = null |
|
|
|
|
|
|
|
|
|
|
|
const updateRemainingTimes = () => { |
|
|
|
|
|
missions.value.forEach(mission => { |
|
|
|
|
|
if (mission.status === DailyMissionStatus.InProgress) { |
|
|
|
|
|
remainingTimes.value[mission.id] = formatRemainingTime(mission.endTime) |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const canAccept = (mission: DailyMission) => mission.status === DailyMissionStatus.Pending |
|
|
const canAccept = (mission: DailyMission) => mission.status === DailyMissionStatus.Pending |
|
|
const canClaim = (mission: DailyMission) => mission.status === DailyMissionStatus.Completed |
|
|
const canClaim = (mission: DailyMission) => mission.status === DailyMissionStatus.Completed |
|
|
|
|
|
|
|
|
@ -118,7 +37,6 @@ const loadMissions = async () => { |
|
|
total: data[0].todayTotalCount |
|
|
total: data[0].todayTotalCount |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
updateRemainingTimes() |
|
|
|
|
|
} catch (error: any) { |
|
|
} catch (error: any) { |
|
|
showMsg(error?.message || '加载任务失败', 'error') |
|
|
showMsg(error?.message || '加载任务失败', 'error') |
|
|
} finally { |
|
|
} finally { |
|
|
@ -152,30 +70,161 @@ const handleGoBack = () => { |
|
|
router.push('/game') |
|
|
router.push('/game') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const handleMissionSelect = (mission: DailyMission) => { |
|
|
|
|
|
selectedMission.value = mission |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
loadMissions() |
|
|
loadMissions() |
|
|
timer = window.setInterval(updateRemainingTimes, 1000) |
|
|
|
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
const selectedMission = ref<DailyMission | null>(null) |
|
|
const activeMissions = computed(() => missions.value.filter(m => !m.isFromYesterday)) |
|
|
|
|
|
const hasAnyMissions = computed(() => activeMissions.value.length > 0) |
|
|
|
|
|
|
|
|
const yesterdayMissions = computed(() => missions.value.filter(m => m.isFromYesterday && m.status === DailyMissionStatus.Completed)) |
|
|
const getMissionTypeClass = (type: MissionType, difficulty: MissionDifficulty) => { |
|
|
const todayMissions = computed(() => missions.value.filter(m => !m.isFromYesterday)) |
|
|
if (type === MissionType.Collection) { |
|
|
|
|
|
return 'mission-collection' |
|
|
|
|
|
} |
|
|
|
|
|
if (difficulty === MissionDifficulty.Purgatory) { |
|
|
|
|
|
return 'mission-hunting-purgatory' |
|
|
|
|
|
} |
|
|
|
|
|
if (difficulty === MissionDifficulty.Hard) { |
|
|
|
|
|
return 'mission-hunting-hard' |
|
|
|
|
|
} |
|
|
|
|
|
return 'mission-hunting-normal' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const hasAnyMissions = computed(() => yesterdayMissions.value.length > 0 || todayMissions.value.length > 0) |
|
|
const getStatusText = (status: DailyMissionStatus) => { |
|
|
|
|
|
switch (status) { |
|
|
|
|
|
case DailyMissionStatus.Pending: return '待接取' |
|
|
|
|
|
case DailyMissionStatus.InProgress: return '进行中' |
|
|
|
|
|
case DailyMissionStatus.Completed: return '待领取' |
|
|
|
|
|
case DailyMissionStatus.Claimed: return '已领取' |
|
|
|
|
|
default: return '未知' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getStatusClass = (status: DailyMissionStatus) => { |
|
|
|
|
|
switch (status) { |
|
|
|
|
|
case DailyMissionStatus.Pending: return 'status-pending' |
|
|
|
|
|
case DailyMissionStatus.InProgress: return 'status-progress' |
|
|
|
|
|
case DailyMissionStatus.Completed: return 'status-completed' |
|
|
|
|
|
case DailyMissionStatus.Claimed: return 'status-claimed' |
|
|
|
|
|
default: return '' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getDifficultyLabel = (difficulty: MissionDifficulty) => { |
|
|
|
|
|
switch (difficulty) { |
|
|
|
|
|
case MissionDifficulty.Normal: return '普通' |
|
|
|
|
|
case MissionDifficulty.Hard: return '困难' |
|
|
|
|
|
case MissionDifficulty.Purgatory: return '炼狱' |
|
|
|
|
|
default: return '' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getDifficultyClass = (difficulty: MissionDifficulty) => { |
|
|
|
|
|
switch (difficulty) { |
|
|
|
|
|
case MissionDifficulty.Normal: return 'difficulty-normal' |
|
|
|
|
|
case MissionDifficulty.Hard: return 'difficulty-hard' |
|
|
|
|
|
case MissionDifficulty.Purgatory: return 'difficulty-purgatory' |
|
|
|
|
|
default: return '' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getRewardIcon = (type: RewardType) => { |
|
|
|
|
|
switch (type) { |
|
|
|
|
|
case RewardType.Money: return '💰' |
|
|
|
|
|
case RewardType.Pill: return '💊' |
|
|
|
|
|
case RewardType.Equipment: return '⚔️' |
|
|
|
|
|
default: return '🎁' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getProgressText = (mission: DailyMission) => { |
|
|
|
|
|
if (!mission.progresses || mission.progresses.length === 0) return '' |
|
|
|
|
|
const progress = mission.progresses[0] |
|
|
|
|
|
const targetType = progress.targetType === ProgressTargetType.CollectItem ? '收集' : '狩猎' |
|
|
|
|
|
const targetName = progress.targetItemName || (progress.targetType === ProgressTargetType.KillMonster ? '怪物' : '物品') |
|
|
|
|
|
return `${targetType} ${progress.currentCount}/${progress.targetCount} ${targetName}` |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getMissionIcon = (type: MissionType, difficulty: MissionDifficulty) => { |
|
|
|
|
|
if (type === MissionType.Collection) { |
|
|
|
|
|
return collectionIcon |
|
|
|
|
|
} |
|
|
|
|
|
// For hunting, use different filters based on difficulty |
|
|
|
|
|
return huntingNormalIcon |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const getMissionIconFilter = (type: MissionType, difficulty: MissionDifficulty) => { |
|
|
|
|
|
if (type === MissionType.Collection) { |
|
|
|
|
|
return 'none' |
|
|
|
|
|
} |
|
|
|
|
|
switch (difficulty) { |
|
|
|
|
|
case MissionDifficulty.Purgatory: |
|
|
|
|
|
return 'hue-rotate(-10deg) saturate(1.5) brightness(0.8)' |
|
|
|
|
|
case MissionDifficulty.Hard: |
|
|
|
|
|
return 'hue-rotate(-30deg) saturate(1.2)' |
|
|
|
|
|
default: |
|
|
|
|
|
return 'none' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const MissionCardContent = defineComponent({ |
|
|
|
|
|
name: 'MissionCardContent', |
|
|
|
|
|
props: { |
|
|
|
|
|
mission: { |
|
|
|
|
|
type: Object as PropType<DailyMission>, |
|
|
|
|
|
required: true |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
emits: ['accept', 'claim'], |
|
|
|
|
|
setup(props, { emit }) { |
|
|
|
|
|
return () => h('div', { class: 'mission-card-content' }, [ |
|
|
|
|
|
h('div', { class: 'card-header' }, [ |
|
|
|
|
|
h('div', { class: 'card-title-row' }, [ |
|
|
|
|
|
h('div', { class: 'card-icon', style: `filter: ${getMissionIconFilter(props.mission.missionType, props.mission.difficulty)}`, innerHTML: getMissionIcon(props.mission.missionType, props.mission.difficulty) }), |
|
|
|
|
|
h('span', { class: 'card-title' }, props.mission.missionTitle), |
|
|
|
|
|
]), |
|
|
|
|
|
h('span', { class: ['card-status', getStatusClass(props.mission.status)] }, getStatusText(props.mission.status)), |
|
|
|
|
|
]), |
|
|
|
|
|
h('div', { class: 'card-desc' }, props.mission.missionDescription), |
|
|
|
|
|
h('div', { class: 'card-difficulty' }, [ |
|
|
|
|
|
h('span', { class: ['difficulty-badge', getDifficultyClass(props.mission.difficulty)] }, getDifficultyLabel(props.mission.difficulty)), |
|
|
|
|
|
]), |
|
|
|
|
|
props.mission.progresses && props.mission.progresses.length > 0 && props.mission.status !== DailyMissionStatus.Pending |
|
|
|
|
|
? h('div', { class: 'card-progress' }, [ |
|
|
|
|
|
h('span', { class: 'progress-label' }, '进度:'), |
|
|
|
|
|
h('span', { class: 'progress-value' }, getProgressText(props.mission)), |
|
|
|
|
|
]) |
|
|
|
|
|
: null, |
|
|
|
|
|
h('div', { class: 'card-rewards' }, [ |
|
|
|
|
|
h('span', { class: 'reward-item exp-reward' }, `✨${props.mission.expReward}`), |
|
|
|
|
|
...props.mission.rewards.slice(0, 2).map((reward: any) => |
|
|
|
|
|
h('span', { class: 'reward-item' }, `${getRewardIcon(reward.rewardType)}${reward.count}`) |
|
|
|
|
|
), |
|
|
|
|
|
props.mission.rewards.length > 2 |
|
|
|
|
|
? h('span', { class: 'reward-more' }, `+${props.mission.rewards.length - 2}`) |
|
|
|
|
|
: null, |
|
|
|
|
|
]), |
|
|
|
|
|
h('div', { class: 'card-actions' }, [ |
|
|
|
|
|
canAccept(props.mission) |
|
|
|
|
|
? h('button', { class: 'card-btn accept-btn', onClick: () => emit('accept') }, '接取') |
|
|
|
|
|
: canClaim(props.mission) |
|
|
|
|
|
? h('button', { class: 'card-btn claim-btn', onClick: () => emit('claim') }, '领取') |
|
|
|
|
|
: props.mission.status === DailyMissionStatus.InProgress |
|
|
|
|
|
? h('button', { class: 'card-btn disabled-btn', disabled: true }, '进行中') |
|
|
|
|
|
: props.mission.status === DailyMissionStatus.Claimed |
|
|
|
|
|
? h('button', { class: 'card-btn disabled-btn', disabled: true }, '已完成') |
|
|
|
|
|
: null, |
|
|
|
|
|
]), |
|
|
|
|
|
]) |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
<template> |
|
|
<template> |
|
|
<div class="daily-mission-page"> |
|
|
<div class="daily-mission-page"> |
|
|
|
|
|
|
|
|
<Particles :particle-count="50" :particle-colors="['#ffffff', '#aaaaaa']" class="particles-bg" /> |
|
|
<Particles :particle-count="50" :particle-colors="['#ffffff', '#aaaaaa']" class="particles-bg" /> |
|
|
|
|
|
|
|
|
<div class="page-container"> |
|
|
<div class="page-container"> |
|
|
|
|
|
|
|
|
<div class="page-header"> |
|
|
<div class="page-header"> |
|
|
<span class="back-btn" @click="handleGoBack">← 返回</span> |
|
|
<span class="back-btn" @click="handleGoBack">← 返回</span> |
|
|
<span class="title">每日任务</span> |
|
|
<span class="title">每日任务</span> |
|
|
@ -191,109 +240,44 @@ const hasAnyMissions = computed(() => yesterdayMissions.value.length > 0 || toda |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-else class="missions-content"> |
|
|
<div v-else class="missions-content"> |
|
|
|
|
|
<div v-if="activeMissions.length > 0" class="stats-bar"> |
|
|
<!-- <div v-if="yesterdayMissions.length > 0" class="mission-section yesterday-section"> |
|
|
<span class="stats-text">今日进度: {{ todayStats.claimed }}/{{ todayStats.total }}</span> |
|
|
<div class="section-header"> |
|
|
|
|
|
<span class="section-title">昨日任务</span> |
|
|
|
|
|
<span class="section-badge highlight">待领取</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="carousel-wrapper"> |
|
|
|
|
|
<MissionCarousel :items="yesterdayMissions" :base-width="340" :card-height="220" |
|
|
|
|
|
@select="handleMissionSelect"> |
|
|
|
|
|
<template #default="{ item }"> |
|
|
|
|
|
<div class="mission-card-content"> |
|
|
|
|
|
<div class="card-header"> |
|
|
|
|
|
<span class="card-title">{{ item.missionTitle }}</span> |
|
|
|
|
|
<span class="card-status" :class="getStatusClass(item.status)"> |
|
|
|
|
|
{{ getStatusText(item.status) }} |
|
|
|
|
|
</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="card-desc">{{ item.missionDescription }}</div> |
|
|
|
|
|
<div class="card-time"> |
|
|
|
|
|
<span v-if="item.status === DailyMissionStatus.InProgress"> |
|
|
|
|
|
剩余: {{ remainingTimes[item.id] || formatRemainingTime(item.endTime) }} |
|
|
|
|
|
</span> |
|
|
|
|
|
<span v-else> |
|
|
|
|
|
{{ item.spendTimeMinutes }}分钟 |
|
|
|
|
|
</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="card-rewards"> |
|
|
|
|
|
<span class="reward-item exp-reward">✨{{ item.expReward }}</span> |
|
|
|
|
|
<span v-for="(reward, idx) in item.rewards.slice(0, 2)" :key="idx" class="reward-item"> |
|
|
|
|
|
{{ getRewardIcon(reward.rewardType) }}{{ reward.count }} |
|
|
|
|
|
</span> |
|
|
|
|
|
<span v-if="item.rewards.length > 2" class="reward-more">+{{ item.rewards.length - 2 }}</span> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
<div class="card-actions"> |
|
|
|
|
|
<button v-if="canAccept(item)" class="card-btn accept-btn" @click.stop="handleAccept(item)"> |
|
|
|
|
|
接取 |
|
|
|
|
|
</button> |
|
|
|
|
|
<button v-if="canClaim(item)" class="card-btn claim-btn" @click.stop="handleClaim(item)"> |
|
|
|
|
|
领取 |
|
|
|
|
|
</button> |
|
|
|
|
|
<button v-if="item.status === DailyMissionStatus.InProgress" class="card-btn disabled-btn" disabled> |
|
|
|
|
|
挂机中 |
|
|
|
|
|
</button> |
|
|
|
|
|
<button v-if="item.status === DailyMissionStatus.Claimed" class="card-btn disabled-btn" disabled> |
|
|
|
|
|
已完成 |
|
|
|
|
|
</button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
</MissionCarousel> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> --> |
|
|
|
|
|
|
|
|
|
|
|
<div v-if="todayMissions.length > 0" class="mission-section"> |
|
|
<div v-if="hasAnyMissions" class="mission-list"> |
|
|
<div class="section-header"> |
|
|
<template v-for="mission in activeMissions" :key="mission.id"> |
|
|
<span class="section-title">今日任务</span> |
|
|
<ElectricBorder |
|
|
<span class="section-badge">{{ todayStats.claimed }}/{{ todayStats.total }}</span> |
|
|
v-if="mission.difficulty === MissionDifficulty.Purgatory" |
|
|
</div> |
|
|
color="#dc2626" |
|
|
<div class="carousel-wrapper"> |
|
|
:speed="1.2" |
|
|
<MissionCarousel :items="todayMissions" :base-width="340" :card-height="180" @select="handleMissionSelect" |
|
|
:chaos="1.5" |
|
|
:round="true" :loop="true"> |
|
|
:thickness="2" |
|
|
<template #default="{ item }"> |
|
|
class="mission-card purgatory-card" |
|
|
<div class="mission-card-content"> |
|
|
> |
|
|
<div class="card-header"> |
|
|
<div class="card-inner"> |
|
|
<span class="card-title">{{ item.missionTitle }}</span> |
|
|
<component |
|
|
<span class="card-status" :class="getStatusClass(item.status)"> |
|
|
:is="MissionCardContent" |
|
|
{{ getStatusText(item.status) }} |
|
|
:mission="mission" |
|
|
</span> |
|
|
@accept="handleAccept(mission)" |
|
|
|
|
|
@claim="handleClaim(mission)" |
|
|
|
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
<div class="card-desc">{{ item.missionDescription }}</div> |
|
|
</ElectricBorder> |
|
|
<div class="card-time"> |
|
|
<div |
|
|
<span v-if="item.status === DailyMissionStatus.InProgress"> |
|
|
v-else |
|
|
剩余: {{ remainingTimes[item.id] || formatRemainingTime(item.endTime) }} |
|
|
class="mission-card" |
|
|
</span> |
|
|
:class="getMissionTypeClass(mission.missionType, mission.difficulty)" |
|
|
<span v-else> |
|
|
> |
|
|
{{ item.spendTimeMinutes }}分钟 |
|
|
<div class="card-inner"> |
|
|
</span> |
|
|
<component |
|
|
</div> |
|
|
:is="MissionCardContent" |
|
|
<div class="card-rewards"> |
|
|
:mission="mission" |
|
|
<span class="reward-item exp-reward">✨{{ item.expReward }}</span> |
|
|
@accept="handleAccept(mission)" |
|
|
<span v-for="(reward, idx) in item.rewards.slice(0, 2)" :key="idx" class="reward-item"> |
|
|
@claim="handleClaim(mission)" |
|
|
{{ getRewardIcon(reward.rewardType) }}{{ reward.count }} |
|
|
/> |
|
|
</span> |
|
|
|
|
|
<span v-if="item.rewards.length > 2" class="reward-more">+{{ item.rewards.length - 2 }}</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="card-actions"> |
|
|
|
|
|
<button v-if="canAccept(item)" class="card-btn accept-btn" @click.stop="handleAccept(item)"> |
|
|
|
|
|
接取 |
|
|
|
|
|
</button> |
|
|
|
|
|
<button v-if="canClaim(item)" class="card-btn claim-btn" @click.stop="handleClaim(item)"> |
|
|
|
|
|
领取 |
|
|
|
|
|
</button> |
|
|
|
|
|
<button v-if="item.status === DailyMissionStatus.InProgress" class="card-btn disabled-btn" disabled> |
|
|
|
|
|
挂机中 |
|
|
|
|
|
</button> |
|
|
|
|
|
<button v-if="item.status === DailyMissionStatus.Claimed" class="card-btn disabled-btn" disabled> |
|
|
|
|
|
已完成 |
|
|
|
|
|
</button> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
</MissionCarousel> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="!hasAnyMissions" class="empty-state"> |
|
|
<div v-if="!hasAnyMissions" class="empty-state"> |
|
|
@ -377,7 +361,6 @@ const hasAnyMissions = computed(() => yesterdayMissions.value.length > 0 || toda |
|
|
opacity: 0; |
|
|
opacity: 0; |
|
|
transform: translateX(-50%) translateY(-10px); |
|
|
transform: translateX(-50%) translateY(-10px); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
to { |
|
|
to { |
|
|
opacity: 1; |
|
|
opacity: 1; |
|
|
transform: translateX(-50%) translateY(0); |
|
|
transform: translateX(-50%) translateY(0); |
|
|
@ -393,124 +376,185 @@ const hasAnyMissions = computed(() => yesterdayMissions.value.length > 0 || toda |
|
|
.missions-content { |
|
|
.missions-content { |
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
flex-direction: column; |
|
|
gap: 24px; |
|
|
gap: 16px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.mission-section { |
|
|
.stats-bar { |
|
|
|
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
padding: 12px 16px; |
|
|
|
|
|
text-align: center; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.stats-text { |
|
|
|
|
|
color: #888888; |
|
|
|
|
|
font-size: 0.9rem; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mission-list { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 12px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mission-card { |
|
|
background: rgba(255, 255, 255, 0.02); |
|
|
background: rgba(255, 255, 255, 0.02); |
|
|
border: 1px solid rgba(255, 255, 255, 0.08); |
|
|
border: 1px solid; |
|
|
border-radius: 12px; |
|
|
border-radius: 12px; |
|
|
padding: 16px; |
|
|
overflow: hidden; |
|
|
|
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.yesterday-section { |
|
|
.mission-card:hover { |
|
|
background: rgba(255, 68, 68, 0.05); |
|
|
transform: translateY(-2px); |
|
|
border-color: rgba(255, 68, 68, 0.2); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.section-header { |
|
|
.purgatory-card { |
|
|
display: flex; |
|
|
border: none; |
|
|
justify-content: space-between; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
margin-bottom: 16px; |
|
|
|
|
|
padding-bottom: 12px; |
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.section-title { |
|
|
.card-inner { |
|
|
color: #ffffff; |
|
|
padding: 16px; |
|
|
font-size: 1rem; |
|
|
|
|
|
font-weight: 500; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.section-badge { |
|
|
.mission-collection { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
border-color: rgba(34, 197, 94, 0.3); |
|
|
color: #888888; |
|
|
|
|
|
padding: 4px 12px; |
|
|
|
|
|
border-radius: 12px; |
|
|
|
|
|
font-size: 0.8rem; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.section-badge.highlight { |
|
|
.mission-hunting-normal { |
|
|
background: rgba(255, 68, 68, 0.2); |
|
|
border-color: rgba(239, 68, 68, 0.3); |
|
|
color: #ff4444; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.carousel-wrapper { |
|
|
.mission-hunting-hard { |
|
|
display: flex; |
|
|
border-color: rgba(249, 115, 22, 0.4); |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mission-hunting-purgatory { |
|
|
|
|
|
border-color: rgba(220, 38, 38, 0.5); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.mission-card-content { |
|
|
.mission-card-content { |
|
|
width: 100%; |
|
|
|
|
|
height: 100%; |
|
|
|
|
|
padding: 12px; |
|
|
|
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
flex-direction: column; |
|
|
box-sizing: border-box; |
|
|
gap: 10px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-header { |
|
|
.card-header { |
|
|
display: flex; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
margin-bottom: 6px; |
|
|
padding-bottom: 10px; |
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.card-title-row { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
min-width: 0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.card-icon { |
|
|
|
|
|
width: 24px; |
|
|
|
|
|
height: 24px; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.card-icon :deep(svg) { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
height: 100%; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-title { |
|
|
.card-title { |
|
|
color: #ffffff; |
|
|
color: #ffffff; |
|
|
font-size: 0.9rem; |
|
|
font-size: 1rem; |
|
|
font-weight: 600; |
|
|
font-weight: 600; |
|
|
flex: 1; |
|
|
|
|
|
overflow: hidden; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
text-overflow: ellipsis; |
|
|
white-space: nowrap; |
|
|
white-space: nowrap; |
|
|
margin-right: 8px; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-status { |
|
|
.card-status { |
|
|
padding: 2px 8px; |
|
|
padding: 2px 8px; |
|
|
border-radius: 8px; |
|
|
border-radius: 8px; |
|
|
font-size: 0.65rem; |
|
|
font-size: 0.7rem; |
|
|
font-weight: 500; |
|
|
font-weight: 500; |
|
|
white-space: nowrap; |
|
|
white-space: nowrap; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-desc { |
|
|
.card-desc { |
|
|
color: #888888; |
|
|
color: #888888; |
|
|
font-size: 0.7rem; |
|
|
font-size: 0.8rem; |
|
|
line-height: 1.3; |
|
|
line-height: 1.4; |
|
|
margin-bottom: 6px; |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
overflow: hidden; |
|
|
|
|
|
display: -webkit-box; |
|
|
|
|
|
-webkit-line-clamp: 2; |
|
|
|
|
|
-webkit-box-orient: vertical; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-time { |
|
|
.card-difficulty { |
|
|
color: #ff8844; |
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.difficulty-badge { |
|
|
|
|
|
padding: 2px 8px; |
|
|
|
|
|
border-radius: 4px; |
|
|
font-size: 0.7rem; |
|
|
font-size: 0.7rem; |
|
|
margin-bottom: 8px; |
|
|
font-weight: 500; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.difficulty-normal { |
|
|
|
|
|
background: rgba(100, 100, 100, 0.3); |
|
|
|
|
|
color: #888888; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.difficulty-hard { |
|
|
|
|
|
background: rgba(249, 115, 22, 0.2); |
|
|
|
|
|
color: #f97316; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.difficulty-purgatory { |
|
|
|
|
|
background: rgba(220, 38, 38, 0.3); |
|
|
|
|
|
color: #fca5a5; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.card-progress { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 6px; |
|
|
|
|
|
background: rgba(255, 255, 255, 0.03); |
|
|
|
|
|
padding: 8px 10px; |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.progress-label { |
|
|
|
|
|
color: #666666; |
|
|
|
|
|
font-size: 0.75rem; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.progress-value { |
|
|
|
|
|
color: #22c55e; |
|
|
|
|
|
font-size: 0.8rem; |
|
|
|
|
|
font-weight: 500; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-rewards { |
|
|
.card-rewards { |
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
flex-wrap: wrap; |
|
|
gap: 3px; |
|
|
gap: 4px; |
|
|
margin-bottom: 8px; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-rewards .reward-item { |
|
|
.reward-item { |
|
|
background: rgba(255, 136, 68, 0.1); |
|
|
background: rgba(255, 136, 68, 0.1); |
|
|
border: 1px solid rgba(255, 136, 68, 0.2); |
|
|
border: 1px solid rgba(255, 136, 68, 0.2); |
|
|
color: #ff8844; |
|
|
color: #ff8844; |
|
|
padding: 1px 5px; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
border-radius: 4px; |
|
|
font-size: 0.65rem; |
|
|
font-size: 0.7rem; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-rewards .exp-reward { |
|
|
.exp-reward { |
|
|
background: rgba(255, 215, 0, 0.1); |
|
|
background: rgba(255, 215, 0, 0.1); |
|
|
border-color: rgba(255, 215, 0, 0.3); |
|
|
border-color: rgba(255, 215, 0, 0.3); |
|
|
color: #ffd700; |
|
|
color: #ffd700; |
|
|
@ -519,21 +563,22 @@ const hasAnyMissions = computed(() => yesterdayMissions.value.length > 0 || toda |
|
|
.reward-more { |
|
|
.reward-more { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
color: #888888; |
|
|
color: #888888; |
|
|
padding: 1px 5px; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
border-radius: 4px; |
|
|
font-size: 0.65rem; |
|
|
font-size: 0.7rem; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-actions { |
|
|
.card-actions { |
|
|
display: flex; |
|
|
display: flex; |
|
|
gap: 6px; |
|
|
gap: 8px; |
|
|
|
|
|
margin-top: 4px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.card-btn { |
|
|
.card-btn { |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
padding: 6px 10px; |
|
|
padding: 8px 12px; |
|
|
border-radius: 6px; |
|
|
border-radius: 6px; |
|
|
font-size: 0.75rem; |
|
|
font-size: 0.8rem; |
|
|
font-weight: 500; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
transition: all 0.2s ease; |
|
|
|