mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-28 09:00:48 +08:00
Compare commits
4 Commits
codex/modi
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c34a9048 | ||
|
|
ca6e45a711 | ||
|
|
803ca9e103 | ||
|
|
9d1e12773a |
@@ -34,6 +34,7 @@
|
|||||||
--page-max-width-mobile: 900px;
|
--page-max-width-mobile: 900px;
|
||||||
--article-info-background-color: #f0f0f0;
|
--article-info-background-color: #f0f0f0;
|
||||||
--activity-card-background-color: #fafafa;
|
--activity-card-background-color: #fafafa;
|
||||||
|
--poll-option-button-background-color: rgb(218, 218, 218);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] {
|
[data-theme='dark'] {
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
--blockquote-text-color: #999;
|
--blockquote-text-color: #999;
|
||||||
--article-info-background-color: #747373;
|
--article-info-background-color: #747373;
|
||||||
--activity-card-background-color: #585858;
|
--activity-card-background-color: #585858;
|
||||||
|
--poll-option-button-background-color: #3a3a3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-frosted='off'] {
|
:root[data-frosted='off'] {
|
||||||
|
|||||||
310
frontend_nuxt/components/PostLottery.vue
Normal file
310
frontend_nuxt/components/PostLottery.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<div class="post-prize-container" v-if="lottery">
|
||||||
|
<div class="prize-content">
|
||||||
|
<div class="prize-info">
|
||||||
|
<div class="prize-info-left">
|
||||||
|
<div class="prize-icon">
|
||||||
|
<BaseImage
|
||||||
|
class="prize-icon-img"
|
||||||
|
v-if="lottery.prizeIcon"
|
||||||
|
:src="lottery.prizeIcon"
|
||||||
|
alt="prize"
|
||||||
|
/>
|
||||||
|
<i v-else class="fa-solid fa-gift default-prize-icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="prize-name">{{ lottery.prizeDescription }}</div>
|
||||||
|
<div class="prize-count">x {{ lottery.prizeCount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="prize-end-time prize-info-right">
|
||||||
|
<div v-if="!isMobile" class="prize-end-time-title">离结束还有</div>
|
||||||
|
<div class="prize-end-time-value">{{ countdown }}</div>
|
||||||
|
<div v-if="!isMobile" class="join-prize-button-container-desktop">
|
||||||
|
<div
|
||||||
|
v-if="loggedIn && !hasJoined && !lotteryEnded"
|
||||||
|
class="join-prize-button"
|
||||||
|
@click="joinLottery"
|
||||||
|
>
|
||||||
|
<div class="join-prize-button-text">
|
||||||
|
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
||||||
|
<div class="join-prize-button-text">已参与</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isMobile" class="join-prize-button-container-mobile">
|
||||||
|
<div
|
||||||
|
v-if="loggedIn && !hasJoined && !lotteryEnded"
|
||||||
|
class="join-prize-button"
|
||||||
|
@click="joinLottery"
|
||||||
|
>
|
||||||
|
<div class="join-prize-button-text">
|
||||||
|
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
||||||
|
<div class="join-prize-button-text">已参与</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prize-member-container">
|
||||||
|
<BaseImage
|
||||||
|
v-for="p in lotteryParticipants"
|
||||||
|
:key="p.id"
|
||||||
|
class="prize-member-avatar"
|
||||||
|
:src="p.avatar"
|
||||||
|
alt="avatar"
|
||||||
|
@click="gotoUser(p.id)"
|
||||||
|
/>
|
||||||
|
<div v-if="lotteryEnded && lotteryWinners.length" class="prize-member-winner">
|
||||||
|
<i class="fas fa-medal medal-icon"></i>
|
||||||
|
<span class="prize-member-winner-name">获奖者: </span>
|
||||||
|
<BaseImage
|
||||||
|
v-for="w in lotteryWinners"
|
||||||
|
:key="w.id"
|
||||||
|
class="prize-member-avatar"
|
||||||
|
:src="w.avatar"
|
||||||
|
alt="avatar"
|
||||||
|
@click="gotoUser(w.id)"
|
||||||
|
/>
|
||||||
|
<div v-if="lotteryWinners.length === 1" class="prize-member-winner-name">
|
||||||
|
{{ lotteryWinners[0].username }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const loggedIn = computed(() => authState.loggedIn)
|
||||||
|
const lotteryParticipants = computed(() => props.lottery?.participants || [])
|
||||||
|
const lotteryWinners = computed(() => props.lottery?.winners || [])
|
||||||
|
const lotteryEnded = computed(() => {
|
||||||
|
if (!props.lottery || !props.lottery.endTime) return false
|
||||||
|
return new Date(props.lottery.endTime).getTime() <= Date.now()
|
||||||
|
})
|
||||||
|
const hasJoined = computed(() => {
|
||||||
|
if (!loggedIn.value) return false
|
||||||
|
return lotteryParticipants.value.some((p) => p.id === Number(authState.userId))
|
||||||
|
})
|
||||||
|
|
||||||
|
const countdown = ref('00:00:00')
|
||||||
|
let timer = null
|
||||||
|
const updateCountdown = () => {
|
||||||
|
if (!props.lottery || !props.lottery.endTime) {
|
||||||
|
countdown.value = '00:00:00'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const diff = new Date(props.lottery.endTime).getTime() - Date.now()
|
||||||
|
if (diff <= 0) {
|
||||||
|
countdown.value = '00:00:00'
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const h = String(Math.floor(diff / 3600000)).padStart(2, '0')
|
||||||
|
const m = String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0')
|
||||||
|
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
|
||||||
|
countdown.value = `${h}:${m}:${s}`
|
||||||
|
}
|
||||||
|
const startCountdown = () => {
|
||||||
|
updateCountdown()
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
timer = setInterval(updateCountdown, 1000)
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => props.lottery?.endTime,
|
||||||
|
() => {
|
||||||
|
if (props.lottery && props.lottery.endTime) startCountdown()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.lottery && props.lottery.endTime) startCountdown()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
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) {
|
||||||
|
toast.error('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/posts/${props.postId}/lottery/join`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success('已参与抽奖')
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.post-prize-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: var(--lottery-background-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-prize-button-container-mobile {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-prize-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-icon-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-name {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-count {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-end-time {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-end-time-title {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-end-time-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-info-left,
|
||||||
|
.prize-info-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-prize-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-prize-button:hover {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-prize-button-disabled {
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--primary-color-disabled);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-member-avatar {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin-left: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-member-winner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-member-winner-name {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.join-prize-button,
|
||||||
|
.join-prize-button-disabled {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
318
frontend_nuxt/components/PostPoll.vue
Normal file
318
frontend_nuxt/components/PostPoll.vue
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<template>
|
||||||
|
<div class="post-poll-container" v-if="poll">
|
||||||
|
<div class="poll-top-container">
|
||||||
|
<div class="poll-options-container">
|
||||||
|
<div v-if="showPollResult || pollEnded || hasVoted">
|
||||||
|
<div v-for="(opt, idx) in poll.options" :key="idx" class="poll-option-result">
|
||||||
|
<div class="poll-option-info-container">
|
||||||
|
<div class="poll-option-text">{{ opt }}</div>
|
||||||
|
<div class="poll-option-progress-info">
|
||||||
|
{{ pollPercentages[idx] }}% ({{ pollVotes[idx] || 0 }}人已投票)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="poll-option-progress">
|
||||||
|
<div
|
||||||
|
class="poll-option-progress-bar"
|
||||||
|
:style="{ width: pollPercentages[idx] + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="poll-participants">
|
||||||
|
<BaseImage
|
||||||
|
v-for="p in pollOptionParticipants[idx] || []"
|
||||||
|
:key="p.id"
|
||||||
|
class="poll-participant-avatar"
|
||||||
|
:src="p.avatar"
|
||||||
|
alt="avatar"
|
||||||
|
@click="gotoUser(p.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="(opt, idx) in poll.options"
|
||||||
|
:key="idx"
|
||||||
|
class="poll-option"
|
||||||
|
@click="voteOption(idx)"
|
||||||
|
>
|
||||||
|
<input type="radio" :checked="false" name="poll-option" class="poll-option-input" />
|
||||||
|
<span class="poll-option-text">{{ opt }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="poll-info">
|
||||||
|
<div class="total-votes">{{ pollParticipants.length }}</div>
|
||||||
|
<div class="total-votes-title">投票人</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="poll-bottom-container">
|
||||||
|
<div
|
||||||
|
v-if="showPollResult && !pollEnded && !hasVoted"
|
||||||
|
class="poll-option-button"
|
||||||
|
@click="showPollResult = false"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left"></i> 投票
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!pollEnded && !hasVoted"
|
||||||
|
class="poll-option-button"
|
||||||
|
@click="showPollResult = true"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-bar"></i> 结果
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poll-left-time">
|
||||||
|
<div class="poll-left-time-title">离结束还有</div>
|
||||||
|
<div class="poll-left-time-value">{{ countdown }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
const loggedIn = computed(() => authState.loggedIn)
|
||||||
|
const showPollResult = ref(false)
|
||||||
|
|
||||||
|
const pollParticipants = computed(() => props.poll?.participants || [])
|
||||||
|
const pollOptionParticipants = computed(() => props.poll?.optionParticipants || {})
|
||||||
|
const pollVotes = computed(() => props.poll?.votes || {})
|
||||||
|
const totalPollVotes = computed(() => Object.values(pollVotes.value).reduce((a, b) => a + b, 0))
|
||||||
|
const pollPercentages = computed(() =>
|
||||||
|
props.poll
|
||||||
|
? props.poll.options.map((_, idx) => {
|
||||||
|
const c = pollVotes.value[idx] || 0
|
||||||
|
return totalPollVotes.value ? ((c / totalPollVotes.value) * 100).toFixed(1) : 0
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
const pollEnded = computed(() => {
|
||||||
|
if (!props.poll || !props.poll.endTime) return false
|
||||||
|
return new Date(props.poll.endTime).getTime() <= Date.now()
|
||||||
|
})
|
||||||
|
const hasVoted = computed(() => {
|
||||||
|
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 timer = null
|
||||||
|
const updateCountdown = () => {
|
||||||
|
if (!props.poll || !props.poll.endTime) {
|
||||||
|
countdown.value = '00:00:00'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const diff = new Date(props.poll.endTime).getTime() - Date.now()
|
||||||
|
if (diff <= 0) {
|
||||||
|
countdown.value = '00:00:00'
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const h = String(Math.floor(diff / 3600000)).padStart(2, '0')
|
||||||
|
const m = String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0')
|
||||||
|
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
|
||||||
|
countdown.value = `${h}:${m}:${s}`
|
||||||
|
}
|
||||||
|
const startCountdown = () => {
|
||||||
|
updateCountdown()
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
timer = setInterval(updateCountdown, 1000)
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => props.poll?.endTime,
|
||||||
|
() => {
|
||||||
|
if (props.poll && props.poll.endTime) startCountdown()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.poll && props.poll.endTime) startCountdown()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
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) {
|
||||||
|
toast.error('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/posts/${props.postId}/poll/vote?option=${idx}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success('投票成功')
|
||||||
|
emit('refresh')
|
||||||
|
showPollResult.value = true
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--poll-option-button-background-color);
|
||||||
|
cursor: pointer;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-top-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--normal-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-options-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-votes {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-votes-title {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-result {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
gap: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-input {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-bottom-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-left-time {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-left-time-title {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-left-time-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-progress {
|
||||||
|
position: relative;
|
||||||
|
background-color: rgb(187, 187, 187);
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-progress-bar {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-progress-info {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-participants {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-participant-avatar {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -714,6 +714,12 @@ const formatType = (t) => {
|
|||||||
return '帖子被删除'
|
return '帖子被删除'
|
||||||
case 'POST_FEATURED':
|
case 'POST_FEATURED':
|
||||||
return '文章被精选'
|
return '文章被精选'
|
||||||
|
case 'POLL_VOTE':
|
||||||
|
return '有人参与你的投票'
|
||||||
|
case 'POLL_RESULT_OWNER':
|
||||||
|
return '发布的投票结果已公布'
|
||||||
|
case 'POLL_RESULT_PARTICIPANT':
|
||||||
|
return '参与的投票结果已公布'
|
||||||
default:
|
default:
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,157 +94,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="lottery" class="post-prize-container">
|
<PostLottery v-if="lottery" :lottery="lottery" :post-id="postId" @refresh="refreshPost" />
|
||||||
<div class="prize-content">
|
|
||||||
<div class="prize-info">
|
|
||||||
<div class="prize-info-left">
|
|
||||||
<div class="prize-icon">
|
|
||||||
<BaseImage
|
|
||||||
class="prize-icon-img"
|
|
||||||
v-if="lottery.prizeIcon"
|
|
||||||
:src="lottery.prizeIcon"
|
|
||||||
alt="prize"
|
|
||||||
/>
|
|
||||||
<i v-else class="fa-solid fa-gift default-prize-icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="prize-name">{{ lottery.prizeDescription }}</div>
|
|
||||||
<div class="prize-count">x {{ lottery.prizeCount }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="prize-end-time prize-info-right">
|
|
||||||
<div v-if="!isMobile" class="prize-end-time-title">离结束还有</div>
|
|
||||||
<div class="prize-end-time-value">{{ countdown }}</div>
|
|
||||||
<div v-if="!isMobile" class="join-prize-button-container-desktop">
|
|
||||||
<div
|
|
||||||
v-if="loggedIn && !hasJoined && !lotteryEnded"
|
|
||||||
class="join-prize-button"
|
|
||||||
@click="joinLottery"
|
|
||||||
>
|
|
||||||
<div class="join-prize-button-text">
|
|
||||||
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
|
||||||
<div class="join-prize-button-text">已参与</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isMobile" class="join-prize-button-container-mobile">
|
|
||||||
<div
|
|
||||||
v-if="loggedIn && !hasJoined && !lotteryEnded"
|
|
||||||
class="join-prize-button"
|
|
||||||
@click="joinLottery"
|
|
||||||
>
|
|
||||||
<div class="join-prize-button-text">
|
|
||||||
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
|
||||||
<div class="join-prize-button-text">已参与</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="prize-member-container">
|
|
||||||
<BaseImage
|
|
||||||
v-for="p in lotteryParticipants"
|
|
||||||
:key="p.id"
|
|
||||||
class="prize-member-avatar"
|
|
||||||
:src="p.avatar"
|
|
||||||
alt="avatar"
|
|
||||||
@click="gotoUser(p.id)"
|
|
||||||
/>
|
|
||||||
<div v-if="lotteryEnded && lotteryWinners.length" class="prize-member-winner">
|
|
||||||
<i class="fas fa-medal medal-icon"></i>
|
|
||||||
<span class="prize-member-winner-name">获奖者: </span>
|
|
||||||
<BaseImage
|
|
||||||
v-for="w in lotteryWinners"
|
|
||||||
:key="w.id"
|
|
||||||
class="prize-member-avatar"
|
|
||||||
:src="w.avatar"
|
|
||||||
alt="avatar"
|
|
||||||
@click="gotoUser(w.id)"
|
|
||||||
/>
|
|
||||||
<div v-if="lotteryWinners.length === 1" class="prize-member-winner-name">
|
|
||||||
{{ lotteryWinners[0].username }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div v-if="poll" class="post-poll-container">
|
<PostPoll v-if="poll" :poll="poll" :post-id="postId" @refresh="refreshPost" />
|
||||||
<div class="poll-top-container">
|
|
||||||
<div class="poll-options-container">
|
|
||||||
<div v-if="showPollResult || pollEnded || hasVoted">
|
|
||||||
<div v-for="(opt, idx) in poll.options" :key="idx" class="poll-option-result">
|
|
||||||
<div class="poll-option-info-container">
|
|
||||||
<div class="poll-option-text">{{ opt }}</div>
|
|
||||||
<div class="poll-option-progress-info">
|
|
||||||
{{ pollPercentages[idx] }}% ({{ pollVotes[idx] || 0 }}人已投票)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="poll-option-progress">
|
|
||||||
<div
|
|
||||||
class="poll-option-progress-bar"
|
|
||||||
:style="{ width: pollPercentages[idx] + '%' }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="poll-participants">
|
|
||||||
<BaseImage
|
|
||||||
v-for="p in pollOptionParticipants[idx] || []"
|
|
||||||
:key="p.id"
|
|
||||||
class="poll-participant-avatar"
|
|
||||||
:src="p.avatar"
|
|
||||||
alt="avatar"
|
|
||||||
@click="gotoUser(p.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div
|
|
||||||
v-for="(opt, idx) in poll.options"
|
|
||||||
:key="idx"
|
|
||||||
class="poll-option"
|
|
||||||
@click="voteOption(idx)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
:checked="false"
|
|
||||||
name="poll-option"
|
|
||||||
class="poll-option-input"
|
|
||||||
/>
|
|
||||||
<span class="poll-option-text">{{ opt }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="poll-info">
|
|
||||||
<div class="total-votes">{{ pollParticipants.length }}</div>
|
|
||||||
<div class="total-votes-title">投票人</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="poll-bottom-container">
|
|
||||||
<div
|
|
||||||
v-if="showPollResult && !pollEnded && !hasVoted"
|
|
||||||
class="poll-option-button"
|
|
||||||
@click="showPollResult = false"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chevron-left"></i> 投票
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="!pollEnded && !hasVoted"
|
|
||||||
class="poll-option-button"
|
|
||||||
@click="showPollResult = true"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chart-bar"></i> 结果
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="poll-left-time">
|
|
||||||
<div class="poll-left-time-title">离结束还有</div>
|
|
||||||
<div class="poll-left-time-value">{{ countdown }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div v-if="closed" class="post-close-container">该帖子已关闭,内容仅供阅读,无法进行互动</div>
|
<div v-if="closed" class="post-close-container">该帖子已关闭,内容仅供阅读,无法进行互动</div>
|
||||||
|
|
||||||
@@ -333,6 +185,8 @@ import ArticleTags from '~/components/ArticleTags.vue'
|
|||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||||
|
import PostLottery from '~/components/PostLottery.vue'
|
||||||
|
import PostPoll from '~/components/PostPoll.vue'
|
||||||
import { renderMarkdown, handleMarkdownClick, stripMarkdownLength } from '~/utils/markdown'
|
import { renderMarkdown, handleMarkdownClick, stripMarkdownLength } from '~/utils/markdown'
|
||||||
import { getMedalTitle } from '~/utils/medal'
|
import { getMedalTitle } from '~/utils/medal'
|
||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
@@ -388,7 +242,6 @@ useHead(() => ({
|
|||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('scroll', updateCurrentIndex)
|
window.removeEventListener('scroll', updateCurrentIndex)
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,73 +253,6 @@ const isAdmin = computed(() => authState.role === 'ADMIN')
|
|||||||
const isAuthor = computed(() => authState.username === author.value.username)
|
const isAuthor = computed(() => authState.username === author.value.username)
|
||||||
const lottery = ref(null)
|
const lottery = ref(null)
|
||||||
const poll = ref(null)
|
const poll = ref(null)
|
||||||
const showPollResult = ref(false)
|
|
||||||
const countdown = ref('00:00:00')
|
|
||||||
let countdownTimer = null
|
|
||||||
const lotteryParticipants = computed(() => lottery.value?.participants || [])
|
|
||||||
const lotteryWinners = computed(() => lottery.value?.winners || [])
|
|
||||||
const lotteryEnded = computed(() => {
|
|
||||||
if (!lottery.value || !lottery.value.endTime) return false
|
|
||||||
return new Date(lottery.value.endTime).getTime() <= Date.now()
|
|
||||||
})
|
|
||||||
const hasJoined = computed(() => {
|
|
||||||
if (!loggedIn.value) return false
|
|
||||||
return lotteryParticipants.value.some((p) => p.id === Number(authState.userId))
|
|
||||||
})
|
|
||||||
const pollParticipants = computed(() => poll.value?.participants || [])
|
|
||||||
const pollOptionParticipants = computed(() => poll.value?.optionParticipants || {})
|
|
||||||
const pollVotes = computed(() => poll.value?.votes || {})
|
|
||||||
const totalPollVotes = computed(() => Object.values(pollVotes.value).reduce((a, b) => a + b, 0))
|
|
||||||
const pollPercentages = computed(() =>
|
|
||||||
poll.value
|
|
||||||
? poll.value.options.map((_, idx) => {
|
|
||||||
const c = pollVotes.value[idx] || 0
|
|
||||||
return totalPollVotes.value ? ((c / totalPollVotes.value) * 100).toFixed(1) : 0
|
|
||||||
})
|
|
||||||
: [],
|
|
||||||
)
|
|
||||||
const pollEnded = computed(() => {
|
|
||||||
if (!poll.value || !poll.value.endTime) return false
|
|
||||||
return new Date(poll.value.endTime).getTime() <= Date.now()
|
|
||||||
})
|
|
||||||
const hasVoted = computed(() => {
|
|
||||||
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 currentEndTime = computed(() => {
|
|
||||||
if (lottery.value && lottery.value.endTime) return lottery.value.endTime
|
|
||||||
if (poll.value && poll.value.endTime) return poll.value.endTime
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
const updateCountdown = () => {
|
|
||||||
if (!currentEndTime.value) {
|
|
||||||
countdown.value = '00:00:00'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const diff = new Date(currentEndTime.value).getTime() - Date.now()
|
|
||||||
if (diff <= 0) {
|
|
||||||
countdown.value = '00:00:00'
|
|
||||||
if (countdownTimer) {
|
|
||||||
clearInterval(countdownTimer)
|
|
||||||
countdownTimer = null
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const h = String(Math.floor(diff / 3600000)).padStart(2, '0')
|
|
||||||
const m = String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0')
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
|
|
||||||
const articleMenuItems = computed(() => {
|
const articleMenuItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
if (isAuthor.value || isAdmin.value) {
|
if (isAuthor.value || isAdmin.value) {
|
||||||
@@ -628,8 +414,6 @@ watchEffect(() => {
|
|||||||
postTime.value = TimeManager.format(data.createdAt)
|
postTime.value = TimeManager.format(data.createdAt)
|
||||||
lottery.value = data.lottery || null
|
lottery.value = data.lottery || null
|
||||||
poll.value = data.poll || null
|
poll.value = data.poll || null
|
||||||
if ((lottery.value && lottery.value.endTime) || (poll.value && poll.value.endTime))
|
|
||||||
startCountdown()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 404 客户端跳转
|
// 404 客户端跳转
|
||||||
@@ -920,45 +704,6 @@ const unsubscribePost = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const joinLottery = async () => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!token) {
|
|
||||||
toast.error('请先登录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/lottery/join`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
const data = await res.json().catch(() => ({}))
|
|
||||||
if (res.ok) {
|
|
||||||
toast.success('已参与抽奖')
|
|
||||||
await refreshPost()
|
|
||||||
} else {
|
|
||||||
toast.error(data.error || '操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const voteOption = async (idx) => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!token) {
|
|
||||||
toast.error('请先登录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/poll/vote?option=${idx}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
const data = await res.json().catch(() => ({}))
|
|
||||||
if (res.ok) {
|
|
||||||
toast.success('投票成功')
|
|
||||||
await refreshPost()
|
|
||||||
showPollResult.value = true
|
|
||||||
} else {
|
|
||||||
toast.error(data.error || '操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchCommentSorts = () => {
|
const fetchCommentSorts = () => {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ id: 'NEWEST', name: '最新', icon: 'fas fa-clock' },
|
{ id: 'NEWEST', name: '最新', icon: 'fas fa-clock' },
|
||||||
@@ -1287,95 +1032,6 @@ onMounted(async () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-option-button {
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: rgb(218, 218, 218);
|
|
||||||
cursor: pointer;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-top-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--normal-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-options-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-votes {
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-votes-title {
|
|
||||||
font-size: 18px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-result {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
gap: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-input {
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
accent-color: var(--primary-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-text {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-bottom-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-left-time {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-left-time-title {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-menu-icon {
|
.action-menu-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -1491,201 +1147,6 @@ onMounted(async () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-prize-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
background-color: var(--lottery-background-color);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-progress-bar {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-info-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-progress-info {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
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;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-participant-avatar {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-prize-button-container-mobile {
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-prize-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-icon-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-name {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-count {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-left: 10px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-end-time {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-left-time-title,
|
|
||||||
.prize-end-time-title {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-left-time-value,
|
|
||||||
.prize-end-time-value {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-info-left,
|
|
||||||
.prize-info-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-prize-button {
|
|
||||||
margin-left: 10px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-prize-button:hover {
|
|
||||||
background-color: var(--primary-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-prize-button-disabled {
|
|
||||||
text-align: center;
|
|
||||||
margin-left: 10px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--primary-color-disabled);
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-member-avatar {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
margin-left: 3px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-member-winner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.medal-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-member-winner-name {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.post-page-main-container {
|
.post-page-main-container {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
@@ -1736,10 +1197,5 @@ onMounted(async () => {
|
|||||||
.loading-container {
|
.loading-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-prize-button,
|
|
||||||
.join-prize-button-disabled {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user