mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-07 15:41:02 +08:00
Compare commits
17 Commits
codex/supp
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d7ca3d29a | ||
|
|
dfef13e2be | ||
|
|
2f4d6e68da | ||
|
|
414872f61e | ||
|
|
82475f71db | ||
|
|
a6874e9be3 | ||
|
|
720031770d | ||
|
|
eb7a25434f | ||
|
|
bda4b24cf0 | ||
|
|
4dedb70d54 | ||
|
|
aea4f59af7 | ||
|
|
84ed778dc0 | ||
|
|
6ca1862034 | ||
|
|
b3ea41ad1e | ||
|
|
210d3dfa6f | ||
|
|
80ecb1620d | ||
|
|
b094f2f287 |
@@ -7,6 +7,7 @@ import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -68,7 +69,10 @@ public class User {
|
||||
@CollectionTable(name = "user_disabled_notification_types", joinColumns = @JoinColumn(name = "user_id"))
|
||||
@Column(name = "notification_type")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Set<NotificationType> disabledNotificationTypes = new HashSet<>();
|
||||
private Set<NotificationType> disabledNotificationTypes = EnumSet.of(
|
||||
NotificationType.POST_VIEWED,
|
||||
NotificationType.USER_ACTIVITY
|
||||
);
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false,
|
||||
|
||||
5
frontend_nuxt/.env
Normal file
5
frontend_nuxt/.env
Normal file
@@ -0,0 +1,5 @@
|
||||
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-xxx.apps.googleusercontent.com
|
||||
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
|
||||
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
||||
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="header-container">
|
||||
<HeaderComponent @toggle-menu="menuVisible = !menuVisible" :show-menu-btn="!hideMenu" />
|
||||
<HeaderComponent
|
||||
ref="header"
|
||||
@toggle-menu="menuVisible = !menuVisible"
|
||||
:show-menu-btn="!hideMenu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
@@ -42,17 +46,26 @@ export default {
|
||||
].includes(useRoute().path)
|
||||
})
|
||||
|
||||
const header = useTemplateRef('header')
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
menuVisible.value = window.innerWidth > 768
|
||||
}
|
||||
})
|
||||
|
||||
const handleMenuOutside = () => {
|
||||
if (isMobile.value) menuVisible.value = false
|
||||
const handleMenuOutside = (event) => {
|
||||
const btn = header.value.$refs.menuBtn
|
||||
if (btn && (btn === event.target || btn.contains(event.target))) {
|
||||
return // 如果是菜单按钮的点击,不处理关闭
|
||||
}
|
||||
|
||||
if (isMobile.value) {
|
||||
menuVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { menuVisible, hideMenu, handleMenuOutside }
|
||||
return { menuVisible, hideMenu, handleMenuOutside, header }
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken } from '../utils/auth'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { getToken } from '~/utils/auth'
|
||||
|
||||
const props = defineProps({
|
||||
medals: {
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed, watch, onUnmounted, useId } from 'vue'
|
||||
import { themeState } from '../utils/theme'
|
||||
import { computed, onMounted, onUnmounted, ref, useId, watch } from 'vue'
|
||||
import { clearVditorStorage } from '~/utils/clearVditorStorage'
|
||||
import { themeState } from '~/utils/theme'
|
||||
import {
|
||||
createVditor,
|
||||
getEditorTheme as getEditorThemeUtil,
|
||||
getPreviewTheme as getPreviewThemeUtil,
|
||||
} from '../utils/vditor'
|
||||
import LoginOverlay from './LoginOverlay.vue'
|
||||
import { clearVditorStorage } from '../utils/clearVditorStorage'
|
||||
} from '~/utils/vditor'
|
||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
||||
|
||||
export default {
|
||||
name: 'CommentEditor',
|
||||
|
||||
@@ -89,19 +89,19 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watch, computed, nextTick } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import VueEasyLightbox from 'vue-easy-lightbox'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CommentEditor from './CommentEditor.vue'
|
||||
import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
|
||||
import { getMedalTitle } from '../utils/medal'
|
||||
import TimeManager from '../utils/time'
|
||||
import BaseTimeline from './BaseTimeline.vue'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, authState } from '../utils/auth'
|
||||
import ReactionsGroup from './ReactionsGroup.vue'
|
||||
import DropdownMenu from './DropdownMenu.vue'
|
||||
import LoginOverlay from './LoginOverlay.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { authState, getToken } from '~/utils/auth'
|
||||
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
|
||||
import { getMedalTitle } from '~/utils/medal'
|
||||
import TimeManager from '~/utils/time'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import CommentEditor from '~/components/CommentEditor.vue'
|
||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||
|
||||
const CommentItem = {
|
||||
name: 'CommentItem',
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
text="建站送奶茶活动火热进行中,快来参与吧!"
|
||||
@close="closeMilkTeaPopup"
|
||||
/>
|
||||
<NotificationSettingPopup :visible="showNotificationPopup" @close="closeNotificationPopup" />
|
||||
<MedalPopup :visible="showMedalPopup" :medals="newMedals" @close="closeMedalPopup" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,25 +14,30 @@
|
||||
<script>
|
||||
import ActivityPopup from '~/components/ActivityPopup.vue'
|
||||
import MedalPopup from '~/components/MedalPopup.vue'
|
||||
import NotificationSettingPopup from '~/components/NotificationSettingPopup.vue'
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import { authState } from '~/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'GlobalPopups',
|
||||
components: { ActivityPopup, MedalPopup },
|
||||
components: { ActivityPopup, MedalPopup, NotificationSettingPopup },
|
||||
data() {
|
||||
return {
|
||||
showMilkTeaPopup: false,
|
||||
milkTeaIcon: '',
|
||||
showNotificationPopup: false,
|
||||
showMedalPopup: false,
|
||||
newMedals: [],
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.checkMilkTeaActivity()
|
||||
if (!this.showMilkTeaPopup) {
|
||||
await this.checkNewMedals()
|
||||
}
|
||||
if (this.showMilkTeaPopup) return
|
||||
|
||||
await this.checkNotificationSetting()
|
||||
if (this.showNotificationPopup) return
|
||||
|
||||
await this.checkNewMedals()
|
||||
},
|
||||
methods: {
|
||||
async checkMilkTeaActivity() {
|
||||
@@ -55,6 +61,18 @@ export default {
|
||||
if (!process.client) return
|
||||
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
||||
this.showMilkTeaPopup = false
|
||||
this.checkNotificationSetting()
|
||||
},
|
||||
async checkNotificationSetting() {
|
||||
if (!process.client) return
|
||||
if (!authState.loggedIn) return
|
||||
if (localStorage.getItem('notificationSettingPopupShown')) return
|
||||
this.showNotificationPopup = true
|
||||
},
|
||||
closeNotificationPopup() {
|
||||
if (!process.client) return
|
||||
localStorage.setItem('notificationSettingPopupShown', 'true')
|
||||
this.showNotificationPopup = false
|
||||
this.checkNewMedals()
|
||||
},
|
||||
async checkNewMedals() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="header-content">
|
||||
<div class="header-content-left">
|
||||
<div v-if="showMenuBtn" class="menu-btn-wrapper">
|
||||
<button class="menu-btn" @click="$emit('toggle-menu')">
|
||||
<button class="menu-btn" ref="menuBtn" @click="$emit('toggle-menu')">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<span v-if="isMobile && unreadCount > 0" class="menu-unread-dot"></span>
|
||||
@@ -49,14 +49,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
||||
import { watch, nextTick, ref, computed } from 'vue'
|
||||
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||
import { ClientOnly } from '#components'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||
import SearchDropdown from '~/components/SearchDropdown.vue'
|
||||
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
||||
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||
import { useIsMobile } from '~/utils/screen'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ClientOnly } from '#components'
|
||||
|
||||
export default {
|
||||
name: 'HeaderComponent',
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props, { expose }) {
|
||||
const isLogin = computed(() => authState.loggedIn)
|
||||
const isMobile = useIsMobile()
|
||||
const unreadCount = computed(() => notificationState.unreadCount)
|
||||
@@ -76,6 +76,11 @@ export default {
|
||||
const showSearch = ref(false)
|
||||
const searchDropdown = ref(null)
|
||||
const userMenu = ref(null)
|
||||
const menuBtn = ref(null)
|
||||
|
||||
expose({
|
||||
menuBtn,
|
||||
})
|
||||
|
||||
const goToHome = () => {
|
||||
router.push('/').then(() => {
|
||||
@@ -183,6 +188,7 @@ export default {
|
||||
searchDropdown,
|
||||
userMenu,
|
||||
avatar,
|
||||
menuBtn,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProgressBar from './ProgressBar.vue'
|
||||
import { prevLevelExp } from '../utils/level'
|
||||
import { prevLevelExp } from '~/utils/level'
|
||||
import ProgressBar from '~/components/ProgressBar.vue'
|
||||
export default {
|
||||
name: 'LevelProgress',
|
||||
components: { ProgressBar },
|
||||
|
||||
@@ -210,17 +210,13 @@ export default {
|
||||
|
||||
const gotoCategory = (c) => {
|
||||
const value = encodeURIComponent(c.id ?? c.name)
|
||||
router.push({ path: '/', query: { category: value } }).then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
router.push({ path: '/', query: { category: value } })
|
||||
handleItemClick()
|
||||
}
|
||||
|
||||
const gotoTag = (t) => {
|
||||
const value = encodeURIComponent(t.id ?? t.name)
|
||||
router.push({ path: '/', query: { tags: value } }).then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
router.push({ path: '/', query: { tags: value } })
|
||||
handleItemClick()
|
||||
}
|
||||
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProgressBar from './ProgressBar.vue'
|
||||
import LevelProgress from './LevelProgress.vue'
|
||||
import BaseInput from './BaseInput.vue'
|
||||
import BasePopup from './BasePopup.vue'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, fetchCurrentUser } from '../utils/auth'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { fetchCurrentUser, getToken } from '~/utils/auth'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import BasePopup from '~/components/BasePopup.vue'
|
||||
import LevelProgress from '~/components/LevelProgress.vue'
|
||||
import ProgressBar from '~/components/ProgressBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'MilkTeaActivityComponent',
|
||||
@@ -218,24 +218,30 @@ export default {
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.redeem-submit-button:disabled {
|
||||
background-color: var(--primary-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.redeem-submit-button:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.redeem-submit-button:disabled:hover {
|
||||
background-color: var(--primary-color-disabled);
|
||||
}
|
||||
|
||||
.redeem-cancel-button {
|
||||
color: var(--primary-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.redeem-cancel-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.user-level-text {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useIsMobile } from '../utils/screen'
|
||||
import { useIsMobile } from '~/utils/screen'
|
||||
export default {
|
||||
name: 'NotificationContainer',
|
||||
props: {
|
||||
|
||||
82
frontend_nuxt/components/NotificationSettingPopup.vue
Normal file
82
frontend_nuxt/components/NotificationSettingPopup.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<BasePopup :visible="visible" @close="close">
|
||||
<div class="notification-popup">
|
||||
<div class="notification-popup-title">🎉 通知设置上线啦</div>
|
||||
<div class="notification-popup-text">现在可以在消息 -> 消息设置中调整通知类型</div>
|
||||
<div class="notification-popup-actions">
|
||||
<div class="notification-popup-close" @click="close">知道了</div>
|
||||
<div class="notification-popup-button" @click="gotoSetting">去看看</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePopup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BasePopup from '~/components/BasePopup.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export default {
|
||||
name: 'NotificationSettingPopup',
|
||||
components: { BasePopup },
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter()
|
||||
const gotoSetting = () => {
|
||||
emit('close')
|
||||
router.push('/message?tab=control')
|
||||
}
|
||||
const close = () => emit('close')
|
||||
return { gotoSetting, close }
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.notification-popup-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.notification-popup-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-popup-button {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-popup-button:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.notification-popup-close {
|
||||
cursor: pointer;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-popup-close:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -8,14 +8,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, watch, onUnmounted, useId } from 'vue'
|
||||
import { themeState } from '../utils/theme'
|
||||
import { onMounted, onUnmounted, ref, useId, watch } from 'vue'
|
||||
import { clearVditorStorage } from '~/utils/clearVditorStorage'
|
||||
import { themeState } from '~/utils/theme'
|
||||
import {
|
||||
createVditor,
|
||||
getEditorTheme as getEditorThemeUtil,
|
||||
getPreviewTheme as getPreviewThemeUtil,
|
||||
} from '../utils/vditor'
|
||||
import { clearVditorStorage } from '../utils/clearVditorStorage'
|
||||
} from '~/utils/vditor'
|
||||
|
||||
export default {
|
||||
name: 'PostEditor',
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, authState } from '../utils/auth'
|
||||
import { reactionEmojiMap } from '../utils/reactions'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { authState, getToken } from '~/utils/auth'
|
||||
import { reactionEmojiMap } from '~/utils/reactions'
|
||||
|
||||
let cachedTypes = null
|
||||
const fetchTypes = async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BasePlaceholder from './BasePlaceholder.vue'
|
||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||
|
||||
export default {
|
||||
name: 'UserList',
|
||||
|
||||
13
frontend_nuxt/jsconfig.json
Normal file
13
frontend_nuxt/jsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*.js", "./**/*.vue"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
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://30.211.97.238:8081'
|
||||
export const GOOGLE_CLIENT_ID =
|
||||
'777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com'
|
||||
export const GITHUB_CLIENT_ID = 'Ov23liVkO1NPAX5JyWxJ'
|
||||
export const DISCORD_CLIENT_ID = '1394985417044000779'
|
||||
export const TWITTER_CLIENT_ID = 'ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ'
|
||||
import { useRuntimeConfig } from '#app'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
export const API_BASE_URL = config.public.apiBaseUrl
|
||||
export const GOOGLE_CLIENT_ID = config.public.googleClientId
|
||||
export const GITHUB_CLIENT_ID = config.public.githubClientId
|
||||
export const DISCORD_CLIENT_ID = config.public.discordClientId
|
||||
export const TWITTER_CLIENT_ID = config.public.twitterClientId
|
||||
|
||||
// 重新导出 toast 功能,使用 composable 方式
|
||||
export { toast } from './composables/useToast'
|
||||
|
||||
@@ -2,6 +2,15 @@ import { defineNuxtConfig } from 'nuxt/config'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
ssr: true,
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || '',
|
||||
googleClientId: process.env.NUXT_PUBLIC_GOOGLE_CLIENT_ID || '',
|
||||
githubClientId: process.env.NUXT_PUBLIC_GITHUB_CLIENT_ID || '',
|
||||
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
|
||||
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
||||
},
|
||||
},
|
||||
// Ensure Vditor styles load before our overrides in global.css
|
||||
css: ['vditor/dist/index.css', '~/assets/global.css'],
|
||||
app: {
|
||||
@@ -43,4 +52,9 @@ export default defineNuxtConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => ['l-hatch', 'l-hatch-spinner'].includes(tag),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
|
||||
|
||||
export default {
|
||||
name: 'AboutPageView',
|
||||
|
||||
@@ -30,19 +30,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import {
|
||||
DataZoomComponent,
|
||||
GridComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { API_BASE_URL } from '../main'
|
||||
import { getToken } from '../utils/auth'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import { getToken } from '~/utils/auth'
|
||||
|
||||
use([LineChart, TitleComponent, TooltipComponent, GridComponent, DataZoomComponent, CanvasRenderer])
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL } from '../main'
|
||||
import TimeManager from '../utils/time'
|
||||
import MilkTeaActivityComponent from '../components/MilkTeaActivityComponent.vue'
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import TimeManager from '~/utils/time'
|
||||
import MilkTeaActivityComponent from '~/components/MilkTeaActivityComponent.vue'
|
||||
|
||||
export default {
|
||||
name: 'ActivityListPageView',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CallbackPage from '../components/CallbackPage.vue'
|
||||
import { discordExchange } from '../utils/discord'
|
||||
import CallbackPage from '~/components/CallbackPage.vue'
|
||||
import { discordExchange } from '~/utils/discord'
|
||||
|
||||
export default {
|
||||
name: 'DiscordCallbackPageView',
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
export default {
|
||||
name: 'ForgotPasswordPageView',
|
||||
components: { BaseInput },
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CallbackPage from '../components/CallbackPage.vue'
|
||||
import { githubExchange } from '../utils/github'
|
||||
import CallbackPage from '~/components/CallbackPage.vue'
|
||||
import { githubExchange } from '~/utils/github'
|
||||
|
||||
export default {
|
||||
name: 'GithubCallbackPageView',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CallbackPage from '../components/CallbackPage.vue'
|
||||
import { googleAuthWithToken } from '../utils/google'
|
||||
import CallbackPage from '~/components/CallbackPage.vue'
|
||||
import { googleAuthWithToken } from '~/utils/google'
|
||||
|
||||
export default {
|
||||
name: 'GoogleCallbackPageView',
|
||||
|
||||
@@ -113,18 +113,17 @@
|
||||
|
||||
<script>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useScrollLoadMore } from '~/utils/loadMore'
|
||||
import { stripMarkdown } from '~/utils/markdown'
|
||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||
import ArticleTags from '~/components/ArticleTags.vue'
|
||||
import CategorySelect from '~/components/CategorySelect.vue'
|
||||
import SearchDropdown from '~/components/SearchDropdown.vue'
|
||||
import TagSelect from '~/components/TagSelect.vue'
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import { getToken } from '~/utils/auth'
|
||||
import TimeManager from '~/utils/time'
|
||||
import CategorySelect from '~/components/CategorySelect.vue'
|
||||
import TagSelect from '~/components/TagSelect.vue'
|
||||
import ArticleTags from '~/components/ArticleTags.vue'
|
||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||
import SearchDropdown from '~/components/SearchDropdown.vue'
|
||||
import { useScrollLoadMore } from '~/utils/loadMore'
|
||||
import { stripMarkdown } from '~/utils/markdown'
|
||||
import { useIsMobile } from '~/utils/screen'
|
||||
import TimeManager from '~/utils/time'
|
||||
|
||||
export default {
|
||||
name: 'HomePageView',
|
||||
@@ -150,22 +149,9 @@ export default {
|
||||
},
|
||||
],
|
||||
})
|
||||
const route = useRoute()
|
||||
const selectedCategory = ref('')
|
||||
if (route.query.category) {
|
||||
const c = decodeURIComponent(route.query.category)
|
||||
selectedCategory.value = isNaN(c) ? c : Number(c)
|
||||
}
|
||||
const selectedTags = ref([])
|
||||
if (route.query.tags) {
|
||||
const t = Array.isArray(route.query.tags) ? route.query.tags.join(',') : route.query.tags
|
||||
selectedTags.value = t
|
||||
.split(',')
|
||||
.filter((v) => v)
|
||||
.map((v) => decodeURIComponent(v))
|
||||
.map((v) => (isNaN(v) ? v : Number(v)))
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const tagOptions = ref([])
|
||||
const categoryOptions = ref([])
|
||||
const isLoadingPosts = ref(false)
|
||||
@@ -177,13 +163,50 @@ export default {
|
||||
? '最新'
|
||||
: '最新回复',
|
||||
)
|
||||
|
||||
const articles = ref([])
|
||||
const page = ref(0)
|
||||
const pageSize = 10
|
||||
const isMobile = useIsMobile()
|
||||
const allLoaded = ref(false)
|
||||
|
||||
const selectedCategorySet = (category) => {
|
||||
const c = decodeURIComponent(category)
|
||||
selectedCategory.value = isNaN(c) ? c : Number(c)
|
||||
}
|
||||
|
||||
const selectedTagsSet = (tags) => {
|
||||
const t = Array.isArray(tags) ? tags.join(',') : tags
|
||||
selectedTags.value = t
|
||||
.split(',')
|
||||
.filter((v) => v)
|
||||
.map((v) => decodeURIComponent(v))
|
||||
.map((v) => (isNaN(v) ? v : Number(v)))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const query = route.query
|
||||
const category = query.category
|
||||
const tags = query.tags
|
||||
|
||||
if (category) {
|
||||
selectedCategorySet(category)
|
||||
}
|
||||
if (tags) {
|
||||
selectedTagsSet(tags)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
const query = route.query
|
||||
const category = query.category
|
||||
const tags = query.tags
|
||||
category && selectedCategorySet(category)
|
||||
tags && selectedTagsSet(tags)
|
||||
},
|
||||
)
|
||||
|
||||
const loadOptions = async () => {
|
||||
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
||||
try {
|
||||
@@ -696,6 +719,7 @@ export default {
|
||||
.header-item.activity {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.article-member-avatar-item:nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { setToken, loadCurrentUser } from '../utils/auth'
|
||||
import { googleAuthorize } from '../utils/google'
|
||||
import { githubAuthorize } from '../utils/github'
|
||||
import { discordAuthorize } from '../utils/discord'
|
||||
import { twitterAuthorize } from '../utils/twitter'
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import { registerPush } from '../utils/push'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { setToken, loadCurrentUser } from '~/utils/auth'
|
||||
import { googleAuthorize } from '~/utils/google'
|
||||
import { githubAuthorize } from '~/utils/github'
|
||||
import { discordAuthorize } from '~/utils/discord'
|
||||
import { twitterAuthorize } from '~/utils/twitter'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import { registerPush } from '~/utils/push'
|
||||
export default {
|
||||
name: 'LoginPageView',
|
||||
components: { BaseInput },
|
||||
|
||||
@@ -481,31 +481,28 @@
|
||||
<script>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { API_BASE_URL } from '../main'
|
||||
import BaseTimeline from '../components/BaseTimeline.vue'
|
||||
import BasePlaceholder from '../components/BasePlaceholder.vue'
|
||||
import NotificationContainer from '../components/NotificationContainer.vue'
|
||||
import { getToken, authState } from '../utils/auth'
|
||||
import {
|
||||
markNotificationsRead,
|
||||
fetchUnreadCount,
|
||||
notificationState,
|
||||
fetchNotificationPreferences,
|
||||
updateNotificationPreference,
|
||||
} from '../utils/notification'
|
||||
import { toast } from '../main'
|
||||
import { stripMarkdownLength } from '../utils/markdown'
|
||||
import TimeManager from '../utils/time'
|
||||
import { reactionEmojiMap } from '../utils/reactions'
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||
import NotificationContainer from '~/components/NotificationContainer.vue'
|
||||
import { getToken, authState } from '~/utils/auth'
|
||||
import { markNotificationsRead, fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||
import { toast } from '~/main'
|
||||
import { stripMarkdownLength } from '~/utils/markdown'
|
||||
import TimeManager from '~/utils/time'
|
||||
import { reactionEmojiMap } from '~/utils/reactions'
|
||||
|
||||
export default {
|
||||
name: 'MessagePageView',
|
||||
components: { BaseTimeline, BasePlaceholder, NotificationContainer },
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const notifications = ref([])
|
||||
const isLoadingMessage = ref(false)
|
||||
const selectedTab = ref('unread')
|
||||
const selectedTab = ref(
|
||||
['all', 'unread', 'control'].includes(route.query.tab) ? route.query.tab : 'unread',
|
||||
)
|
||||
const notificationPrefs = ref([])
|
||||
const filteredNotifications = computed(() =>
|
||||
selectedTab.value === 'all'
|
||||
@@ -790,6 +787,10 @@ export default {
|
||||
return '关注的用户有新动态'
|
||||
case 'MENTION':
|
||||
return '有人提到了你'
|
||||
case 'REGISTER_REQUEST':
|
||||
return '有人申请注册'
|
||||
case 'ACTIVITY_REDEEM':
|
||||
return '有人申请兑换奶茶'
|
||||
default:
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -78,18 +78,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import PostEditor from '../components/PostEditor.vue'
|
||||
import CategorySelect from '../components/CategorySelect.vue'
|
||||
import TagSelect from '../components/TagSelect.vue'
|
||||
import PostTypeSelect from '../components/PostTypeSelect.vue'
|
||||
import AvatarCropper from '../components/AvatarCropper.vue'
|
||||
import FlatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, authState } from '../utils/auth'
|
||||
import LoginOverlay from '../components/LoginOverlay.vue'
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import FlatPickr from 'vue-flatpickr-component'
|
||||
import AvatarCropper from '~/components/AvatarCropper.vue'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import CategorySelect from '~/components/CategorySelect.vue'
|
||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
||||
import PostEditor from '~/components/PostEditor.vue'
|
||||
import PostTypeSelect from '~/components/PostTypeSelect.vue'
|
||||
import TagSelect from '~/components/TagSelect.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { authState, getToken } from '~/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'NewPostPageView',
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
<script>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import PostEditor from '../../../components/PostEditor.vue'
|
||||
import CategorySelect from '../../../components/CategorySelect.vue'
|
||||
import TagSelect from '../../../components/TagSelect.vue'
|
||||
import { API_BASE_URL, toast } from '../../../main'
|
||||
import { getToken, authState } from '../../../utils/auth'
|
||||
import LoginOverlay from '../../../components/LoginOverlay.vue'
|
||||
import PostEditor from '~/components/PostEditor.vue'
|
||||
import CategorySelect from '~/components/CategorySelect.vue'
|
||||
import TagSelect from '~/components/TagSelect.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { getToken, authState } from '~/utils/auth'
|
||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
||||
|
||||
export default {
|
||||
name: 'EditPostPageView',
|
||||
|
||||
@@ -234,21 +234,22 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||
import VueEasyLightbox from 'vue-easy-lightbox'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CommentItem from '../../../components/CommentItem.vue'
|
||||
import CommentEditor from '../../../components/CommentEditor.vue'
|
||||
import BaseTimeline from '../../../components/BaseTimeline.vue'
|
||||
import ArticleTags from '../../../components/ArticleTags.vue'
|
||||
import ArticleCategory from '../../../components/ArticleCategory.vue'
|
||||
import ReactionsGroup from '../../../components/ReactionsGroup.vue'
|
||||
import DropdownMenu from '../../../components/DropdownMenu.vue'
|
||||
import { renderMarkdown, handleMarkdownClick, stripMarkdownLength } from '../../../utils/markdown'
|
||||
import { getMedalTitle } from '../../../utils/medal'
|
||||
import { API_BASE_URL, toast } from '../../../main'
|
||||
import { getToken, authState } from '../../../utils/auth'
|
||||
import TimeManager from '../../../utils/time'
|
||||
import CommentItem from '~/components/CommentItem.vue'
|
||||
import CommentEditor from '~/components/CommentEditor.vue'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import ArticleTags from '~/components/ArticleTags.vue'
|
||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||
import { renderMarkdown, handleMarkdownClick, stripMarkdownLength } from '~/utils/markdown'
|
||||
import { getMedalTitle } from '~/utils/medal'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { getToken, authState } from '~/utils/auth'
|
||||
import TimeManager from '~/utils/time'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useIsMobile } from '../../../utils/screen'
|
||||
import Dropdown from '../../../components/Dropdown.vue'
|
||||
import { useIsMobile } from '~/utils/screen'
|
||||
import Dropdown from '~/components/Dropdown.vue'
|
||||
import { ClientOnly } from '#components'
|
||||
|
||||
export default {
|
||||
name: 'PostPageView',
|
||||
@@ -262,6 +263,7 @@ export default {
|
||||
DropdownMenu,
|
||||
VueEasyLightbox,
|
||||
Dropdown,
|
||||
ClientOnly,
|
||||
},
|
||||
async setup() {
|
||||
const route = useRoute()
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, fetchCurrentUser, setToken } from '../utils/auth'
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import Dropdown from '../components/Dropdown.vue'
|
||||
import AvatarCropper from '../components/AvatarCropper.vue'
|
||||
import AvatarCropper from '~/components/AvatarCropper.vue'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import Dropdown from '~/components/Dropdown.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { fetchCurrentUser, getToken, setToken } from '~/utils/auth'
|
||||
export default {
|
||||
name: 'SettingsPageView',
|
||||
components: { BaseInput, Dropdown, AvatarCropper },
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
|
||||
export default {
|
||||
name: 'SignupReasonPageView',
|
||||
|
||||
@@ -70,19 +70,19 @@
|
||||
|
||||
<div class="other-signup-page-content">
|
||||
<div class="signup-page-button" @click="googleAuthorize">
|
||||
<img class="signup-page-button-icon" src="../assets/icons/google.svg" alt="Google Logo" />
|
||||
<img class="signup-page-button-icon" src="~/assets/icons/google.svg" alt="Google Logo" />
|
||||
<div class="signup-page-button-text">Google 注册</div>
|
||||
</div>
|
||||
<div class="signup-page-button" @click="signupWithGithub">
|
||||
<img class="signup-page-button-icon" src="../assets/icons/github.svg" alt="GitHub Logo" />
|
||||
<img class="signup-page-button-icon" src="~/assets/icons/github.svg" alt="GitHub Logo" />
|
||||
<div class="signup-page-button-text">GitHub 注册</div>
|
||||
</div>
|
||||
<div class="signup-page-button" @click="signupWithDiscord">
|
||||
<img class="signup-page-button-icon" src="../assets/icons/discord.svg" alt="Discord Logo" />
|
||||
<img class="signup-page-button-icon" src="~/assets/icons/discord.svg" alt="Discord Logo" />
|
||||
<div class="signup-page-button-text">Discord 注册</div>
|
||||
</div>
|
||||
<div class="signup-page-button" @click="signupWithTwitter">
|
||||
<img class="signup-page-button-icon" src="../assets/icons/twitter.svg" alt="Twitter Logo" />
|
||||
<img class="signup-page-button-icon" src="~/assets/icons/twitter.svg" alt="Twitter Logo" />
|
||||
<div class="signup-page-button-text">Twitter 注册</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,12 +90,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { googleAuthorize } from '../utils/google'
|
||||
import { githubAuthorize } from '../utils/github'
|
||||
import { discordAuthorize } from '../utils/discord'
|
||||
import { twitterAuthorize } from '../utils/twitter'
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { discordAuthorize } from '~/utils/discord'
|
||||
import { githubAuthorize } from '~/utils/github'
|
||||
import { googleAuthorize } from '~/utils/google'
|
||||
import { twitterAuthorize } from '~/utils/twitter'
|
||||
export default {
|
||||
name: 'SignupPageView',
|
||||
components: { BaseInput },
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CallbackPage from '../components/CallbackPage.vue'
|
||||
import { twitterExchange } from '../utils/twitter'
|
||||
import CallbackPage from '~/components/CallbackPage.vue'
|
||||
import { twitterExchange } from '~/utils/twitter'
|
||||
|
||||
export default {
|
||||
name: 'TwitterCallbackPageView',
|
||||
|
||||
@@ -297,27 +297,26 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, authState } from '../../utils/auth'
|
||||
import BaseTimeline from '../components/BaseTimeline.vue'
|
||||
import UserList from '../components/UserList.vue'
|
||||
import BasePlaceholder from '../components/BasePlaceholder.vue'
|
||||
import LevelProgress from '../components/LevelProgress.vue'
|
||||
import { stripMarkdown, stripMarkdownLength } from '../utils/markdown'
|
||||
import TimeManager from '../utils/time'
|
||||
import { prevLevelExp } from '../utils/level'
|
||||
import AchievementList from '../components/AchievementList.vue'
|
||||
|
||||
definePageMeta({
|
||||
alias: ['/users/:id/'],
|
||||
})
|
||||
import AchievementList from '~/components/AchievementList.vue'
|
||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import LevelProgress from '~/components/LevelProgress.vue'
|
||||
import UserList from '~/components/UserList.vue'
|
||||
import { API_BASE_URL, toast } from '~/main'
|
||||
import { authState, getToken } from '~/utils/auth'
|
||||
import { prevLevelExp } from '~/utils/level'
|
||||
import { stripMarkdown, stripMarkdownLength } from '~/utils/markdown'
|
||||
import TimeManager from '~/utils/time'
|
||||
|
||||
export default {
|
||||
name: 'ProfileView',
|
||||
components: { BaseTimeline, UserList, BasePlaceholder, LevelProgress, AchievementList },
|
||||
setup() {
|
||||
definePageMeta({
|
||||
alias: ['/users/:id/'],
|
||||
})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const username = route.params.id
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// plugins/ldrs.client.ts
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
// 动态引入,防止打包时把 ldrs 拉进 SSR bundle
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
NProgress.configure({ showSpinner: false })
|
||||
@@ -12,7 +13,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
nuxtApp.hook('page:error', () => {
|
||||
nuxtApp.hook('app:error', () => {
|
||||
NProgress.done()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
import { initTheme } from '~/utils/theme'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
import '~/assets/toast.css'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { toast } from '../main'
|
||||
import { tiebaEmoji } from './tiebaEmoji'
|
||||
|
||||
@@ -86,18 +86,19 @@ export function handleMarkdownClick(e) {
|
||||
|
||||
export function stripMarkdown(text) {
|
||||
const html = md.render(text || '')
|
||||
// SSR 环境下没有 document
|
||||
if (typeof window === 'undefined') {
|
||||
// 用正则去除 HTML 标签
|
||||
return html
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
} else {
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
return el.textContent || el.innerText || ''
|
||||
}
|
||||
|
||||
// 统一使用正则表达式方法,确保服务端和客户端行为一致
|
||||
let plainText = html.replace(/<[^>]+>/g, '')
|
||||
|
||||
// 标准化空白字符处理
|
||||
plainText = plainText
|
||||
.replace(/\r\n/g, '\n') // Windows换行符转为Unix格式
|
||||
.replace(/\r/g, '\n') // 旧Mac换行符转为Unix格式
|
||||
.replace(/[ \t]+/g, ' ') // 合并空格和制表符为单个空格
|
||||
.replace(/\n{3,}/g, '\n\n') // 最多保留两个连续换行(一个空行)
|
||||
.trim()
|
||||
|
||||
return plainText
|
||||
}
|
||||
|
||||
export function stripMarkdownLength(text, length) {
|
||||
|
||||
Reference in New Issue
Block a user