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 @@
+
+
+
+
+
+
+
+
+
+
{{ lottery.prizeDescription }}
+
x {{ lottery.prizeCount }}
+
+
+
离结束还有
+
{{ countdown }}
+
+
+
+
+
+
+
+
+
+
+
获奖者:
+
+
+ {{ lotteryWinners[0].username }}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
{{ opt }}
+
+ {{ pollPercentages[idx] }}% ({{ pollVotes[idx] || 0 }}人已投票)
+
+
+
+
+
+
+
+
+
+
+
+
{{ pollParticipants.length }}
+
投票人
+
+
+
+
+ 投票
+
+
+ 结果
+
+
+
+
离结束还有
+
{{ countdown }}
+
+
+
+
+
+
+
+
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 }}
-
-
-
-
-
-
-
-
-
-
-
获奖者:
-
-
- {{ lotteryWinners[0].username }}
-
-
-
-
+
-
-
-
-
-
-
-
{{ opt }}
-
- {{ pollPercentages[idx] }}% ({{ pollVotes[idx] || 0 }}人已投票)
-
-
-
-
-
-
-
-
-
-
-
-
{{ 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;
}
}