mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-08 13:00:45 +08:00
Merge pull request #432 from nagisa77/feature/nuxt_optimization
Feature: nuxt optimization
This commit is contained in:
@@ -76,7 +76,9 @@ export default {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const goToHome = () => {
|
const goToHome = () => {
|
||||||
router.push('/')
|
router.push('/').then(() => {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const search = () => {
|
const search = () => {
|
||||||
showSearch.value = true
|
showSearch.value = true
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<div v-if="isLoadingCategory" class="menu-loading-container">
|
<div v-if="isLoadingCategory" class="menu-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>
|
||||||
<div v-else v-for="c in categories" :key="c.id" class="section-item" @click="gotoCategory(c)">
|
<div v-else v-for="c in categoryData" :key="c.id" class="section-item" @click="gotoCategory(c)">
|
||||||
<template v-if="c.smallIcon || c.icon">
|
<template v-if="c.smallIcon || c.icon">
|
||||||
<img v-if="isImageIcon(c.smallIcon || c.icon)" :src="c.smallIcon || c.icon" class="section-item-icon" :alt="c.name" />
|
<img v-if="isImageIcon(c.smallIcon || c.icon)" :src="c.smallIcon || c.icon" class="section-item-icon" :alt="c.name" />
|
||||||
<i v-else :class="['section-item-icon', c.smallIcon || c.icon]"></i>
|
<i v-else :class="['section-item-icon', c.smallIcon || c.icon]"></i>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
<div v-if="isLoadingTag" class="menu-loading-container">
|
<div v-if="isLoadingTag" class="menu-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>
|
||||||
<div v-else v-for="t in tags" :key="t.id" class="section-item" @click="gotoTag(t)">
|
<div v-else v-for="t in tagData" :key="t.id" class="section-item" @click="gotoTag(t)">
|
||||||
<img v-if="isImageIcon(t.smallIcon || t.icon)" :src="t.smallIcon || t.icon" class="section-item-icon" :alt="t.name" />
|
<img v-if="isImageIcon(t.smallIcon || t.icon)" :src="t.smallIcon || t.icon" class="section-item-icon" :alt="t.name" />
|
||||||
<i v-else class="section-item-icon fas fa-hashtag"></i>
|
<i v-else class="section-item-icon fas fa-hashtag"></i>
|
||||||
<span class="section-item-text">{{ t.name }} <span class="section-item-text-count">x {{ t.count
|
<span class="section-item-text">{{ t.name }} <span class="section-item-text-count">x {{ t.count
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
|
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
|
||||||
import { authState } from '~/utils/auth'
|
import { authState } from '~/utils/auth'
|
||||||
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||||
import { watch } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { API_BASE_URL } from '~/main'
|
import { API_BASE_URL } from '~/main'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -126,18 +126,39 @@ export default {
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
async setup(props, { emit }) {
|
||||||
return {
|
const router = useRouter()
|
||||||
categories: [],
|
const categories = ref([])
|
||||||
tags: [],
|
const tags = ref([])
|
||||||
categoryOpen: true,
|
const categoryOpen = ref(true)
|
||||||
tagOpen: true,
|
const tagOpen = ref(true)
|
||||||
isLoadingCategory: false,
|
const isLoadingCategory = ref(false)
|
||||||
isLoadingTag: false
|
const isLoadingTag = ref(false)
|
||||||
|
const categoryData = ref([])
|
||||||
|
const tagData = ref([])
|
||||||
|
|
||||||
|
const fetchCategoryData = async () => {
|
||||||
|
isLoadingCategory.value = true
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/categories`)
|
||||||
|
const data = await res.json()
|
||||||
|
categoryData.value = data
|
||||||
|
isLoadingCategory.value = false
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
const fetchTagData = async () => {
|
||||||
iconClass() {
|
isLoadingTag.value = true
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/tags?limit=10`)
|
||||||
|
const data = await res.json()
|
||||||
|
tagData.value = data
|
||||||
|
isLoadingTag.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// fetchCategoryData()
|
||||||
|
// fetchTagData()
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconClass = computed(() => {
|
||||||
switch (themeState.mode) {
|
switch (themeState.mode) {
|
||||||
case ThemeMode.DARK:
|
case ThemeMode.DARK:
|
||||||
return 'fas fa-moon'
|
return 'fas fa-moon'
|
||||||
@@ -146,18 +167,14 @@ export default {
|
|||||||
default:
|
default:
|
||||||
return 'fas fa-desktop'
|
return 'fas fa-desktop'
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
unreadCount() {
|
|
||||||
return notificationState.unreadCount
|
const unreadCount = computed(() => notificationState.unreadCount)
|
||||||
},
|
const showUnreadCount = computed(() =>
|
||||||
showUnreadCount() {
|
unreadCount.value > 99 ? '99+' : unreadCount.value
|
||||||
return this.unreadCount > 99 ? '99+' : this.unreadCount
|
)
|
||||||
},
|
const shouldShowStats = computed(() => authState.role === 'ADMIN')
|
||||||
shouldShowStats() {
|
|
||||||
return authState.role === 'ADMIN'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
const updateCount = async () => {
|
const updateCount = async () => {
|
||||||
if (authState.loggedIn) {
|
if (authState.loggedIn) {
|
||||||
await fetchUnreadCount()
|
await fetchUnreadCount()
|
||||||
@@ -165,94 +182,64 @@ export default {
|
|||||||
notificationState.unreadCount = 0
|
notificationState.unreadCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => authState.loggedIn, async () => {
|
onMounted(async () => {
|
||||||
await updateCount()
|
await updateCount()
|
||||||
|
watch(() => authState.loggedIn, updateCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
const CAT_CACHE_KEY = 'menu-categories'
|
const handleHomeClick = () => {
|
||||||
const TAG_CACHE_KEY = 'menu-tags'
|
router.push('/').then(() => {
|
||||||
|
|
||||||
const cachedCategories = localStorage.getItem(CAT_CACHE_KEY)
|
|
||||||
if (cachedCategories) {
|
|
||||||
try {
|
|
||||||
this.categories = JSON.parse(cachedCategories)
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachedTags = localStorage.getItem(TAG_CACHE_KEY)
|
|
||||||
if (cachedTags) {
|
|
||||||
try {
|
|
||||||
this.tags = JSON.parse(cachedTags)
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoadingCategory = !cachedCategories
|
|
||||||
this.isLoadingTag = !cachedTags
|
|
||||||
|
|
||||||
const fetchCategories = () => {
|
|
||||||
fetch(`${API_BASE_URL}/api/categories`).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
res.json().then(data => {
|
|
||||||
this.categories = data.slice(0, 10)
|
|
||||||
localStorage.setItem(CAT_CACHE_KEY, JSON.stringify(this.categories))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.isLoadingCategory = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchTags = () => {
|
|
||||||
fetch(`${API_BASE_URL}/api/tags?limit=10`).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
res.json().then(data => {
|
|
||||||
this.tags = data
|
|
||||||
localStorage.setItem(TAG_CACHE_KEY, JSON.stringify(this.tags))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.isLoadingTag = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedCategories) {
|
|
||||||
setTimeout(fetchCategories, 1500)
|
|
||||||
} else {
|
|
||||||
fetchCategories()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedTags) {
|
|
||||||
setTimeout(fetchTags, 1500)
|
|
||||||
} else {
|
|
||||||
fetchTags()
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateCount()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
cycleTheme,
|
|
||||||
handleHomeClick() {
|
|
||||||
this.$router.push('/').then(() => {
|
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
handleItemClick() {
|
|
||||||
if (window.innerWidth <= 768) this.$emit('item-click')
|
const handleItemClick = () => {
|
||||||
},
|
if (window.innerWidth <= 768) emit('item-click')
|
||||||
isImageIcon(icon) {
|
}
|
||||||
|
|
||||||
|
const isImageIcon = (icon) => {
|
||||||
if (!icon) return false
|
if (!icon) return false
|
||||||
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
||||||
},
|
}
|
||||||
gotoCategory(c) {
|
|
||||||
|
const gotoCategory = (c) => {
|
||||||
const value = encodeURIComponent(c.id ?? c.name)
|
const value = encodeURIComponent(c.id ?? c.name)
|
||||||
this.$router
|
router
|
||||||
.push({ path: '/', query: { category: value } })
|
.push({ path: '/', query: { category: value } }).then(() => {
|
||||||
this.handleItemClick()
|
window.location.reload()
|
||||||
},
|
})
|
||||||
gotoTag(t) {
|
handleItemClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
const gotoTag = (t) => {
|
||||||
const value = encodeURIComponent(t.id ?? t.name)
|
const value = encodeURIComponent(t.id ?? t.name)
|
||||||
this.$router
|
router
|
||||||
.push({ path: '/', query: { tags: value } })
|
.push({ path: '/', query: { tags: value } }).then(() => {
|
||||||
this.handleItemClick()
|
window.location.reload()
|
||||||
|
})
|
||||||
|
handleItemClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([fetchCategoryData(), fetchTagData()])
|
||||||
|
|
||||||
|
return {
|
||||||
|
categoryData,
|
||||||
|
tagData,
|
||||||
|
categoryOpen,
|
||||||
|
tagOpen,
|
||||||
|
isLoadingCategory,
|
||||||
|
isLoadingTag,
|
||||||
|
iconClass,
|
||||||
|
unreadCount,
|
||||||
|
showUnreadCount,
|
||||||
|
shouldShowStats,
|
||||||
|
cycleTheme,
|
||||||
|
handleHomeClick,
|
||||||
|
handleItemClick,
|
||||||
|
isImageIcon,
|
||||||
|
gotoCategory,
|
||||||
|
gotoTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const API_BASE_URL = 'https://www.open-isle.com'
|
// export const API_BASE_URL = 'https://www.open-isle.com'
|
||||||
// export const API_BASE_URL = 'http://127.0.0.1:8081'
|
export const API_BASE_URL = 'http://127.0.0.1:8081'
|
||||||
export const GOOGLE_CLIENT_ID = '777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com'
|
export const GOOGLE_CLIENT_ID = '777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com'
|
||||||
export const GITHUB_CLIENT_ID = 'Ov23liVkO1NPAX5JyWxJ'
|
export const GITHUB_CLIENT_ID = 'Ov23liVkO1NPAX5JyWxJ'
|
||||||
export const DISCORD_CLIENT_ID = '1394985417044000779'
|
export const DISCORD_CLIENT_ID = '1394985417044000779'
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useScrollLoadMore } from '~/utils/loadMore'
|
import { useScrollLoadMore } from '~/utils/loadMore'
|
||||||
import { stripMarkdown } from '~/utils/markdown'
|
import { stripMarkdown } from '~/utils/markdown'
|
||||||
@@ -131,12 +131,8 @@ export default {
|
|||||||
SearchDropdown,
|
SearchDropdown,
|
||||||
ClientOnly: () => import('vue').then(m => m.defineAsyncComponent(() => import('vue').then(() => ({ template: '<slot />' }))))
|
ClientOnly: () => import('vue').then(m => m.defineAsyncComponent(() => import('vue').then(() => ({ template: '<slot />' }))))
|
||||||
},
|
},
|
||||||
setup() {
|
async setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
/**
|
|
||||||
* -------- 1. PARAMS & REFS --------
|
|
||||||
*/
|
|
||||||
const selectedCategory = ref('')
|
const selectedCategory = ref('')
|
||||||
if (route.query.category) {
|
if (route.query.category) {
|
||||||
const c = decodeURIComponent(route.query.category)
|
const c = decodeURIComponent(route.query.category)
|
||||||
@@ -169,19 +165,6 @@ export default {
|
|||||||
const pageSize = 10
|
const pageSize = 10
|
||||||
const allLoaded = ref(false)
|
const allLoaded = ref(false)
|
||||||
|
|
||||||
/**
|
|
||||||
* -------- 2. CLIENT‑SIDE ONLY: LDRS REGISTER --------
|
|
||||||
* 这里使用动态 import 避免 SSR 阶段触发 HTMLElement 未定义错误。
|
|
||||||
*/
|
|
||||||
onMounted(async () => {
|
|
||||||
// 首次加载
|
|
||||||
fetchContent()
|
|
||||||
await loadOptions()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* -------- 3. FETCH OPTION HELPERS --------
|
|
||||||
*/
|
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
||||||
try {
|
try {
|
||||||
@@ -245,9 +228,6 @@ export default {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* -------- 4. FETCH CORE --------
|
|
||||||
*/
|
|
||||||
const fetchPosts = async (reset = false) => {
|
const fetchPosts = async (reset = false) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page.value = 0
|
page.value = 0
|
||||||
@@ -299,7 +279,12 @@ export default {
|
|||||||
if (isLoadingPosts.value || allLoaded.value) return
|
if (isLoadingPosts.value || allLoaded.value) return
|
||||||
try {
|
try {
|
||||||
isLoadingPosts.value = true
|
isLoadingPosts.value = true
|
||||||
const res = await fetch(buildRankUrl())
|
const token = getToken()
|
||||||
|
const res = await fetch(buildRankUrl(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: token ? `Bearer ${token}` : ''
|
||||||
|
}
|
||||||
|
})
|
||||||
isLoadingPosts.value = false
|
isLoadingPosts.value = false
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -336,7 +321,12 @@ export default {
|
|||||||
if (isLoadingPosts.value || allLoaded.value) return
|
if (isLoadingPosts.value || allLoaded.value) return
|
||||||
try {
|
try {
|
||||||
isLoadingPosts.value = true
|
isLoadingPosts.value = true
|
||||||
const res = await fetch(buildReplyUrl())
|
const token = getToken()
|
||||||
|
const res = await fetch(buildReplyUrl(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: token ? `Bearer ${token}` : ''
|
||||||
|
}
|
||||||
|
})
|
||||||
isLoadingPosts.value = false
|
isLoadingPosts.value = false
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -366,11 +356,11 @@ export default {
|
|||||||
|
|
||||||
const fetchContent = async (reset = false) => {
|
const fetchContent = async (reset = false) => {
|
||||||
if (selectedTopic.value === '排行榜') {
|
if (selectedTopic.value === '排行榜') {
|
||||||
fetchRanking(reset)
|
await fetchRanking(reset)
|
||||||
} else if (selectedTopic.value === '最新回复') {
|
} else if (selectedTopic.value === '最新回复') {
|
||||||
fetchLatestReply(reset)
|
await fetchLatestReply(reset)
|
||||||
} else {
|
} else {
|
||||||
fetchPosts(reset)
|
await fetchPosts(reset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +376,8 @@ export default {
|
|||||||
|
|
||||||
const sanitizeDescription = text => stripMarkdown(text)
|
const sanitizeDescription = text => stripMarkdown(text)
|
||||||
|
|
||||||
|
await Promise.all([loadOptions(), fetchContent()])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
topics,
|
topics,
|
||||||
selectedTopic,
|
selectedTopic,
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ import Dropdown from '../../../components/Dropdown.vue'
|
|||||||
export default {
|
export default {
|
||||||
name: 'PostPageView',
|
name: 'PostPageView',
|
||||||
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ArticleCategory, ReactionsGroup, DropdownMenu, VueEasyLightbox, Dropdown },
|
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ArticleCategory, ReactionsGroup, DropdownMenu, VueEasyLightbox, Dropdown },
|
||||||
setup() {
|
async setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const postId = route.params.id
|
const postId = route.params.id
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -150,27 +150,35 @@ export default {
|
|||||||
const commentSort = ref('NEWEST')
|
const commentSort = ref('NEWEST')
|
||||||
const isFetchingComments = ref(false)
|
const isFetchingComments = ref(false)
|
||||||
|
|
||||||
// record default metadata from the main document
|
// record default metadata from the main document (client only)
|
||||||
const defaultTitle = document.title
|
const defaultTitle = process.client ? document.title : ''
|
||||||
const metaDescriptionEl = document.querySelector('meta[name="description"]')
|
const metaDescriptionEl = process.client
|
||||||
const defaultDescription = metaDescriptionEl ? metaDescriptionEl.getAttribute('content') : ''
|
? document.querySelector('meta[name="description"]')
|
||||||
const headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
: null
|
||||||
|
const defaultDescription = process.client && metaDescriptionEl
|
||||||
|
? metaDescriptionEl.getAttribute('content')
|
||||||
|
: ''
|
||||||
|
const headerHeight = process.client
|
||||||
|
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
||||||
|
: 0
|
||||||
|
|
||||||
watch(title, t => {
|
if (process.client) {
|
||||||
document.title = `OpenIsle - ${t}`
|
watch(title, t => {
|
||||||
})
|
document.title = `OpenIsle - ${t}`
|
||||||
|
})
|
||||||
|
|
||||||
watch(postContent, c => {
|
watch(postContent, c => {
|
||||||
if (metaDescriptionEl) {
|
if (metaDescriptionEl) {
|
||||||
metaDescriptionEl.setAttribute('content', stripMarkdownLength(c, 400))
|
metaDescriptionEl.setAttribute('content', stripMarkdownLength(c, 400))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.title = defaultTitle
|
document.title = defaultTitle
|
||||||
if (metaDescriptionEl) metaDescriptionEl.setAttribute('content', defaultDescription)
|
if (metaDescriptionEl) metaDescriptionEl.setAttribute('content', defaultDescription)
|
||||||
window.removeEventListener('scroll', updateCurrentIndex)
|
window.removeEventListener('scroll', updateCurrentIndex)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const lightboxVisible = ref(false)
|
const lightboxVisible = ref(false)
|
||||||
const lightboxIndex = ref(0)
|
const lightboxIndex = ref(0)
|
||||||
@@ -294,7 +302,7 @@ export default {
|
|||||||
})
|
})
|
||||||
isWaitingFetchingPost.value = false;
|
isWaitingFetchingPost.value = false;
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 404) {
|
if (res.status === 404 && process.client) {
|
||||||
router.replace('/404')
|
router.replace('/404')
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -581,10 +589,11 @@ export default {
|
|||||||
router.push(`/users/${author.value.id}`)
|
router.push(`/users/${author.value.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fetchPost()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
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
|
||||||
await fetchPost()
|
|
||||||
if (id) expandCommentPath(id)
|
if (id) expandCommentPath(id)
|
||||||
updateCurrentIndex()
|
updateCurrentIndex()
|
||||||
window.addEventListener('scroll', updateCurrentIndex)
|
window.addEventListener('scroll', updateCurrentIndex)
|
||||||
|
|||||||
@@ -86,9 +86,15 @@ export function handleMarkdownClick(e) {
|
|||||||
|
|
||||||
export function stripMarkdown(text) {
|
export function stripMarkdown(text) {
|
||||||
const html = md.render(text || '')
|
const html = md.render(text || '')
|
||||||
const el = document.createElement('div')
|
// SSR 环境下没有 document
|
||||||
el.innerHTML = html
|
if (typeof window === 'undefined') {
|
||||||
return el.textContent || el.innerText || ''
|
// 用正则去除 HTML 标签
|
||||||
|
return html.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim()
|
||||||
|
} else {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.innerHTML = html
|
||||||
|
return el.textContent || el.innerText || ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stripMarkdownLength(text, length) {
|
export function stripMarkdownLength(text, length) {
|
||||||
|
|||||||
@@ -3,6 +3,20 @@ import { ref, computed } from 'vue'
|
|||||||
const width = ref(0)
|
const width = ref(0)
|
||||||
const isClient = ref(false)
|
const isClient = ref(false)
|
||||||
|
|
||||||
|
// 检测移动设备的用户代理字符串
|
||||||
|
const isMobileUserAgent = () => {
|
||||||
|
if (typeof navigator === 'undefined') return false
|
||||||
|
|
||||||
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
|
const mobileKeywords = [
|
||||||
|
'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone',
|
||||||
|
'mobile', 'tablet', 'opera mini', 'iemobile'
|
||||||
|
]
|
||||||
|
|
||||||
|
return mobileKeywords.some(keyword => userAgent.includes(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端初始化
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
isClient.value = true
|
isClient.value = true
|
||||||
width.value = window.innerWidth
|
width.value = window.innerWidth
|
||||||
@@ -11,4 +25,13 @@ if (typeof window !== 'undefined') {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMobile = computed(() => isClient.value && width.value <= 768)
|
// 服务端和客户端的移动设备检测
|
||||||
|
export const isMobile = computed(() => {
|
||||||
|
if (isClient.value) {
|
||||||
|
// 客户端:优先使用窗口宽度,如果窗口宽度不可用则使用用户代理
|
||||||
|
return width.value > 0 ? width.value <= 768 : isMobileUserAgent()
|
||||||
|
} else {
|
||||||
|
// 服务端:使用用户代理字符串
|
||||||
|
return isMobileUserAgent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user