mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 11:46:36 +08:00
feat: merge conflict resolution, site management, UI fixes
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { usePermissionStore } from '../stores/permissions'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
/**
|
||||
* usePermission hook - 检查用户是否拥有特定权限
|
||||
*
|
||||
* @param key - 权限键名
|
||||
* @param options - 配置选项
|
||||
* @param options.autoFetch - 是否在权限未加载时自动获取(默认 true)
|
||||
* @returns boolean - 用户是否拥有该权限
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const canEdit = usePermission('can_edit_media')
|
||||
*
|
||||
* if (canEdit) {
|
||||
* return <EditButton />
|
||||
* }
|
||||
* return null
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function usePermission(
|
||||
key: string,
|
||||
options: { autoFetch?: boolean } = {}
|
||||
): boolean {
|
||||
const { autoFetch = true } = options
|
||||
const { hasPermission, isSuper, permissions, isLoading, fetchPermissions } = usePermissionStore()
|
||||
const tier = useAuthStore((state) => state.tier)
|
||||
const role = useAuthStore((state) => state.user?.role)
|
||||
const isAuthenticated = useAuthStore((state) => state.token !== null)
|
||||
|
||||
// 超级用户有所有权限
|
||||
if (isSuper || tier === 'plus' || role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// 权限未加载且未认证时,返回 false
|
||||
if (!isAuthenticated) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 权限未加载时自动获取
|
||||
useEffect(() => {
|
||||
if (autoFetch && Object.keys(permissions).length === 0 && !isLoading) {
|
||||
fetchPermissions()
|
||||
}
|
||||
}, [autoFetch, permissions, isLoading, fetchPermissions])
|
||||
|
||||
return hasPermission(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* usePermissions hook - 获取所有权限
|
||||
*
|
||||
* @returns 权限状态和检查函数
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { permissions, isSuper, check } = usePermissions()
|
||||
*
|
||||
* if (isSuper) {
|
||||
* return <AdminPanel />
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {check('can_view_dashboard') && <Dashboard />}
|
||||
* {check('can_play_media') && <Player />}
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function usePermissions() {
|
||||
const { permissions, isSuper, isLoading, fetchPermissions } = usePermissionStore()
|
||||
const tier = useAuthStore((state) => state.tier)
|
||||
const role = useAuthStore((state) => state.user?.role)
|
||||
const isAuthenticated = useAuthStore((state) => state.token !== null)
|
||||
|
||||
const check = (key: string): boolean => {
|
||||
if (isSuper || tier === 'plus' || role === 'admin') {
|
||||
return true
|
||||
}
|
||||
return permissions[key] === true
|
||||
}
|
||||
|
||||
return {
|
||||
permissions,
|
||||
isSuper: isSuper || tier === 'plus' || role === 'admin',
|
||||
isLoading,
|
||||
check,
|
||||
refetch: fetchPermissions,
|
||||
isAuthenticated,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* usePermissionMany hook - 批量检查多个权限
|
||||
*
|
||||
* @param keys - 权限键数组
|
||||
* @returns 每个权限的布尔值映射
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const perms = usePermissionMany([
|
||||
* 'can_edit_media',
|
||||
* 'can_manage_users',
|
||||
* 'can_access_settings',
|
||||
* ])
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {perms['can_edit_media'] && <EditButton />}
|
||||
* {perms['can_manage_users'] && <UserManagement />}
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function usePermissionMany(keys: string[]): Record<string, boolean> {
|
||||
const { check } = usePermissions()
|
||||
|
||||
return keys.reduce((acc, key) => {
|
||||
acc[key] = check(key)
|
||||
return acc
|
||||
}, {} as Record<string, boolean>)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import type { SSEEvent } from '../types'
|
||||
|
||||
type SSEEventHandler = (event: SSEEvent) => void
|
||||
|
||||
/**
|
||||
* useSSE hook - 管理 Server-Sent Events 连接
|
||||
*
|
||||
* @param onEvent - 事件处理函数
|
||||
* @param options - 配置选项
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { connect, disconnect } = useSSE((event) => {
|
||||
* if (event.type === 'scan') {
|
||||
* console.log('Scan progress:', event.payload)
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* useEffect(() => {
|
||||
* connect()
|
||||
* return () => disconnect()
|
||||
* }, [])
|
||||
*
|
||||
* return <div>SSE Demo</div>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useSSE(
|
||||
onEvent: SSEEventHandler,
|
||||
options: { autoConnect?: boolean } = {}
|
||||
) {
|
||||
const { autoConnect = true } = options
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const isConnectedRef = useRef(false)
|
||||
const reconnectAttemptsRef = useRef(0)
|
||||
const maxReconnectAttempts = 5
|
||||
|
||||
const connect = useCallback(() => {
|
||||
// 如果已有连接,先断开
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
const token = useAuthStore.getState().token
|
||||
if (!token) {
|
||||
console.warn('Cannot connect to SSE: No auth token')
|
||||
return
|
||||
}
|
||||
|
||||
const url = `/api/events?token=${encodeURIComponent(token)}`
|
||||
const eventSource = new EventSource(url)
|
||||
eventSourceRef.current = eventSource
|
||||
|
||||
eventSource.onopen = () => {
|
||||
isConnectedRef.current = true
|
||||
reconnectAttemptsRef.current = 0
|
||||
console.log('SSE connected')
|
||||
}
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as SSEEvent
|
||||
onEvent(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to parse SSE event:', err)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource.onerror = () => {
|
||||
isConnectedRef.current = false
|
||||
eventSource.close()
|
||||
|
||||
// 尝试重连
|
||||
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000)
|
||||
reconnectAttemptsRef.current++
|
||||
console.log(`SSE reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current})`)
|
||||
reconnectTimeoutRef.current = setTimeout(connect, delay)
|
||||
} else {
|
||||
console.error('SSE connection failed after max attempts')
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义事件类型
|
||||
eventSource.addEventListener('connected', (event) => {
|
||||
console.log('SSE handshake received:', event)
|
||||
})
|
||||
|
||||
}, [onEvent])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
isConnectedRef.current = false
|
||||
reconnectAttemptsRef.current = 0
|
||||
}, [])
|
||||
|
||||
const isConnected = useCallback(() => {
|
||||
return isConnectedRef.current
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (autoConnect) {
|
||||
connect()
|
||||
}
|
||||
return () => {
|
||||
disconnect()
|
||||
}
|
||||
}, [autoConnect, connect, disconnect])
|
||||
|
||||
return {
|
||||
connect,
|
||||
disconnect,
|
||||
isConnected,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user