mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-20 05:50:53 +08:00
Compare commits
7 Commits
feature/fi
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7ff73c7f9 | ||
|
|
4ee9532d5f | ||
|
|
80c3fd8ea2 | ||
|
|
7e277d06d5 | ||
|
|
d2b68119bd | ||
|
|
f7b0d7edd5 | ||
|
|
cdea1ab911 |
@@ -8,7 +8,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<span v-if="isMobile && unreadCount > 0" class="menu-unread-dot"></span>
|
<span v-if="isMobile && unreadCount > 0" class="menu-unread-dot"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="logo-container" @click="goToHome">
|
<NuxtLink class="logo-container" to="/" replace @click="handleLogoClick">
|
||||||
<img
|
<img
|
||||||
alt="OpenIsle"
|
alt="OpenIsle"
|
||||||
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
height="60"
|
height="60"
|
||||||
/>
|
/>
|
||||||
<div class="logo-text">OpenIsle</div>
|
<div class="logo-text">OpenIsle</div>
|
||||||
</div>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
@@ -74,11 +74,10 @@ const searchDropdown = ref(null)
|
|||||||
const userMenu = ref(null)
|
const userMenu = ref(null)
|
||||||
const menuBtn = ref(null)
|
const menuBtn = ref(null)
|
||||||
|
|
||||||
const goToHome = async () => {
|
const handleLogoClick = (event) => {
|
||||||
if (router.currentRoute.value.fullPath === '/') {
|
if (router.currentRoute.value.fullPath === '/') {
|
||||||
|
event.preventDefault()
|
||||||
window.dispatchEvent(new Event('refresh-home'))
|
window.dispatchEvent(new Event('refresh-home'))
|
||||||
} else {
|
|
||||||
await navigateTo('/', { replace: true })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const search = () => {
|
const search = () => {
|
||||||
@@ -184,6 +183,8 @@ onMounted(async () => {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLoadingPosts && articles.length === 0" class="loading-container">
|
<div v-if="pendingFirst" class="loading-container">
|
||||||
<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>
|
||||||
|
|
||||||
@@ -60,7 +60,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="article-item" v-for="article in articles" :key="article.id">
|
<div
|
||||||
|
v-if="!pendingFirst"
|
||||||
|
class="article-item"
|
||||||
|
v-for="article in articles"
|
||||||
|
:key="article.id"
|
||||||
|
>
|
||||||
<div class="article-main-container">
|
<div class="article-main-container">
|
||||||
<NuxtLink class="article-item-title main-item" :to="`/posts/${article.id}`">
|
<NuxtLink class="article-item-title main-item" :to="`/posts/${article.id}`">
|
||||||
<i v-if="article.pinned" class="fas fa-thumbtack pinned-icon"></i>
|
<i v-if="article.pinned" class="fas fa-thumbtack pinned-icon"></i>
|
||||||
@@ -104,15 +109,14 @@
|
|||||||
热门帖子功能开发中,敬请期待。
|
热门帖子功能开发中,敬请期待。
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="placeholder-container">分类浏览功能开发中,敬请期待。</div>
|
<div v-else class="placeholder-container">分类浏览功能开发中,敬请期待。</div>
|
||||||
<div v-if="isLoadingPosts && articles.length > 0" class="loading-container bottom-loading">
|
<div v-if="isLoadingMore && articles.length > 0" class="loading-container bottom-loading">
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, watch, watchEffect, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||||
import ArticleTags from '~/components/ArticleTags.vue'
|
import ArticleTags from '~/components/ArticleTags.vue'
|
||||||
import CategorySelect from '~/components/CategorySelect.vue'
|
import CategorySelect from '~/components/CategorySelect.vue'
|
||||||
@@ -142,7 +146,9 @@ const selectedTags = ref([])
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const tagOptions = ref([])
|
const tagOptions = ref([])
|
||||||
const categoryOptions = ref([])
|
const categoryOptions = ref([])
|
||||||
const isLoadingPosts = ref(false)
|
|
||||||
|
const isLoadingMore = ref(false)
|
||||||
|
|
||||||
const topics = ref(['最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
const topics = ref(['最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
||||||
const selectedTopic = ref(
|
const selectedTopic = ref(
|
||||||
route.query.view === 'ranking' ? '排行榜' : route.query.view === 'latest' ? '最新' : '最新回复',
|
route.query.view === 'ranking' ? '排行榜' : route.query.view === 'latest' ? '最新' : '最新回复',
|
||||||
@@ -153,11 +159,11 @@ const pageSize = 10
|
|||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const allLoaded = ref(false)
|
const allLoaded = ref(false)
|
||||||
|
|
||||||
|
/** URL 参数 -> 本地筛选值 **/
|
||||||
const selectedCategorySet = (category) => {
|
const selectedCategorySet = (category) => {
|
||||||
const c = decodeURIComponent(category)
|
const c = decodeURIComponent(category)
|
||||||
selectedCategory.value = isNaN(c) ? c : Number(c)
|
selectedCategory.value = isNaN(c) ? c : Number(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTagsSet = (tags) => {
|
const selectedTagsSet = (tags) => {
|
||||||
const t = Array.isArray(tags) ? tags.join(',') : tags
|
const t = Array.isArray(tags) ? tags.join(',') : tags
|
||||||
selectedTags.value = t
|
selectedTags.value = t
|
||||||
@@ -167,23 +173,17 @@ const selectedTagsSet = (tags) => {
|
|||||||
.map((v) => (isNaN(v) ? v : Number(v)))
|
.map((v) => (isNaN(v) ? v : Number(v)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 初始化:仅在客户端首渲染时根据路由同步一次 **/
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const query = route.query
|
const { category, tags } = route.query
|
||||||
const category = query.category
|
if (category) selectedCategorySet(category)
|
||||||
const tags = query.tags
|
if (tags) selectedTagsSet(tags)
|
||||||
|
|
||||||
if (category) {
|
|
||||||
selectedCategorySet(category)
|
|
||||||
}
|
|
||||||
if (tags) {
|
|
||||||
selectedTagsSet(tags)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 路由变更时同步筛选 **/
|
||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => route.query,
|
||||||
() => {
|
(query) => {
|
||||||
const query = route.query
|
|
||||||
const category = query.category
|
const category = query.category
|
||||||
const tags = query.tags
|
const tags = query.tags
|
||||||
category && selectedCategorySet(category)
|
category && selectedCategorySet(category)
|
||||||
@@ -191,18 +191,14 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 选项加载(分类/标签名称回填) **/
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/categories/${selectedCategory.value}`)
|
const res = await fetch(`${API_BASE_URL}/api/categories/${selectedCategory.value}`)
|
||||||
if (res.ok) {
|
if (res.ok) categoryOptions.value = [await res.json()]
|
||||||
categoryOptions.value = [await res.json()]
|
} catch {}
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTags.value.length) {
|
if (selectedTags.value.length) {
|
||||||
const arr = []
|
const arr = []
|
||||||
for (const t of selectedTags.value) {
|
for (const t of selectedTags.value) {
|
||||||
@@ -210,218 +206,158 @@ const loadOptions = async () => {
|
|||||||
try {
|
try {
|
||||||
const r = await fetch(`${API_BASE_URL}/api/tags/${t}`)
|
const r = await fetch(`${API_BASE_URL}/api/tags/${t}`)
|
||||||
if (r.ok) arr.push(await r.json())
|
if (r.ok) arr.push(await r.json())
|
||||||
} catch (e) {
|
} catch {}
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tagOptions.value = arr
|
tagOptions.value = arr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildUrl = () => {
|
/** 列表 API 路径与查询参数 **/
|
||||||
let url = `${API_BASE_URL}/api/posts?page=${page.value}&pageSize=${pageSize}`
|
const baseQuery = computed(() => ({
|
||||||
if (selectedCategory.value) {
|
categoryId: selectedCategory.value || undefined,
|
||||||
url += `&categoryId=${selectedCategory.value}`
|
tagIds: selectedTags.value.length ? selectedTags.value : undefined,
|
||||||
}
|
}))
|
||||||
if (selectedTags.value.length) {
|
const listApiPath = computed(() => {
|
||||||
selectedTags.value.forEach((t) => {
|
if (selectedTopic.value === '排行榜') return '/api/posts/ranking'
|
||||||
url += `&tagIds=${t}`
|
if (selectedTopic.value === '最新回复') return '/api/posts/latest-reply'
|
||||||
})
|
return '/api/posts'
|
||||||
}
|
})
|
||||||
return url
|
const buildUrl = ({ pageNo }) => {
|
||||||
|
const url = new URL(`${API_BASE_URL}${listApiPath.value}`)
|
||||||
|
url.searchParams.set('page', pageNo)
|
||||||
|
url.searchParams.set('pageSize', pageSize)
|
||||||
|
if (baseQuery.value.categoryId) url.searchParams.set('categoryId', baseQuery.value.categoryId)
|
||||||
|
if (baseQuery.value.tagIds)
|
||||||
|
for (const t of baseQuery.value.tagIds) url.searchParams.append('tagIds', t)
|
||||||
|
return url.toString()
|
||||||
}
|
}
|
||||||
|
const tokenHeader = computed(() => {
|
||||||
const buildRankUrl = () => {
|
const token = getToken()
|
||||||
let url = `${API_BASE_URL}/api/posts/ranking?page=${page.value}&pageSize=${pageSize}`
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
if (selectedCategory.value) {
|
|
||||||
url += `&categoryId=${selectedCategory.value}`
|
|
||||||
}
|
|
||||||
if (selectedTags.value.length) {
|
|
||||||
selectedTags.value.forEach((t) => {
|
|
||||||
url += `&tagIds=${t}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildReplyUrl = () => {
|
|
||||||
let url = `${API_BASE_URL}/api/posts/latest-reply?page=${page.value}&pageSize=${pageSize}`
|
|
||||||
if (selectedCategory.value) {
|
|
||||||
url += `&categoryId=${selectedCategory.value}`
|
|
||||||
}
|
|
||||||
if (selectedTags.value.length) {
|
|
||||||
selectedTags.value.forEach((t) => {
|
|
||||||
url += `&tagIds=${t}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPosts = async (reset = false) => {
|
|
||||||
if (reset) {
|
|
||||||
page.value = 0
|
|
||||||
allLoaded.value = false
|
|
||||||
articles.value = []
|
|
||||||
}
|
|
||||||
if (isLoadingPosts.value || allLoaded.value) return
|
|
||||||
try {
|
|
||||||
isLoadingPosts.value = true
|
|
||||||
const token = getToken()
|
|
||||||
const res = await fetch(buildUrl(), {
|
|
||||||
headers: {
|
|
||||||
Authorization: token ? `Bearer ${token}` : '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
isLoadingPosts.value = false
|
|
||||||
if (!res.ok) return
|
|
||||||
const data = await res.json()
|
|
||||||
articles.value.push(
|
|
||||||
...data.map((p) => ({
|
|
||||||
id: p.id,
|
|
||||||
title: p.title,
|
|
||||||
description: p.content,
|
|
||||||
category: p.category,
|
|
||||||
tags: p.tags || [],
|
|
||||||
members: (p.participants || []).map((m) => ({ id: m.id, avatar: m.avatar })),
|
|
||||||
comments: p.commentCount,
|
|
||||||
views: p.views,
|
|
||||||
time: TimeManager.format(p.createdAt),
|
|
||||||
pinned: !!p.pinnedAt,
|
|
||||||
type: p.type,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
if (data.length < pageSize) {
|
|
||||||
allLoaded.value = true
|
|
||||||
} else {
|
|
||||||
page.value += 1
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRanking = async (reset = false) => {
|
|
||||||
if (reset) {
|
|
||||||
page.value = 0
|
|
||||||
allLoaded.value = false
|
|
||||||
articles.value = []
|
|
||||||
}
|
|
||||||
if (isLoadingPosts.value || allLoaded.value) return
|
|
||||||
try {
|
|
||||||
isLoadingPosts.value = true
|
|
||||||
const token = getToken()
|
|
||||||
const res = await fetch(buildRankUrl(), {
|
|
||||||
headers: {
|
|
||||||
Authorization: token ? `Bearer ${token}` : '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
isLoadingPosts.value = false
|
|
||||||
if (!res.ok) return
|
|
||||||
const data = await res.json()
|
|
||||||
articles.value.push(
|
|
||||||
...data.map((p) => ({
|
|
||||||
id: p.id,
|
|
||||||
title: p.title,
|
|
||||||
description: p.content,
|
|
||||||
category: p.category,
|
|
||||||
tags: p.tags || [],
|
|
||||||
members: (p.participants || []).map((m) => ({ id: m.id, avatar: m.avatar })),
|
|
||||||
comments: p.commentCount,
|
|
||||||
views: p.views,
|
|
||||||
time: TimeManager.format(p.createdAt),
|
|
||||||
pinned: !!p.pinnedAt,
|
|
||||||
type: p.type,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
if (data.length < pageSize) {
|
|
||||||
allLoaded.value = true
|
|
||||||
} else {
|
|
||||||
page.value += 1
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchLatestReply = async (reset = false) => {
|
|
||||||
if (reset) {
|
|
||||||
page.value = 0
|
|
||||||
allLoaded.value = false
|
|
||||||
articles.value = []
|
|
||||||
}
|
|
||||||
if (isLoadingPosts.value || allLoaded.value) return
|
|
||||||
try {
|
|
||||||
isLoadingPosts.value = true
|
|
||||||
const token = getToken()
|
|
||||||
const res = await fetch(buildReplyUrl(), {
|
|
||||||
headers: {
|
|
||||||
Authorization: token ? `Bearer ${token}` : '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
isLoadingPosts.value = false
|
|
||||||
if (!res.ok) return
|
|
||||||
const data = await res.json()
|
|
||||||
articles.value.push(
|
|
||||||
...data.map((p) => ({
|
|
||||||
id: p.id,
|
|
||||||
title: p.title,
|
|
||||||
description: p.content,
|
|
||||||
category: p.category,
|
|
||||||
tags: p.tags || [],
|
|
||||||
members: (p.participants || []).map((m) => ({ id: m.id, avatar: m.avatar })),
|
|
||||||
comments: p.commentCount,
|
|
||||||
views: p.views,
|
|
||||||
time: TimeManager.format(p.lastReplyAt || p.createdAt),
|
|
||||||
pinned: !!p.pinnedAt,
|
|
||||||
type: p.type,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
if (data.length < pageSize) {
|
|
||||||
allLoaded.value = true
|
|
||||||
} else {
|
|
||||||
page.value += 1
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchContent = async (reset = false) => {
|
|
||||||
if (selectedTopic.value === '排行榜') {
|
|
||||||
await fetchRanking(reset)
|
|
||||||
} else if (selectedTopic.value === '最新回复') {
|
|
||||||
await fetchLatestReply(reset)
|
|
||||||
} else {
|
|
||||||
await fetchPosts(reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshHome = () => {
|
|
||||||
fetchContent(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('refresh-home', refreshHome)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
/** —— 首屏数据托管(SSR) —— **/
|
||||||
window.removeEventListener('refresh-home', refreshHome)
|
const asyncKey = computed(() => [
|
||||||
})
|
'home:firstpage',
|
||||||
|
selectedTopic.value,
|
||||||
|
String(baseQuery.value.categoryId ?? ''),
|
||||||
|
JSON.stringify(baseQuery.value.tagIds ?? []),
|
||||||
|
])
|
||||||
|
const {
|
||||||
|
data: firstPage,
|
||||||
|
pending: pendingFirst,
|
||||||
|
refresh: refreshFirst,
|
||||||
|
} = await useAsyncData(
|
||||||
|
() => asyncKey.value.join('::'),
|
||||||
|
async () => {
|
||||||
|
const res = await $fetch(buildUrl({ pageNo: 0 }), { headers: tokenHeader.value })
|
||||||
|
const data = Array.isArray(res) ? res : []
|
||||||
|
return data.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
title: p.title,
|
||||||
|
description: p.content,
|
||||||
|
category: p.category,
|
||||||
|
tags: p.tags || [],
|
||||||
|
members: (p.participants || []).map((m) => ({ id: m.id, avatar: m.avatar })),
|
||||||
|
comments: p.commentCount,
|
||||||
|
views: p.views,
|
||||||
|
time: TimeManager.format(
|
||||||
|
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
||||||
|
),
|
||||||
|
pinned: !!p.pinnedAt,
|
||||||
|
type: p.type,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: true,
|
||||||
|
default: () => [],
|
||||||
|
watch: [selectedTopic, baseQuery],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
useScrollLoadMore(fetchContent)
|
/** 首屏/筛选变更:重置分页并灌入 firstPage **/
|
||||||
|
watch(
|
||||||
|
firstPage,
|
||||||
|
(data) => {
|
||||||
|
page.value = 0
|
||||||
|
articles.value = [...(data || [])]
|
||||||
|
allLoaded.value = (data?.length || 0) < pageSize
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
/** —— 滚动加载更多 —— **/
|
||||||
|
let inflight = null
|
||||||
|
const fetchNextPage = async () => {
|
||||||
|
if (allLoaded.value || pendingFirst.value || inflight) return
|
||||||
|
const nextPage = page.value + 1
|
||||||
|
isLoadingMore.value = true
|
||||||
|
inflight = $fetch(buildUrl({ pageNo: nextPage }), { headers: tokenHeader.value })
|
||||||
|
.then((res) => {
|
||||||
|
const data = Array.isArray(res) ? res : []
|
||||||
|
const mapped = data.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
title: p.title,
|
||||||
|
description: p.content,
|
||||||
|
category: p.category,
|
||||||
|
tags: p.tags || [],
|
||||||
|
members: (p.participants || []).map((m) => ({ id: m.id, avatar: m.avatar })),
|
||||||
|
comments: p.commentCount,
|
||||||
|
views: p.views,
|
||||||
|
time: TimeManager.format(
|
||||||
|
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
||||||
|
),
|
||||||
|
pinned: !!p.pinnedAt,
|
||||||
|
type: p.type,
|
||||||
|
}))
|
||||||
|
articles.value.push(...mapped)
|
||||||
|
if (data.length < pageSize) {
|
||||||
|
allLoaded.value = true
|
||||||
|
} else {
|
||||||
|
page.value = nextPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
inflight = null
|
||||||
|
isLoadingMore.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定滚动加载(避免挂载瞬间触发) **/
|
||||||
|
let initialReady = false
|
||||||
|
const loadMoreGuarded = async () => {
|
||||||
|
if (!initialReady) return
|
||||||
|
await fetchNextPage()
|
||||||
|
}
|
||||||
|
useScrollLoadMore(loadMoreGuarded)
|
||||||
|
watch(
|
||||||
|
articles,
|
||||||
|
() => {
|
||||||
|
if (!initialReady && articles.value.length) initialReady = true
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 切换分类/标签/Tab:useAsyncData 已 watch,这里只需确保 options 加载 **/
|
||||||
watch([selectedCategory, selectedTags], () => {
|
watch([selectedCategory, selectedTags], () => {
|
||||||
fetchContent(true)
|
loadOptions()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(selectedTopic, () => {
|
watch(selectedTopic, () => {
|
||||||
fetchContent(true)
|
// 仅当需要额外选项时加载
|
||||||
|
loadOptions()
|
||||||
})
|
})
|
||||||
|
|
||||||
const sanitizeDescription = (text) => stripMarkdown(text)
|
/** 选项首屏加载:服务端执行一次;客户端兜底 **/
|
||||||
|
if (import.meta.server) {
|
||||||
|
await loadOptions()
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
if (categoryOptions.value.length === 0 && tagOptions.value.length === 0) loadOptions()
|
||||||
|
})
|
||||||
|
|
||||||
await Promise.all([loadOptions(), fetchContent()])
|
/** 其他工具函数 **/
|
||||||
|
const sanitizeDescription = (text) => stripMarkdown(text)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user