Compare commits

..

1 Commits

Author SHA1 Message Date
Tim
39c34a9048 feat: add PostPoll and PostLottery components 2025-08-31 11:10:20 +08:00
3 changed files with 171 additions and 92 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div class="post-prize-container">
<div class="post-prize-container" v-if="lottery">
<div class="prize-content">
<div class="prize-info">
<div class="prize-info-left">
@@ -79,30 +79,20 @@
</template>
<script setup>
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { toast } from '~/main'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { getToken, authState } from '~/utils/auth'
import { toast } from '~/main'
import { useRuntimeConfig } from '#imports'
import { useIsMobile } from '~/utils/screen'
const props = defineProps({
lottery: {
type: Object,
required: true,
},
postId: {
type: [String, Number],
required: true,
},
lottery: { type: Object, required: true },
postId: { type: [String, Number], required: true },
})
const emit = defineEmits(['refresh'])
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const loggedIn = computed(() => authState.loggedIn)
const isMobile = useIsMobile()
const loggedIn = computed(() => authState.loggedIn)
const lotteryParticipants = computed(() => props.lottery?.participants || [])
const lotteryWinners = computed(() => props.lottery?.winners || [])
const lotteryEnded = computed(() => {
@@ -115,8 +105,7 @@ const hasJoined = computed(() => {
})
const countdown = ref('00:00:00')
let countdownTimer = null
let timer = null
const updateCountdown = () => {
if (!props.lottery || !props.lottery.endTime) {
countdown.value = '00:00:00'
@@ -125,9 +114,9 @@ const updateCountdown = () => {
const diff = new Date(props.lottery.endTime).getTime() - Date.now()
if (diff <= 0) {
countdown.value = '00:00:00'
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
if (timer) {
clearInterval(timer)
timer = null
}
return
}
@@ -136,30 +125,28 @@ const updateCountdown = () => {
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
countdown.value = `${h}:${m}:${s}`
}
const startCountdown = () => {
if (!import.meta.client) return
if (countdownTimer) clearInterval(countdownTimer)
updateCountdown()
countdownTimer = setInterval(updateCountdown, 1000)
if (timer) clearInterval(timer)
timer = setInterval(updateCountdown, 1000)
}
watch(
() => props.lottery?.endTime,
() => {
if (props.lottery && props.lottery.endTime) {
startCountdown()
}
if (props.lottery && props.lottery.endTime) startCountdown()
},
{ immediate: true },
)
onMounted(() => {
if (props.lottery && props.lottery.endTime) startCountdown()
})
onBeforeUnmount(() => {
if (countdownTimer) clearInterval(countdownTimer)
if (timer) clearInterval(timer)
})
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const joinLottery = async () => {
const token = getToken()
if (!token) {
@@ -313,4 +300,11 @@ const joinLottery = async () => {
font-size: 13px;
opacity: 0.7;
}
@media (max-width: 768px) {
.join-prize-button,
.join-prize-button-disabled {
margin-left: 0;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="post-poll-container">
<div class="post-poll-container" v-if="poll">
<div class="poll-top-container">
<div class="poll-options-container">
<div v-if="showPollResult || pollEnded || hasVoted">
@@ -70,26 +70,18 @@
</template>
<script setup>
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { toast } from '~/main'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { getToken, authState } from '~/utils/auth'
import { toast } from '~/main'
import { useRuntimeConfig } from '#imports'
const props = defineProps({
poll: {
type: Object,
required: true,
},
postId: {
type: [String, Number],
required: true,
},
poll: { type: Object, required: true },
postId: { type: [String, Number], required: true },
})
const emit = defineEmits(['refresh'])
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const loggedIn = computed(() => authState.loggedIn)
const showPollResult = ref(false)
const pollParticipants = computed(() => props.poll?.participants || [])
@@ -109,17 +101,15 @@ const pollEnded = computed(() => {
return new Date(props.poll.endTime).getTime() <= Date.now()
})
const hasVoted = computed(() => {
if (!authState.loggedIn) return false
if (!loggedIn.value) return false
return pollParticipants.value.some((p) => p.id === Number(authState.userId))
})
watch([hasVoted, pollEnded], ([voted, ended]) => {
if (voted || ended) showPollResult.value = true
})
const countdown = ref('00:00:00')
let countdownTimer = null
let timer = null
const updateCountdown = () => {
if (!props.poll || !props.poll.endTime) {
countdown.value = '00:00:00'
@@ -128,9 +118,9 @@ const updateCountdown = () => {
const diff = new Date(props.poll.endTime).getTime() - Date.now()
if (diff <= 0) {
countdown.value = '00:00:00'
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
if (timer) {
clearInterval(timer)
timer = null
}
return
}
@@ -139,30 +129,28 @@ const updateCountdown = () => {
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
countdown.value = `${h}:${m}:${s}`
}
const startCountdown = () => {
if (!import.meta.client) return
if (countdownTimer) clearInterval(countdownTimer)
updateCountdown()
countdownTimer = setInterval(updateCountdown, 1000)
if (timer) clearInterval(timer)
timer = setInterval(updateCountdown, 1000)
}
watch(
() => props.poll?.endTime,
() => {
if (props.poll && props.poll.endTime) {
startCountdown()
}
if (props.poll && props.poll.endTime) startCountdown()
},
{ immediate: true },
)
onMounted(() => {
if (props.poll && props.poll.endTime) startCountdown()
})
onBeforeUnmount(() => {
if (countdownTimer) clearInterval(countdownTimer)
if (timer) clearInterval(timer)
})
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const voteOption = async (idx) => {
const token = getToken()
if (!token) {
@@ -185,6 +173,16 @@ const voteOption = async (idx) => {
</script>
<style scoped>
.post-poll-container {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background-color: var(--lottery-background-color);
border-radius: 10px;
padding: 10px;
}
.poll-option-button {
color: var(--text-color);
padding: 5px 10px;
@@ -272,7 +270,6 @@ const voteOption = async (idx) => {
.poll-left-time-title {
font-size: 13px;
opacity: 0.7;
margin-right: 5px;
}
.poll-left-time-value {
@@ -281,21 +278,6 @@ const voteOption = async (idx) => {
color: var(--primary-color);
}
.post-poll-container {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background-color: var(--lottery-background-color);
border-radius: 10px;
padding: 10px;
}
.poll-question {
font-weight: bold;
margin-bottom: 10px;
}
.poll-option-progress {
position: relative;
background-color: rgb(187, 187, 187);
@@ -321,13 +303,6 @@ const voteOption = async (idx) => {
color: var(--text-color);
}
.poll-vote-button {
margin-top: 5px;
color: var(--primary-color);
cursor: pointer;
width: fit-content;
}
.poll-participants {
display: flex;
flex-wrap: wrap;

View File

@@ -1032,7 +1032,120 @@ onMounted(async () => {
cursor: pointer;
}
.action-menu-icon {
cursor: pointer;
font-size: 18px;
padding: 5px;
}
.article-info-container {
display: flex;
flex-direction: row;
margin-top: 10px;
gap: 10px;
align-items: center;
}
.info-content-container {
display: flex;
flex-direction: row;
gap: 10px;
padding: 0px;
border-bottom: 1px solid var(--normal-border-color);
}
.user-avatar-container {
cursor: pointer;
}
.user-avatar-item {
width: 50px;
height: 50px;
}
.user-avatar-item-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.info-content {
display: flex;
flex-direction: column;
gap: 3px;
width: 100%;
}
.info-content-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.user-name {
font-size: 16px;
font-weight: bold;
opacity: 0.7;
}
.user-medal {
font-size: 12px;
margin-left: 4px;
opacity: 0.6;
cursor: pointer;
text-decoration: none;
color: var(--text-color);
}
.post-time {
font-size: 14px;
opacity: 0.5;
}
.info-content-text {
font-size: 16px;
line-height: 1.5;
}
.article-footer-container {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 0px;
}
.reactions-viewer {
display: flex;
flex-direction: row;
gap: 20px;
align-items: center;
}
.reactions-viewer-item-container {
display: flex;
flex-direction: row;
gap: 2px;
align-items: center;
}
.reactions-viewer-item {
font-size: 16px;
}
.make-reaction-container {
display: flex;
flex-direction: row;
gap: 10px;
}
.copy-link:hover {
background-color: #e2e2e2;
}
.comment-editor-wrapper {
position: relative;
}
@media (max-width: 768px) {
.post-page-main-container {
@@ -1084,8 +1197,5 @@ onMounted(async () => {
.loading-container {
width: 100%;
}
margin-left: 0;
}
}
</style>