Merge pull request #460 from nagisa77/feature/nuxt_opt_v1

Feature/nuxt opt
This commit is contained in:
Tim
2025-08-10 12:30:22 +08:00
committed by GitHub
7 changed files with 60 additions and 33 deletions

View File

@@ -47,6 +47,10 @@ export default {
showLoginOverlay: { showLoginOverlay: {
type: Boolean, type: Boolean,
default: false default: false
},
parentUserName: {
type: String,
default: ''
} }
}, },
components: { LoginOverlay }, components: { LoginOverlay },
@@ -71,7 +75,7 @@ export default {
if (!vditorInstance.value || isDisabled.value) return if (!vditorInstance.value || isDisabled.value) return
const value = vditorInstance.value.getValue() const value = vditorInstance.value.getValue()
console.debug('CommentEditor submit', value) console.debug('CommentEditor submit', value)
emit('submit', value, () => { emit('submit', props.parentUserName, value, () => {
if (!vditorInstance.value) return if (!vditorInstance.value) return
vditorInstance.value.setValue('') vditorInstance.value.setValue('')
text.value = '' text.value = ''

View File

@@ -42,9 +42,9 @@
</div> </div>
</ReactionsGroup> </ReactionsGroup>
</div> </div>
<div class="comment-editor-wrapper"> <div class="comment-editor-wrapper" ref="editorWrapper">
<CommentEditor v-if="showEditor" @submit="submitReply" :loading="isWaitingForReply" :disabled="!loggedIn" <CommentEditor v-if="showEditor" @submit="submitReply" :loading="isWaitingForReply" :disabled="!loggedIn"
:show-login-overlay="!loggedIn" /> :show-login-overlay="!loggedIn" :parent-user-name="comment.userName" />
</div> </div>
<div v-if="replyCount && level < 2" class="reply-toggle" @click="toggleReplies"> <div v-if="replyCount && level < 2" class="reply-toggle" @click="toggleReplies">
<i v-if="showReplies" class="fas fa-chevron-up reply-toggle-icon"></i> <i v-if="showReplies" class="fas fa-chevron-up reply-toggle-icon"></i>
@@ -65,7 +65,7 @@
</template> </template>
<script> <script>
import { ref, watch, computed } from 'vue' import { ref, watch, computed, nextTick } from 'vue'
import VueEasyLightbox from 'vue-easy-lightbox' import VueEasyLightbox from 'vue-easy-lightbox'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CommentEditor from './CommentEditor.vue' import CommentEditor from './CommentEditor.vue'
@@ -106,6 +106,7 @@ const CommentItem = {
} }
) )
const showEditor = ref(false) const showEditor = ref(false)
const editorWrapper = ref(null)
const isWaitingForReply = ref(false) const isWaitingForReply = ref(false)
const lightboxVisible = ref(false) const lightboxVisible = ref(false)
const lightboxIndex = ref(0) const lightboxIndex = ref(0)
@@ -118,6 +119,11 @@ const CommentItem = {
} }
const toggleEditor = () => { const toggleEditor = () => {
showEditor.value = !showEditor.value showEditor.value = !showEditor.value
if (showEditor.value) {
setTimeout(() => {
editorWrapper.value?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}, 100)
}
} }
// 合并所有子回复为一个扁平数组 // 合并所有子回复为一个扁平数组
@@ -164,7 +170,7 @@ const CommentItem = {
toast.error('操作失败') toast.error('操作失败')
} }
} }
const submitReply = async (text, clear) => { const submitReply = async (parentUserName, text, clear) => {
if (!text.trim()) return if (!text.trim()) return
isWaitingForReply.value = true isWaitingForReply.value = true
const token = getToken() const token = getToken()
@@ -190,7 +196,9 @@ const CommentItem = {
userName: data.author.username, userName: data.author.username,
time: TimeManager.format(data.createdAt), time: TimeManager.format(data.createdAt),
avatar: data.author.avatar, avatar: data.author.avatar,
medal: data.author.displayMedal,
text: data.content, text: data.content,
parentUserName: parentUserName,
reactions: [], reactions: [],
reply: (data.replies || []).map(r => ({ reply: (data.replies || []).map(r => ({
id: r.id, id: r.id,
@@ -239,7 +247,7 @@ const CommentItem = {
lightboxVisible.value = true lightboxVisible.value = true
} }
} }
return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply, commentMenuItems, deleteComment, lightboxVisible, lightboxIndex, lightboxImgs, handleContentClick, loggedIn, replyCount, replyList, getMedalTitle } return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply, commentMenuItems, deleteComment, lightboxVisible, lightboxIndex, lightboxImgs, handleContentClick, loggedIn, replyCount, replyList, getMedalTitle, editorWrapper }
} }
} }

View File

@@ -153,11 +153,6 @@ export default {
isLoadingTag.value = false isLoadingTag.value = false
} }
onMounted(() => {
// fetchCategoryData()
// fetchTagData()
})
const iconClass = computed(() => { const iconClass = computed(() => {
switch (themeState.mode) { switch (themeState.mode) {
case ThemeMode.DARK: case ThemeMode.DARK:

View File

@@ -82,7 +82,6 @@ export default {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: this.username, password: this.password }) body: JSON.stringify({ username: this.username, password: this.password })
}) })
this.isWaitingForLogin = false
const data = await res.json() const data = await res.json()
if (res.ok && data.token) { if (res.ok && data.token) {
setToken(data.token) setToken(data.token)
@@ -103,6 +102,8 @@ export default {
} }
} catch (e) { } catch (e) {
toast.error('登录失败') toast.error('登录失败')
} finally {
this.isWaitingForLogin = false
} }
}, },

