mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-05-10 20:57:28 +08:00
Merge pull request #460 from nagisa77/feature/nuxt_opt_v1
Feature/nuxt opt
This commit is contained in:
@@ -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 = ''
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -93,7 +91,7 @@
|
|||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="comments-container">
|
<div v-else class="comments-container">
|
||||||
<BaseTimeline :items="comments">
|
<BaseTimeline :items="comments">
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<CommentItem :key="item.id" :comment="item" :level="0" :default-show-replies="item.openReplies"
|
<CommentItem :key="item.id" :comment="item" :level="0" :default-show-replies="item.openReplies"
|
||||||
@deleted="onCommentDeleted" />
|
@deleted="onCommentDeleted" />
|
||||||
@@ -197,7 +195,7 @@ export default {
|
|||||||
window.removeEventListener('scroll', updateCurrentIndex)
|
window.removeEventListener('scroll', updateCurrentIndex)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lightboxVisible = ref(false)
|
const lightboxVisible = ref(false)
|
||||||
const lightboxIndex = ref(0)
|
const lightboxIndex = ref(0)
|
||||||
const lightboxImgs = ref([])
|
const lightboxImgs = ref([])
|
||||||
@@ -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
|
||||||
@@ -498,7 +495,7 @@ export default {
|
|||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editPost = () => {
|
const editPost = () => {
|
||||||
router.push(`/posts/${postId}/edit`)
|
router.push(`/posts/${postId}/edit`)
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
21
frontend_nuxt/plugins/auth-fetch.client.ts
Normal file
21
frontend_nuxt/plugins/auth-fetch.client.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user