mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-17 01:20:46 +08:00
Merge pull request #266 from nagisa77/codex/add-popup-for-new-user-activity-guidance
Add milk-tea activity popup for new users
This commit is contained in:
@@ -14,23 +14,60 @@
|
|||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ActivityPopup
|
||||||
|
:visible="showMilkTeaPopup"
|
||||||
|
:icon="milkTeaIcon"
|
||||||
|
text="建站送奶茶活动火热进行中,快来参与吧!"
|
||||||
|
@close="closeMilkTeaPopup"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeaderComponent from './components/HeaderComponent.vue'
|
import HeaderComponent from './components/HeaderComponent.vue'
|
||||||
import MenuComponent from './components/MenuComponent.vue'
|
import MenuComponent from './components/MenuComponent.vue'
|
||||||
|
import ActivityPopup from './components/ActivityPopup.vue'
|
||||||
|
import { API_BASE_URL } from './main'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: { HeaderComponent, MenuComponent },
|
components: { HeaderComponent, MenuComponent, ActivityPopup },
|
||||||
data() {
|
data() {
|
||||||
return { menuVisible: window.innerWidth > 768 }
|
return {
|
||||||
|
menuVisible: window.innerWidth > 768,
|
||||||
|
showMilkTeaPopup: false,
|
||||||
|
milkTeaIcon: ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideMenu() {
|
hideMenu() {
|
||||||
return ['/login', '/signup', '/404', '/signup-reason', '/github-callback', '/twitter-callback', '/discord-callback', '/forgot-password'].includes(this.$route.path)
|
return ['/login', '/signup', '/404', '/signup-reason', '/github-callback', '/twitter-callback', '/discord-callback', '/forgot-password'].includes(this.$route.path)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.checkMilkTeaActivity()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async checkMilkTeaActivity() {
|
||||||
|
if (localStorage.getItem('milkTeaActivityPopupShown')) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||||
|
if (res.ok) {
|
||||||
|
const list = await res.json()
|
||||||
|
const a = list.find(i => i.type === 'MILK_TEA' && !i.ended)
|
||||||
|
if (a) {
|
||||||
|
this.milkTeaIcon = a.icon
|
||||||
|
this.showMilkTeaPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore network errors
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeMilkTeaPopup() {
|
||||||
|
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
||||||
|
this.showMilkTeaPopup = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
79
open-isle-cli/src/components/ActivityPopup.vue
Normal file
79
open-isle-cli/src/components/ActivityPopup.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<BasePopup :visible="visible" @close="close">
|
||||||
|
<div class="activity-popup">
|
||||||
|
<img v-if="icon" :src="icon" class="activity-popup-icon" />
|
||||||
|
<div class="activity-popup-text">{{ text }}</div>
|
||||||
|
<div class="activity-popup-actions">
|
||||||
|
<div class="activity-popup-button" @click="gotoActivity">立即前往</div>
|
||||||
|
<div class="activity-popup-close" @click="close">稍后再说</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BasePopup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BasePopup from './BasePopup.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ActivityPopup',
|
||||||
|
components: { BasePopup },
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
icon: String,
|
||||||
|
text: String
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
setup (props, { emit }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const gotoActivity = () => {
|
||||||
|
emit('close')
|
||||||
|
router.push('/activities')
|
||||||
|
}
|
||||||
|
const close = () => emit('close')
|
||||||
|
return { gotoActivity, close }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.activity-popup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-popup-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.activity-popup-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.activity-popup-button {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.activity-popup-button:hover {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
.activity-popup-close {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--primary-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.activity-popup-close:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
open-isle-cli/src/components/BasePopup.vue
Normal file
58
open-isle-cli/src/components/BasePopup.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="popup">
|
||||||
|
<div class="popup-overlay" @click="onOverlayClick"></div>
|
||||||
|
<div class="popup-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'BasePopup',
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
closeOnOverlay: { type: Boolean, default: true }
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
methods: {
|
||||||
|
onOverlayClick () {
|
||||||
|
if (this.closeOnOverlay) this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.popup-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
-webkit-backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
.popup-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,22 +17,28 @@
|
|||||||
<ProgressBar :value="info.level1Count" :max="50" />
|
<ProgressBar :value="info.level1Count" :max="50" />
|
||||||
<div class="status-text">当前 {{ info.level1Count }} / 50</div>
|
<div class="status-text">当前 {{ info.level1Count }} / 50</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="user" class="user-level">
|
<div v-if="isLoadingUser" class="loading-user">
|
||||||
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
|
<div class="user-level-text">加载当前等级中...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="user" class="user-level">
|
||||||
<LevelProgress :exp="user.experience" :current-level="user.currentLevel" :next-exp="user.nextLevelExp" />
|
<LevelProgress :exp="user.experience" :current-level="user.currentLevel" :next-exp="user.nextLevelExp" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="user-level">
|
||||||
|
<div class="user-level-text"><i class="fas fa-user-circle"></i> 请登录查看自身等级</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="user && user.currentLevel >= 1 && !info.ended" class="redeem-button" @click="openDialog">兑换</div>
|
<div v-if="user && user.currentLevel >= 1 && !info.ended" class="redeem-button" @click="openDialog">兑换</div>
|
||||||
<div v-else class="redeem-button disabled">兑换</div>
|
<div v-else class="redeem-button disabled">兑换</div>
|
||||||
<div v-if="dialogVisible" class="redeem-dialog">
|
<BasePopup :visible="dialogVisible" @close="closeDialog">
|
||||||
<div class="redeem-dialog-overlay" @click="closeDialog"></div>
|
<div class="redeem-dialog-content">
|
||||||
<div class="redeem-dialog-content">
|
<BaseInput textarea="" rows="5" v-model="contact" placeholder="联系方式 (手机号/Email/微信/instagram/telegram等, 务必注明来源)" />
|
||||||
<BaseInput textarea="" rows="5" v-model="contact" placeholder="联系方式 (手机号/Email/微信/instagram/telegram等, 务必注明来源)" />
|
<div class="redeem-actions">
|
||||||
<div class="redeem-actions">
|
<div class="redeem-submit-button" @click="submitRedeem" :disabled="loading">提交</div>
|
||||||
<div class="redeem-submit-button" @click="submitRedeem" :disabled="loading">提交</div>
|
<div class="redeem-cancel-button" @click="closeDialog">取消</div>
|
||||||
<div class="redeem-cancel-button" @click="closeDialog">取消</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BasePopup>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -40,24 +46,30 @@
|
|||||||
import ProgressBar from '../components/ProgressBar.vue'
|
import ProgressBar from '../components/ProgressBar.vue'
|
||||||
import LevelProgress from '../components/LevelProgress.vue'
|
import LevelProgress from '../components/LevelProgress.vue'
|
||||||
import BaseInput from './BaseInput.vue'
|
import BaseInput from './BaseInput.vue'
|
||||||
|
import BasePopup from './BasePopup.vue'
|
||||||
import { API_BASE_URL, toast } from '../main'
|
import { API_BASE_URL, toast } from '../main'
|
||||||
import { getToken, fetchCurrentUser } from '../utils/auth'
|
import { getToken, fetchCurrentUser } from '../utils/auth'
|
||||||
|
import { hatch } from 'ldrs'
|
||||||
|
hatch.register()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MilkTeaActivityComponent',
|
name: 'MilkTeaActivityComponent',
|
||||||
components: { ProgressBar, LevelProgress, BaseInput },
|
components: { ProgressBar, LevelProgress, BaseInput, BasePopup },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
info: { level1Count: 0, ended: false },
|
info: { level1Count: 0, ended: false },
|
||||||
user: null,
|
user: null,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
contact: '',
|
contact: '',
|
||||||
loading: false
|
loading: false,
|
||||||
|
isLoadingUser: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
await this.loadInfo()
|
await this.loadInfo()
|
||||||
|
this.isLoadingUser = true
|
||||||
this.user = await fetchCurrentUser()
|
this.user = await fetchCurrentUser()
|
||||||
|
this.isLoadingUser = false
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadInfo () {
|
async loadInfo () {
|
||||||
@@ -146,27 +158,6 @@ export default {
|
|||||||
background-color: var(--primary-color-disabled);
|
background-color: var(--primary-color-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
.redeem-dialog {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.redeem-dialog-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
-webkit-backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.milk-tea-status-container {
|
.milk-tea-status-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -188,9 +179,6 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -231,6 +219,11 @@ export default {
|
|||||||
.redeem-cancel-button:hover {
|
.redeem-cancel-button:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
.user-level-text {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="activity-list-page">
|
<div class="activity-list-page">
|
||||||
|
<div v-if="isLoadingActivities" class="loading-activities">
|
||||||
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="activity-list-page-card" v-for="a in activities" :key="a.id">
|
<div class="activity-list-page-card" v-for="a in activities" :key="a.id">
|
||||||
<div class="activity-list-page-card-normal">
|
<div class="activity-list-page-card-normal">
|
||||||
<div v-if="a.icon" class="activity-card-normal-left">
|
<div v-if="a.icon" class="activity-card-normal-left">
|
||||||
@@ -29,23 +33,43 @@
|
|||||||
import { API_BASE_URL } from '../main'
|
import { API_BASE_URL } from '../main'
|
||||||
import TimeManager from '../utils/time'
|
import TimeManager from '../utils/time'
|
||||||
import MilkTeaActivityComponent from '../components/MilkTeaActivityComponent.vue'
|
import MilkTeaActivityComponent from '../components/MilkTeaActivityComponent.vue'
|
||||||
|
import { hatch } from 'ldrs'
|
||||||
|
hatch.register()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ActivityListPageView',
|
name: 'ActivityListPageView',
|
||||||
components: { MilkTeaActivityComponent },
|
components: { MilkTeaActivityComponent },
|
||||||
data() {
|
data() {
|
||||||
return { activities: [], TimeManager }
|
return {
|
||||||
|
activities: [],
|
||||||
|
TimeManager,
|
||||||
|
isLoadingActivities: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
this.isLoadingActivities = true
|
||||||
if (res.ok) {
|
try {
|
||||||
this.activities = await res.json()
|
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||||
|
if (res.ok) {
|
||||||
|
this.activities = await res.json()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
this.isLoadingActivities = false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.loading-activities {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-list-page {
|
.activity-list-page {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ export default {
|
|||||||
.topic-container {
|
.topic-container {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 10;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
Reference in New Issue
Block a user