feat: add medal popup

This commit is contained in:
Tim
2025-08-09 17:18:31 +08:00
parent 57c0aa5899
commit cdc35878a2
3 changed files with 160 additions and 3 deletions

View File

@@ -6,24 +6,36 @@
text="建站送奶茶活动火热进行中,快来参与吧!"
@close="closeMilkTeaPopup"
/>
<MedalPopup
:visible="showMedalPopup"
:medals="newMedals"
@close="closeMedalPopup"
/>
</div>
</template>
<script>
import ActivityPopup from '~/components/ActivityPopup.vue'
import MedalPopup from '~/components/MedalPopup.vue'
import { API_BASE_URL } from '~/main'
import { authState } from '~/utils/auth'
export default {
name: 'GlobalPopups',
components: { ActivityPopup },
components: { ActivityPopup, MedalPopup },
data () {
return {
showMilkTeaPopup: false,
milkTeaIcon: ''
milkTeaIcon: '',
showMedalPopup: false,
newMedals: []
}
},
async mounted () {
await this.checkMilkTeaActivity()
if (!this.showMilkTeaPopup) {
await this.checkNewMedals()
}
},
methods: {
async checkMilkTeaActivity () {
@@ -47,7 +59,34 @@ export default {
if (!process.client) return
localStorage.setItem('milkTeaActivityPopupShown', 'true')
this.showMilkTeaPopup = false
this.checkNewMedals()
},
async checkNewMedals () {
if (!process.client) return
if (!authState.loggedIn || !authState.userId) return
try {
const res = await fetch(`${API_BASE_URL}/api/medals?userId=${authState.userId}`)
if (res.ok) {
const medals = await res.json()
const seen = JSON.parse(localStorage.getItem('seenMedals') || '[]')
const m = medals.filter(i => i.completed && !seen.includes(i.type))
if (m.length > 0) {
this.newMedals = m
this.showMedalPopup = true
}
}
} catch (e) {
// ignore errors
}
},
closeMedalPopup () {
if (!process.client) return
const seen = new Set(JSON.parse(localStorage.getItem('seenMedals') || '[]'))
this.newMedals.forEach(m => seen.add(m.type))
localStorage.setItem('seenMedals', JSON.stringify([...seen]))
this.showMedalPopup = false
}
}
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<BasePopup :visible="visible" @close="close">
<div class="medal-popup">
<div class="medal-popup-title">恭喜你获得以下勋章</div>
<div class="medal-popup-list">
<div v-for="medal in medals" :key="medal.type" class="medal-popup-item">
<img :src="medal.icon" :alt="medal.title" class="medal-popup-item-icon" />
<div class="medal-popup-item-title">{{ medal.title }}</div>
</div>
</div>
<div class="medal-popup-actions">
<div class="medal-popup-button" @click="gotoMedals">去看看</div>
<div class="medal-popup-close" @click="close">知道了</div>
</div>
</div>
</BasePopup>
</template>
<script>
import BasePopup from '~/components/BasePopup.vue'
import { useRouter } from 'vue-router'
import { authState } from '~/utils/auth'
export default {
name: 'MedalPopup',
components: { BasePopup },
props: {
visible: { type: Boolean, default: false },
medals: { type: Array, default: () => [] }
},
emits: ['close'],
setup (props, { emit }) {
const router = useRouter()
const gotoMedals = () => {
emit('close')
if (authState.username) {
router.push(`/users/${authState.username}?tab=achievements`)
} else {
router.push('/')
}
}
const close = () => emit('close')
return { gotoMedals, close }
}
}
</script>
<style scoped>
.medal-popup {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
min-width: 200px;
}
.medal-popup-title {
font-size: 18px;
font-weight: bold;
}
.medal-popup-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.medal-popup-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.medal-popup-item-icon {
width: 60px;
height: 60px;
object-fit: contain;
}
.medal-popup-actions {
margin-top: 10px;
display: flex;
flex-direction: row;
gap: 20px;
}
.medal-popup-button {
background-color: var(--primary-color);
color: #fff;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
}
.medal-popup-button:hover {
background-color: var(--primary-color-hover);
}
.medal-popup-close {
cursor: pointer;
color: var(--primary-color);
display: flex;
align-items: center;
}
.medal-popup-close:hover {
text-decoration: underline;
}
</style>

View File

@@ -288,7 +288,11 @@ export default {
const subscribed = ref(false)
const isLoading = ref(true)
const tabLoading = ref(false)
const selectedTab = ref('summary')
const selectedTab = ref(
['summary', 'timeline', 'following', 'achievements'].includes(route.query.tab)
? route.query.tab
: 'summary'
)
const followTab = ref('followers')
const levelInfo = computed(() => {
@@ -473,6 +477,7 @@ export default {
onMounted(init)
watch(selectedTab, async val => {
router.replace({ query: { ...route.query, tab: val } })
if (val === 'timeline' && timelineItems.value.length === 0) {
await loadTimeline()
} else if (val === 'following' && followers.value.length === 0 && followings.value.length === 0) {