- CursorFollower: 空心环→随机实心几何形状(7选1)+随机颜色,每次刷新换 - 照片墙拖动照片(移动后)隐藏自定义鼠标,松手复现 - App: 加 resolving 闸,异步路由解析完成前不渲染目标页, 修复已登录刷新特殊文件夹时先闪 BlogPage 再跳回照片墙 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { CursorFollower } from './components/CursorFollower'
|
||
import { LoginPage } from './components/LoginPage'
|
||
import { BlogPage } from './components/BlogPage'
|
||
import { StartPage } from './components/StartPage'
|
||
import { PhotoWallPage } from './components/PhotoWallPage'
|
||
import { useAuth } from './hooks/useAuth'
|
||
import { checkGate } from './api/gate'
|
||
import { checkFolder } from './api/folders'
|
||
|
||
// 固定入口 hash:#photo 进个人照片墙(公开路由,不走后端 gate,登录成功即进)。
|
||
const PHOTO_HASH = 'photo'
|
||
|
||
function App() {
|
||
const auth = useAuth()
|
||
const [showLogin, setShowLogin] = useState(false)
|
||
// 当前是否走个人照片墙入口(地址栏 #photo)
|
||
const [wantsPhoto, setWantsPhoto] = useState(
|
||
() => window.location.hash.replace(/^#/, '') === PHOTO_HASH,
|
||
)
|
||
// 特殊文件夹入口:如果 hash 匹配一个注册文件夹,存文件夹名
|
||
const [wantsFolder, setWantsFolder] = useState<string | null>(null)
|
||
// 路由是否还在异步解析中。非 #photo 的 hash 要等 checkFolder/checkGate 返回才知道
|
||
// 去向;解析完成前先不渲染任何目标页,避免已登录时一闪 BlogPage 兜底再跳回。
|
||
const [resolving, setResolving] = useState(() => {
|
||
const h = window.location.hash.replace(/^#/, '')
|
||
return !!h && h !== PHOTO_HASH
|
||
})
|
||
const signOut = auth.signOut
|
||
|
||
// 监听地址栏 hash:
|
||
// - #photo → 个人照片墙(公开入口,直接放行到登录页)
|
||
// - 其余 hash → 先查是否是特殊文件夹 → 否则查 gate → 否则起始页
|
||
useEffect(() => {
|
||
let alive = true
|
||
const check = async () => {
|
||
const hash = window.location.hash.replace(/^#/, '')
|
||
const photo = hash === PHOTO_HASH
|
||
if (alive) {
|
||
setWantsPhoto(photo)
|
||
setWantsFolder(null)
|
||
setShowLogin(false)
|
||
// #photo 与无 hash 都能同步定向,不需要异步解析
|
||
setResolving(!!hash && !photo)
|
||
}
|
||
if (photo || !hash) return
|
||
|
||
// 先查是否是已注册的特殊文件夹
|
||
const isFolder = await checkFolder(hash)
|
||
if (!alive || hash !== window.location.hash.replace(/^#/, '')) return
|
||
if (isFolder) {
|
||
setWantsFolder(hash)
|
||
setResolving(false)
|
||
return
|
||
}
|
||
// 不是文件夹,查 gate
|
||
const ok = await checkGate(hash)
|
||
if (alive && hash === window.location.hash.replace(/^#/, '')) {
|
||
setShowLogin(ok)
|
||
setResolving(false)
|
||
}
|
||
}
|
||
check()
|
||
window.addEventListener('hashchange', check)
|
||
return () => {
|
||
alive = false
|
||
window.removeEventListener('hashchange', check)
|
||
}
|
||
}, [])
|
||
|
||
// 回到导航页:清掉登录态,并抹掉地址栏 hash
|
||
const goStart = () => {
|
||
signOut()
|
||
setShowLogin(false)
|
||
setWantsFolder(null)
|
||
if (window.location.hash) {
|
||
history.replaceState(null, '', window.location.pathname + window.location.search)
|
||
}
|
||
}
|
||
|
||
// 403 无权访问特殊文件夹时,也退回起始页
|
||
const onFolderForbidden = () => {
|
||
setWantsFolder(null)
|
||
if (window.location.hash) {
|
||
history.replaceState(null, '', window.location.pathname + window.location.search)
|
||
}
|
||
}
|
||
|
||
let view
|
||
if (resolving) {
|
||
// 路由解析中:先留空,等 checkFolder/checkGate 定向后再渲染目标页
|
||
view = null
|
||
} else if (auth.username) {
|
||
if (wantsPhoto) {
|
||
view = <PhotoWallPage onExit={goStart} token={auth.token ?? ''} />
|
||
} else if (wantsFolder) {
|
||
view = (
|
||
<PhotoWallPage
|
||
onExit={goStart}
|
||
token={auth.token ?? ''}
|
||
folderName={wantsFolder}
|
||
isAdmin={auth.username === 'cc'}
|
||
onForbidden={onFolderForbidden}
|
||
/>
|
||
)
|
||
} else {
|
||
view = <BlogPage username={auth.username} onSignOut={goStart} />
|
||
}
|
||
} else if (wantsPhoto || wantsFolder) {
|
||
// 照片墙 / 特殊文件夹未登录:复用登录页,去掉顶部服务器负载栏
|
||
view = <LoginPage onSignIn={auth.signIn} onBack={goStart} hideStats />
|
||
} else if (showLogin) {
|
||
view = <LoginPage onSignIn={auth.signIn} onBack={goStart} />
|
||
} else {
|
||
view = <StartPage />
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<CursorFollower />
|
||
{view}
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default App
|