mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-03 16:37:41 +08:00
feat: add initial Nuxt frontend with SSR
This commit is contained in:
105
frontend_nuxt/utils/auth.js
Normal file
105
frontend_nuxt/utils/auth.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const TOKEN_KEY = 'token'
|
||||
const USER_ID_KEY = 'userId'
|
||||
const USERNAME_KEY = 'username'
|
||||
const ROLE_KEY = 'role'
|
||||
|
||||
export const authState = reactive({
|
||||
loggedIn: false,
|
||||
userId: null,
|
||||
username: null,
|
||||
role: null
|
||||
})
|
||||
|
||||
if (process.client) {
|
||||
authState.loggedIn = localStorage.getItem(TOKEN_KEY) !== null && localStorage.getItem(TOKEN_KEY) !== ''
|
||||
authState.userId = localStorage.getItem(USER_ID_KEY)
|
||||
authState.username = localStorage.getItem(USERNAME_KEY)
|
||||
authState.role = localStorage.getItem(ROLE_KEY)
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return process.client ? localStorage.getItem(TOKEN_KEY) : null
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
if (process.client) {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
authState.loggedIn = true
|
||||
}
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
if (process.client) {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
clearUserInfo()
|
||||
authState.loggedIn = false
|
||||
}
|
||||
}
|
||||
|
||||
export function setUserInfo({ id, username }) {
|
||||
if (process.client) {
|
||||
authState.userId = id
|
||||
authState.username = username
|
||||
if (arguments[0] && arguments[0].role) {
|
||||
authState.role = arguments[0].role
|
||||
localStorage.setItem(ROLE_KEY, arguments[0].role)
|
||||
}
|
||||
if (id !== undefined && id !== null) localStorage.setItem(USER_ID_KEY, id)
|
||||
if (username) localStorage.setItem(USERNAME_KEY, username)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearUserInfo() {
|
||||
if (process.client) {
|
||||
localStorage.removeItem(USER_ID_KEY)
|
||||
localStorage.removeItem(USERNAME_KEY)
|
||||
localStorage.removeItem(ROLE_KEY)
|
||||
authState.userId = null
|
||||
authState.username = null
|
||||
authState.role = null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCurrentUser() {
|
||||
const token = getToken()
|
||||
if (!token) return null
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (!res.ok) return null
|
||||
return await res.json()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadCurrentUser() {
|
||||
const user = await fetchCurrentUser()
|
||||
if (user) {
|
||||
setUserInfo({ id: user.id, username: user.username, role: user.role })
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
export function isLogin() {
|
||||
return authState.loggedIn
|
||||
}
|
||||
|
||||
export async function checkToken() {
|
||||
const token = getToken()
|
||||
if (!token) return false
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/check`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
authState.loggedIn = res.ok
|
||||
return res.ok
|
||||
} catch (e) {
|
||||
authState.loggedIn = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
38
frontend_nuxt/utils/loadMore.js
Normal file
38
frontend_nuxt/utils/loadMore.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ref, onMounted, onUnmounted, onActivated, nextTick } from 'vue'
|
||||
|
||||
export function useScrollLoadMore(loadMore, offset = 50) {
|
||||
const savedScrollTop = ref(0)
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!process.client) return
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop
|
||||
const scrollHeight = document.documentElement.scrollHeight
|
||||
const windowHeight = window.innerHeight
|
||||
savedScrollTop.value = scrollTop
|
||||
if (scrollHeight - (scrollTop + windowHeight) <= offset) {
|
||||
loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (process.client) {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
if (process.client) {
|
||||
nextTick(() => {
|
||||
window.scrollTo({ top: savedScrollTop.value })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return { savedScrollTop }
|
||||
}
|
||||
11
frontend_nuxt/utils/markdown.js
Normal file
11
frontend_nuxt/utils/markdown.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function stripMarkdown(text) {
|
||||
return text ? text.replace(/[#_*`>\-\[\]\(\)!]/g, '') : ''
|
||||
}
|
||||
|
||||
export function stripMarkdownLength(text, length) {
|
||||
const plain = stripMarkdown(text)
|
||||
if (!length || plain.length <= length) {
|
||||
return plain
|
||||
}
|
||||
return plain.slice(0, length) + '...'
|
||||
}
|
||||
48
frontend_nuxt/utils/notification.js
Normal file
48
frontend_nuxt/utils/notification.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { API_BASE_URL } from '~/main'
|
||||
import { getToken } from './auth'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const notificationState = reactive({
|
||||
unreadCount: 0
|
||||
})
|
||||
|
||||
export async function fetchUnreadCount() {
|
||||
try {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
notificationState.unreadCount = 0
|
||||
return 0
|
||||
}
|
||||
const res = await fetch(`${API_BASE_URL}/api/notifications/unread-count`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (!res.ok) {
|
||||
notificationState.unreadCount = 0
|
||||
return 0
|
||||
}
|
||||
const data = await res.json()
|
||||
notificationState.unreadCount = data.count
|
||||
return data.count
|
||||
} catch (e) {
|
||||
notificationState.unreadCount = 0
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export async function markNotificationsRead(ids) {
|
||||
try {
|
||||
const token = getToken()
|
||||
if (!token || !ids || ids.length === 0) return false
|
||||
const res = await fetch(`${API_BASE_URL}/api/notifications/read`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids })
|
||||
})
|
||||
return res.ok
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
12
frontend_nuxt/utils/screen.js
Normal file
12
frontend_nuxt/utils/screen.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const width = ref(0)
|
||||
|
||||
if (process.client) {
|
||||
width.value = window.innerWidth
|
||||
window.addEventListener('resize', () => {
|
||||
width.value = window.innerWidth
|
||||
})
|
||||
}
|
||||
|
||||
export const isMobile = computed(() => width.value <= 768)
|
||||
64
frontend_nuxt/utils/theme.js
Normal file
64
frontend_nuxt/utils/theme.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { reactive } from 'vue'
|
||||
import { toast } from '~/main'
|
||||
|
||||
export const ThemeMode = {
|
||||
SYSTEM: 'system',
|
||||
LIGHT: 'light',
|
||||
DARK: 'dark'
|
||||
}
|
||||
|
||||
const THEME_KEY = 'theme-mode'
|
||||
|
||||
export const themeState = reactive({
|
||||
mode: ThemeMode.SYSTEM
|
||||
})
|
||||
|
||||
function apply(mode) {
|
||||
if (!process.client) return
|
||||
const root = document.documentElement
|
||||
if (mode === ThemeMode.SYSTEM) {
|
||||
root.dataset.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
} else {
|
||||
root.dataset.theme = mode
|
||||
}
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
if (!process.client) return
|
||||
const saved = localStorage.getItem(THEME_KEY)
|
||||
if (saved && Object.values(ThemeMode).includes(saved)) {
|
||||
themeState.mode = saved
|
||||
}
|
||||
apply(themeState.mode)
|
||||
}
|
||||
|
||||
export function setTheme(mode) {
|
||||
if (!process.client) return
|
||||
if (!Object.values(ThemeMode).includes(mode)) return
|
||||
themeState.mode = mode
|
||||
localStorage.setItem(THEME_KEY, mode)
|
||||
apply(mode)
|
||||
}
|
||||
|
||||
export function cycleTheme() {
|
||||
if (!process.client) return
|
||||
const modes = [ThemeMode.SYSTEM, ThemeMode.LIGHT, ThemeMode.DARK]
|
||||
const index = modes.indexOf(themeState.mode)
|
||||
const next = modes[(index + 1) % modes.length]
|
||||
if (next === ThemeMode.SYSTEM) {
|
||||
toast.success('💻 已经切换到系统主题')
|
||||
} else if (next === ThemeMode.LIGHT) {
|
||||
toast.success('🌞 已经切换到明亮主题')
|
||||
} else {
|
||||
toast.success('🌙 已经切换到暗色主题')
|
||||
}
|
||||
setTheme(next)
|
||||
}
|
||||
|
||||
if (process.client && window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (themeState.mode === ThemeMode.SYSTEM) {
|
||||
apply(ThemeMode.SYSTEM)
|
||||
}
|
||||
})
|
||||
}
|
||||
32
frontend_nuxt/utils/time.js
Normal file
32
frontend_nuxt/utils/time.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export default class TimeManager {
|
||||
static format(input) {
|
||||
const date = new Date(input)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
|
||||
const now = new Date()
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const diffDays = Math.floor((startOfToday - startOfDate) / 86400000)
|
||||
|
||||
const hh = date.getHours().toString().padStart(2, '0')
|
||||
const mm = date.getMinutes().toString().padStart(2, '0')
|
||||
const timePart = `${hh}:${mm}`
|
||||
|
||||
if (diffDays === 0) return `今天 ${timePart}`
|
||||
if (diffDays === 1) return `昨天 ${timePart}`
|
||||
if (diffDays === 2) return `前天 ${timePart}`
|
||||
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
|
||||
if (date.getFullYear() === now.getFullYear()) {
|
||||
return `${month}.${day} ${timePart}`
|
||||
}
|
||||
|
||||
if (date.getFullYear() === now.getFullYear() - 1) {
|
||||
return `去年 ${month}.${day} ${timePart}`
|
||||
}
|
||||
|
||||
return `${date.getFullYear()}.${month}.${day} ${timePart}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user