diff --git a/frontend_nuxt/components/PostLottery.vue b/frontend_nuxt/components/PostLottery.vue new file mode 100644 index 000000000..19e7d6393 --- /dev/null +++ b/frontend_nuxt/components/PostLottery.vue @@ -0,0 +1,316 @@ + + + + + diff --git a/frontend_nuxt/components/PostPoll.vue b/frontend_nuxt/components/PostPoll.vue new file mode 100644 index 000000000..6672916f6 --- /dev/null +++ b/frontend_nuxt/components/PostPoll.vue @@ -0,0 +1,343 @@ + + + + + diff --git a/frontend_nuxt/pages/posts/[id]/index.vue b/frontend_nuxt/pages/posts/[id]/index.vue index 97630fee3..db7d9ae86 100644 --- a/frontend_nuxt/pages/posts/[id]/index.vue +++ b/frontend_nuxt/pages/posts/[id]/index.vue @@ -94,157 +94,9 @@ -
-
-
-
-
- - -
-
{{ lottery.prizeDescription }}
-
x {{ lottery.prizeCount }}
-
-
-
离结束还有
-
{{ countdown }}
-
-
-
- 参与抽奖 {{ lottery.pointCost }} -
-
-
-
已参与
-
-
-
-
- -
-
-
- 参与抽奖 {{ lottery.pointCost }} -
-
-
-
已参与
-
-
-
-
- -
- - 获奖者: - -
- {{ lotteryWinners[0].username }} -
-
-
-
+ -
-
-
-
-
-
-
{{ opt }}
-
- {{ pollPercentages[idx] }}% ({{ pollVotes[idx] || 0 }}人已投票) -
-
-
-
-
-
- -
-
-
-
-
- - {{ opt }} -
-
-
-
-
{{ pollParticipants.length }}
-
投票人
-
-
-
-
- 投票 -
-
- 结果 -
- -
-
离结束还有
-
{{ countdown }}
-
-
-
+
该帖子已关闭,内容仅供阅读,无法进行互动
@@ -333,6 +185,8 @@ import ArticleTags from '~/components/ArticleTags.vue' import ArticleCategory from '~/components/ArticleCategory.vue' import ReactionsGroup from '~/components/ReactionsGroup.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 { getMedalTitle } from '~/utils/medal' import { toast } from '~/main' @@ -388,7 +242,6 @@ useHead(() => ({ if (import.meta.client) { onBeforeUnmount(() => { 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 lottery = 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 items = [] if (isAuthor.value || isAdmin.value) { @@ -628,8 +414,6 @@ watchEffect(() => { postTime.value = TimeManager.format(data.createdAt) lottery.value = data.lottery || null poll.value = data.poll || null - if ((lottery.value && lottery.value.endTime) || (poll.value && poll.value.endTime)) - startCountdown() }) // 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 = () => { return Promise.resolve([ { id: 'NEWEST', name: '最新', icon: 'fas fa-clock' }, @@ -1287,404 +1032,7 @@ onMounted(async () => { cursor: pointer; } -.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; -} - -.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; -} - -.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) { .post-page-main-container { @@ -1737,8 +1085,6 @@ onMounted(async () => { width: 100%; } - .join-prize-button, - .join-prize-button-disabled { margin-left: 0; } }