View File

@@ -21,11 +21,15 @@
</div> </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> <i class="fas fa-user-plus"></i>
<div class="article-subscribe-button-text">订阅文章</div> <div class="article-subscribe-button-text">
{{ isMobile ? '订阅' : '订阅文章' }}
</div>
</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> <i class="fas fa-user-minus"></i>
<div class="article-unsubscribe-button-text">取消订阅</div> <div class="article-unsubscribe-button-text">
{{ isMobile ? '退订' : '取消订阅' }}
</div>
</div> </div>
<DropdownMenu v-if="articleMenuItems.length > 0" :items="articleMenuItems"> <DropdownMenu v-if="articleMenuItems.length > 0" :items="articleMenuItems">
<template #trigger> <template #trigger>
@@ -44,11 +48,8 @@
<div class="user-name"> <div class="user-name">
{{ author.username }} {{ author.username }}
<i class="fas fa-medal medal-icon"></i> <i class="fas fa-medal medal-icon"></i>
<router-link <router-link v-if="author.displayMedal" class="user-medal" :to="`/users/${author.id}?tab=achievements`">{{
v-if="author.displayMedal" getMedalTitle(author.displayMedal) }}</router-link>
class="user-medal"
:to="`/users/${author.id}?tab=achievements`"
>{{ getMedalTitle(author.displayMedal) }}</router-link>
</div> </div>
<div class="post-time">{{ postTime }}</div> <div class="post-time">{{ postTime }}</div>
</div> </div>
@@ -59,11 +60,8 @@
<div class="user-name"> <div class="user-name">
{{ author.username }} {{ author.username }}
<i class="fas fa-medal medal-icon"></i> <i class="fas fa-medal medal-icon"></i>
<router-link <router-link v-if="author.displayMedal" class="user-medal" :to="`/users/${author.id}?tab=achievements`">{{
v-if="author.displayMedal" getMedalTitle(author.displayMedal) }}</router-link>
class="user-medal"
:to="`/users/${author.id}?tab=achievements`"
>{{ getMedalTitle(author.displayMedal) }}</router-link>
</div> </div>
<div class="post-time">{{ postTime }}</div> <div class="post-time">{{ postTime }}</div>
</div> </div>
@@ -334,7 +332,6 @@ export default {
category.value = data.category category.value = data.category
tags.value = data.tags || [] tags.value = data.tags || []
postReactions.value = data.reactions || [] postReactions.value = data.reactions || []
await fetchComments()
subscribed.value = !!data.subscribed subscribed.value = !!data.subscribed
status.value = data.status status.value = data.status
pinnedAt.value = data.pinnedAt pinnedAt.value = data.pinnedAt
@@ -593,7 +590,6 @@ export default {
const hash = location.hash const hash = location.hash
if (hash.startsWith('#comment-')) { if (hash.startsWith('#comment-')) {
const id = hash.substring('#comment-'.length) const id = hash.substring('#comment-'.length)
// 不清楚啥原因先wait一下子不然会定不准 😅
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 500))
const el = document.getElementById('comment-' + id) const el = document.getElementById('comment-' + id)
if (el) { if (el) {
@@ -609,17 +605,18 @@ export default {
router.push(`/users/${author.value.id}`) router.push(`/users/${author.value.id}`)
} }
await fetchPost()
onMounted(async () => { onMounted(async () => {
await fetchComments()
const hash = location.hash const hash = location.hash
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
if (id) expandCommentPath(id) if (id) expandCommentPath(id)
updateCurrentIndex() updateCurrentIndex()
window.addEventListener('scroll', updateCurrentIndex) window.addEventListener('scroll', updateCurrentIndex)
await jumpToHashComment() jumpToHashComment()
}) })
await fetchPost()
return { return {
postContent, postContent,
author, author,

View File

@@ -187,7 +187,6 @@ export default {
username: this.username username: this.username
}) })
}) })
this.isWaitingForEmailVerified = false
const data = await res.json() const data = await res.json()
if (res.ok) { if (res.ok) {
if (this.registerMode === 'WHITELIST') { if (this.registerMode === 'WHITELIST') {
@@ -201,6 +200,8 @@ export default {
} }
} catch (e) { } catch (e) {
toast.error('注册失败') toast.error('注册失败')
} finally {
this.isWaitingForEmailVerified = false
} }
}, },
signupWithGithub() { signupWithGithub() {

View File

@@ -0,0 +1,21 @@
import { clearToken } from '~/utils/auth'
export default defineNuxtPlugin(() => {
if (process.client) {
const originalFetch = window.fetch
window.fetch = async (input, init) => {
const response = await originalFetch(input, init)
if (response.status === 401) {
try {
const data = await response.clone().json()
if (data && data.error === 'Invalid or expired token') {
clearToken()
}
} catch (e) {
// ignore JSON parsing errors
}
}
return response
}
}
})