fix: 弹出弹窗逻辑修改

This commit is contained in:
Tim
2025-08-19 21:48:48 +08:00
parent e9458f5419
commit fb89c9fb25
4 changed files with 194 additions and 90 deletions

View File

@@ -1,21 +1,21 @@
<template>
<BasePopup :visible="state.visible" @close="onCancel">
<div class="confirm-dialog">
<h3 class="confirm-title">{{ state.title }}</h3>
<p class="confirm-message">{{ state.message }}</p>
<BasePopup :visible="visible" @close="onCancel">
<div class="confirm-dialog" role="dialog" aria-modal="true">
<h3 class="confirm-title">{{ title }}</h3>
<p class="confirm-message">{{ message }}</p>
<div class="confirm-actions">
<button class="cancel-button" @click="onCancel">取消</button>
<button class="confirm-button" @click="onConfirm">确认</button>
<div class="cancel-button" @click="onCancel">取消</div>
<div class="confirm-button" @click="onConfirm">确认</div>
</div>
</div>
</BasePopup>
</template>
<script setup>
<script setup lang="ts">
import BasePopup from '~/components/BasePopup.vue'
import { useConfirm } from '~/composables/useConfirm'
const { state, onConfirm, onCancel } = useConfirm()
const { visible, title, message, onConfirm, onCancel } = useConfirm()
</script>
<style scoped>
@@ -25,29 +25,47 @@ const { state, onConfirm, onCancel } = useConfirm()
}
.confirm-title {
margin-top: 0;
font-size: 18px;
font-weight: 600;
}
.confirm-message {
margin: 20px 0;
margin: 16px 0 20px;
line-height: 1.6;
color: var(--text-secondary, #666);
}
.confirm-actions {
display: flex;
justify-content: center;
gap: 10px;
gap: 12px;
}
.confirm-button,
.cancel-button {
min-width: 88px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
cursor: pointer;
border: 1px solid transparent;
}
.confirm-button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
background: var(--primary-color);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.confirm-button:hover {
background: var(--primary-color-hover);
}
.cancel-button {
background-color: #ccc;
color: black;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
background: transparent;
color: var(--primary-color);
border-color: currentColor;
display: flex;
align-items: center;
justify-content: center;
}
</style>
.cancel-button:hover {
opacity: 0.85;
}
</style>

View File

@@ -1,42 +0,0 @@
import { ref } from 'vue'
const state = ref({
visible: false,
title: '',
message: '',
resolve: null,
reject: null,
})
export const useConfirm = () => {
const confirm = (title, message) => {
state.value.title = title
state.value.message = message
state.value.visible = true
return new Promise((resolve, reject) => {
state.value.resolve = resolve
state.value.reject = reject
})
}
const onConfirm = () => {
if (state.value.resolve) {
state.value.resolve(true)
}
state.value.visible = false
}
const onCancel = () => {
if (state.value.reject) {
state.value.reject(false)
}
state.value.visible = false
}
return {
confirm,
onConfirm,
onCancel,
state,
}
}

View File

@@ -0,0 +1,52 @@
// composables/useConfirm.ts
import { ref } from 'vue'
// 全局单例SPA 下即可Nuxt/SSR 下见文末“SSR 提醒”)
const visible = ref(false)
const title = ref('')
const message = ref('')
let resolver: ((ok: boolean) => void) | null = null
function reset() {
visible.value = false
title.value = ''
message.value = ''
resolver = null
}
export function useConfirm() {
/**
* 打开确认框,返回 Promise<boolean>
* - 确认 => resolve(true)
* - 取消/关闭 => resolve(false)
* 若并发调用,以最后一次为准(更简单直观)
*/
const confirm = (t: string, m: string) => {
title.value = t
message.value = m
visible.value = true
return new Promise<boolean>((resolve) => {
resolver = resolve
})
}
const onConfirm = () => {
resolver?.(true)
reset()
}
const onCancel = () => {
resolver?.(false)
reset()
}
return {
visible,
title,
message,
confirm,
onConfirm,
onCancel,
}
}

View File

@@ -15,13 +15,21 @@
<div class="article-title-container-right">
<div v-if="status === 'PENDING'" class="article-pending-button">审核中</div>
<div v-if="status === 'REJECTED'" class="article-block-button">已拒绝</div>
<div v-if="loggedIn && !isAuthor && !subscribed" class="article-subscribe-button" @click="subscribePost">
<div
v-if="loggedIn && !isAuthor && !subscribed"
class="article-subscribe-button"
@click="subscribePost"
>
<i class="fas fa-user-plus"></i>
<div class="article-subscribe-button-text">
{{ isMobile ? '订阅' : '订阅文章' }}
</div>
</div>
<div v-if="loggedIn && !isAuthor && subscribed" class="article-unsubscribe-button" @click="unsubscribePost">
<div
v-if="loggedIn && !isAuthor && subscribed"
class="article-unsubscribe-button"
@click="unsubscribePost"
>
<i class="fas fa-user-minus"></i>
<div class="article-unsubscribe-button-text">
{{ isMobile ? '退订' : '取消订阅' }}
@@ -44,8 +52,12 @@
<div class="user-name">
{{ author.username }}
<i class="fas fa-medal medal-icon"></i>
<NuxtLink v-if="author.displayMedal" class="user-medal" :to="`/users/${author.id}?tab=achievements`">{{
getMedalTitle(author.displayMedal) }}</NuxtLink>
<NuxtLink
v-if="author.displayMedal"
class="user-medal"
:to="`/users/${author.id}?tab=achievements`"
>{{ getMedalTitle(author.displayMedal) }}</NuxtLink
>
</div>
<div class="post-time">{{ postTime }}</div>
</div>
@@ -56,12 +68,20 @@
<div class="user-name">
{{ author.username }}
<i class="fas fa-medal medal-icon"></i>
<NuxtLink v-if="author.displayMedal" class="user-medal" :to="`/users/${author.id}?tab=achievements`">{{
getMedalTitle(author.displayMedal) }}</NuxtLink>
<NuxtLink
v-if="author.displayMedal"
class="user-medal"
:to="`/users/${author.id}?tab=achievements`"
>{{ getMedalTitle(author.displayMedal) }}</NuxtLink
>
</div>
<div class="post-time">{{ postTime }}</div>
</div>
<div class="info-content-text" v-html="renderMarkdown(postContent)" @click="handleContentClick"></div>
<div
class="info-content-text"
v-html="renderMarkdown(postContent)"
@click="handleContentClick"
></div>
<div class="article-footer-container">
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
@@ -78,7 +98,12 @@
<div class="prize-info">
<div class="prize-info-left">
<div class="prize-icon">
<img class="prize-icon-img" v-if="lottery.prizeIcon" :src="lottery.prizeIcon" alt="prize" />
<img
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>
@@ -88,7 +113,11 @@
<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
v-if="loggedIn && !hasJoined && !lotteryEnded"
class="join-prize-button"
@click="joinLottery"
>
<div class="join-prize-button-text">参与抽奖</div>
</div>
<div v-else-if="hasJoined" class="join-prize-button-disabled">
@@ -99,7 +128,11 @@
</div>
<div v-if="isMobile" class="join-prize-button-container-mobile">
<div v-if="loggedIn && !hasJoined && !lotteryEnded" class="join-prize-button" @click="joinLottery">
<div
v-if="loggedIn && !hasJoined && !lotteryEnded"
class="join-prize-button"
@click="joinLottery"
>
<div class="join-prize-button-text">参与抽奖</div>
</div>
<div v-else-if="hasJoined" class="join-prize-button-disabled">
@@ -108,13 +141,25 @@
</div>
</div>
<div class="prize-member-container">
<img v-for="p in lotteryParticipants" :key="p.id" class="prize-member-avatar" :src="p.avatar" alt="avatar"
@click="gotoUser(p.id)" />
<img
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>
<img v-for="w in lotteryWinners" :key="w.id" class="prize-member-avatar" :src="w.avatar" alt="avatar"
@click="gotoUser(w.id)" />
<img
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>
@@ -123,8 +168,13 @@
</div>
<ClientOnly>
<CommentEditor @submit="postComment" :loading="isWaitingPostingComment" :disabled="!loggedIn"
:show-login-overlay="!loggedIn" :parent-user-name="author.username" />
<CommentEditor
@submit="postComment"
:loading="isWaitingPostingComment"
:disabled="!loggedIn"
:show-login-overlay="!loggedIn"
:parent-user-name="author.username"
/>
</ClientOnly>
<div class="comment-config-container">
@@ -140,8 +190,14 @@
<div v-else class="comments-container">
<BaseTimeline :items="comments">
<template #item="{ item }">
<CommentItem :key="item.id" :comment="item" :level="0" :default-show-replies="item.openReplies"
:post-author-id="author.id" @deleted="onCommentDeleted" />
<CommentItem
:key="item.id"
:comment="item"
:level="0"
:default-show-replies="item.openReplies"
:post-author-id="author.id"
@deleted="onCommentDeleted"
/>
</template>
</BaseTimeline>
</div>
@@ -152,21 +208,40 @@
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
<div v-else class="scroller-time">{{ scrollerTopTime }}</div>
<div class="scroller-middle">
<input type="range" class="scroller-range" :max="totalPosts" :min="1" v-model.number="currentIndex"
@input="onSliderInput" />
<input
type="range"
class="scroller-range"
:max="totalPosts"
:min="1"
v-model.number="currentIndex"
@input="onSliderInput"
/>
<div class="scroller-index">{{ currentIndex }}/{{ totalPosts }}</div>
</div>
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
<div v-else class="scroller-time">{{ lastReplyTime }}</div>
</div>
</div>
<vue-easy-lightbox :visible="lightboxVisible" :index="lightboxIndex" :imgs="lightboxImgs"
@hide="lightboxVisible = false" />
<vue-easy-lightbox
:visible="lightboxVisible"
:index="lightboxIndex"
:imgs="lightboxImgs"
@hide="lightboxVisible = false"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, watchEffect, onActivated } from 'vue'
import {
ref,
computed,
onMounted,
onBeforeUnmount,
nextTick,
watch,
watchEffect,
onActivated,
} from 'vue'
import VueEasyLightbox from 'vue-easy-lightbox'
import { useRoute } from 'vue-router'
import CommentItem from '~/components/CommentItem.vue'
@@ -186,6 +261,7 @@ import { useIsMobile } from '~/utils/screen'
import Dropdown from '~/components/Dropdown.vue'
import { ClientOnly } from '#components'
import { useConfirm } from '~/composables/useConfirm'
const { confirm } = useConfirm()
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
@@ -213,7 +289,6 @@ const subscribed = ref(false)
const commentSort = ref('NEWEST')
const isFetchingComments = ref(false)
const isMobile = useIsMobile()
const { confirm } = useConfirm()
const headerHeight = process.client
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
@@ -624,7 +699,8 @@ const editPost = () => {
const deletePost = async () => {
try {
await confirm('确认删除', '确定要删除这篇文章吗?此操作不可撤销。')
const ok = await confirm('删除帖子', '此操作不可恢复,确认要删除吗?')
if (!ok) return
const token = getToken()
if (!token) {
toast.error('请先登录')