feat(modules): update layout, notifications, questions, rbac, files

- layout: update site-header.tsx

- notifications: update use-desktop-notifications.ts

- questions: update question-actions.tsx, question-columns.tsx

- rbac: update permission-catalog.ts

- files: update types.ts
This commit is contained in:
SpecialX
2026-07-04 10:23:16 +08:00
parent 98429e87eb
commit 3569d83b8e
6 changed files with 138 additions and 95 deletions

View File

@@ -1,6 +1,6 @@
"use client"
import { useEffect, useRef, useCallback, useState } from "react"
import { useEffect, useRef, useCallback, useSyncExternalStore } from "react"
import type { Notification } from "../types"
@@ -22,6 +22,33 @@ interface UseDesktopNotificationsResult {
showNotification: (notification: Notification) => void
}
// v3: 使用 useSyncExternalStore 订阅浏览器 Notification 权限。
// 模块级监听器集合requestPermission 完成后手动通知 React 重新读取快照。
// Notification API 没有标准的 permissionchange 事件,需手动触发。
const permissionListeners = new Set<() => void>()
function subscribePermission(onStoreChange: () => void): () => void {
permissionListeners.add(onStoreChange)
return () => {
permissionListeners.delete(onStoreChange)
}
}
function getPermissionSnapshot(): NotificationPermission | "unsupported" {
if (typeof window === "undefined" || !("Notification" in window)) return "unsupported"
return Notification.permission
}
function getPermissionServerSnapshot(): NotificationPermission | "unsupported" {
// SSR 期间返回固定值,与 client hydration 一致,避免 hydration mismatch。
// hydration 完成后 React 自动切换到 getSnapshot 并触发重新渲染。
return "unsupported"
}
function notifyPermissionChange(): void {
permissionListeners.forEach((listener) => listener())
}
/**
* 桌面通知 Hook
*
@@ -41,15 +68,24 @@ interface UseDesktopNotificationsResult {
* - Chrome/Edge/Firefox 桌面版完整支持
* - Safari 桌面版 13+ 支持
* - 移动端浏览器不支持(自动降级为无桌面推送)
*
* v3: 使用 useSyncExternalStore 替代 useState + useEffect 检测权限,
* 避免 SSR/CSR 不一致导致的 hydration mismatch同时满足
* react-hooks/set-state-in-effect 规则。
*/
export function useDesktopNotifications(
options?: UseDesktopNotificationsOptions
): UseDesktopNotificationsResult {
const enabled = options?.enabled ?? false
const [permission, setPermission] = useState<NotificationPermission | "unsupported">(
typeof window !== "undefined" && "Notification" in window
? Notification.permission
: "unsupported"
// v3: useSyncExternalStore 自动处理 SSR/CSR 一致性
// - SSR: getPermissionServerSnapshot → "unsupported"
// - hydration: 仍使用 server snapshot一致无 mismatch
// - hydration 后: 切换到 getPermissionSnapshot如有差异自动 re-render
const permission = useSyncExternalStore(
subscribePermission,
getPermissionSnapshot,
getPermissionServerSnapshot
)
const onClickRef = useRef(options?.onClick)
@@ -63,8 +99,9 @@ export function useDesktopNotifications(
if (typeof window === "undefined" || !("Notification" in window)) return
try {
const result = await Notification.requestPermission()
setPermission(result)
await Notification.requestPermission()
// 通知 React 重新读取快照Notification.permission 可能已改变)
notifyPermissionChange()
} catch {
// 某些浏览器可能抛出异常,静默处理
}
@@ -95,17 +132,18 @@ export function useDesktopNotifications(
)
// 当 enabled 变为 true 且权限为 default 时,自动请求权限
// 使用异步队列避免在 effect 中同步调用 setState
useEffect(() => {
if (!enabled || permission !== "default") return
if (typeof window === "undefined" || !("Notification" in window)) return
let cancelled = false
Notification.requestPermission().then((result) => {
if (!cancelled) setPermission(result)
}).catch(() => {
// 某些浏览器可能抛出异常,静默处理
})
Notification.requestPermission()
.then(() => {
if (!cancelled) notifyPermissionChange()
})
.catch(() => {
// 某些浏览器可能抛出异常,静默处理
})
return () => {
cancelled = true