mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import {isCancelError} from '@/lib/services/core'
|
||||
import {toast} from 'sonner'
|
||||
|
||||
/**
|
||||
* 错误处理选项
|
||||
*/
|
||||
interface HandleContextErrorOptions {
|
||||
/** 是否显示 toast 提示 */
|
||||
showToast?: boolean
|
||||
/** 是否在控制台记录错误 */
|
||||
logError?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一处理 Context 中的错误
|
||||
*
|
||||
* @param error - 捕获的错误对象
|
||||
* @param defaultMessage - 默认错误消息
|
||||
* @param options - 错误处理选项
|
||||
* @returns 标准化的 Error 对象
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* await fetchData()
|
||||
* } catch (error) {
|
||||
* const errorObject = handleContextError(error, '加载数据失败', {
|
||||
* showToast: true,
|
||||
* logError: true
|
||||
* })
|
||||
* setError(errorObject)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function handleContextError(
|
||||
error: unknown,
|
||||
defaultMessage: string,
|
||||
options: HandleContextErrorOptions = {}
|
||||
): Error {
|
||||
const { showToast = false, logError = true } = options
|
||||
|
||||
// 取消的请求不算错误
|
||||
if (isCancelError(error)) {
|
||||
return new Error('请求已取消')
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : defaultMessage
|
||||
const errorObject = error instanceof Error ? error : new Error(defaultMessage)
|
||||
|
||||
if (logError) {
|
||||
console.error(defaultMessage, error)
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
toast.error(defaultMessage, {
|
||||
description: errorMessage
|
||||
})
|
||||
}
|
||||
|
||||
return errorObject
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function getStrictContext<T>(
|
||||
name?: string,
|
||||
): readonly [
|
||||
({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: T;
|
||||
children?: React.ReactNode;
|
||||
}) => React.JSX.Element,
|
||||
() => T,
|
||||
] {
|
||||
const Context = React.createContext<T | undefined>(undefined);
|
||||
|
||||
const Provider = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: T;
|
||||
children?: React.ReactNode;
|
||||
}) => <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
|
||||
const useSafeContext = () => {
|
||||
const ctx = React.useContext(Context);
|
||||
if (ctx === undefined) {
|
||||
throw new Error(`useContext must be used within ${name ?? 'a Provider'}`);
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
return [Provider, useSafeContext] as const;
|
||||
}
|
||||
|
||||
export { getStrictContext };
|
||||
@@ -0,0 +1,185 @@
|
||||
import PinyinMatch from 'pinyin-match'
|
||||
|
||||
type MatchResult = [number, number] | false;
|
||||
type MatchFunction = (input: string, keys: string) => MatchResult;
|
||||
|
||||
// Handle CJS/ESM interop for pinyin-match
|
||||
const match = ((): MatchFunction | null => {
|
||||
const p = PinyinMatch as unknown;
|
||||
if (!p) return null;
|
||||
|
||||
// Check for .default.match (common in some ESM bundles)
|
||||
const withDefault = p as { default?: { match?: MatchFunction } };
|
||||
if (typeof withDefault.default?.match === 'function') {
|
||||
return withDefault.default.match;
|
||||
}
|
||||
|
||||
// Check for .match (defined in its typings)
|
||||
const withMatch = p as { match?: MatchFunction };
|
||||
if (typeof withMatch.match === 'function') {
|
||||
return withMatch.match;
|
||||
}
|
||||
|
||||
// Check if it's the function itself
|
||||
if (typeof p === 'function') {
|
||||
return p as MatchFunction;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return null;
|
||||
})();
|
||||
|
||||
export interface SearchItem {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
url: string
|
||||
category: 'page' | 'feature' | 'setting' | 'admin'
|
||||
keywords: string[]
|
||||
icon?: string
|
||||
matchRange?: [number, number]
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局搜索数据源
|
||||
* 包含所有可搜索的页面和功能
|
||||
*/
|
||||
export const searchData: SearchItem[] = [
|
||||
// ==================== 首页 ====================
|
||||
{
|
||||
id: 'home',
|
||||
title: '首页',
|
||||
description: '返回首页控制台',
|
||||
url: '/home',
|
||||
category: 'page',
|
||||
keywords: ['home', '主页', '首页', 'dashboard'],
|
||||
},
|
||||
|
||||
// ==================== 文档库 ====================
|
||||
{
|
||||
id: 'docs-api',
|
||||
title: '开发接口文档',
|
||||
description: '查看 RESTful API 接口规格定义',
|
||||
url: '/docs/api',
|
||||
category: 'page',
|
||||
keywords: ['api', 'docs', '文档', '接口', 'specification'],
|
||||
},
|
||||
{
|
||||
id: 'docs-how-to-use',
|
||||
title: '使用帮助文档',
|
||||
description: '查看新手教程和集成示例',
|
||||
url: '/docs/how-to-use',
|
||||
category: 'page',
|
||||
keywords: ['docs', '文档', '使用', 'how to', 'tutorial', '教程', 'help'],
|
||||
},
|
||||
|
||||
// ==================== 个人设置 ====================
|
||||
{
|
||||
id: 'settings',
|
||||
title: '全局设置',
|
||||
description: '配置应用个人偏好选项',
|
||||
url: '/settings',
|
||||
category: 'setting',
|
||||
keywords: ['settings', '设置', '偏好', 'preferences'],
|
||||
},
|
||||
{
|
||||
id: 'settings-profile',
|
||||
title: '我的资料',
|
||||
description: '编辑昵称、头像和个人属性',
|
||||
url: '/settings/profile',
|
||||
category: 'setting',
|
||||
keywords: ['profile', '资料', '个人', '我的', '信息', 'avatar'],
|
||||
},
|
||||
{
|
||||
id: 'settings-appearance',
|
||||
title: '外观设置',
|
||||
description: '配置系统显示主题(亮色/暗色)',
|
||||
url: '/settings/appearance',
|
||||
category: 'setting',
|
||||
keywords: ['appearance', '外观', '主题', 'theme', 'dark', 'light'],
|
||||
},
|
||||
// ==================== 管理员 ====================
|
||||
{
|
||||
id: 'admin-settings',
|
||||
title: '系统设置',
|
||||
description: '管理系统登录注册与认证源配置 (管理员专属)',
|
||||
url: '/admin/settings',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '系统设置', '安全', 'security', 'oidc', 'login'],
|
||||
},
|
||||
{
|
||||
id: 'admin-system',
|
||||
title: '系统配置',
|
||||
description: '动态修改平台核心运行时配置 (管理员专属)',
|
||||
url: '/admin/system',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '系统', '配置', 'system', 'configurations'],
|
||||
},
|
||||
{
|
||||
id: 'admin-users',
|
||||
title: '用户管理',
|
||||
description: '管理平台注册用户的活跃状态 (管理员专属)',
|
||||
url: '/admin/users',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '用户', '管理', 'users', 'status'],
|
||||
},
|
||||
{
|
||||
id: 'admin-tasks',
|
||||
title: '异步任务管理',
|
||||
description: '下发与排查后台异步定时任务 (管理员专属)',
|
||||
url: '/admin/tasks',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '任务', '异步', 'tasks', 'scheduler', 'worker'],
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 搜索功能
|
||||
* @param query 搜索关键词
|
||||
* @param isAdmin 是否为管理员
|
||||
* @returns 匹配的搜索结果
|
||||
*/
|
||||
export function searchItems(query: string, isAdmin: boolean = false): SearchItem[] {
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
// 非管理员不能搜索 admin 类别项
|
||||
const filteredData = isAdmin
|
||||
? searchData
|
||||
: searchData.filter(item => item.category !== 'admin')
|
||||
|
||||
if (!trimmedQuery) {
|
||||
return filteredData
|
||||
}
|
||||
|
||||
return filteredData.map(item => {
|
||||
// 优先匹配标题
|
||||
const titleMatch = typeof match === 'function' ? match(item.title, trimmedQuery) : null
|
||||
if (titleMatch) {
|
||||
return { ...item, matchRange: titleMatch as [number, number] }
|
||||
}
|
||||
|
||||
// 匹配描述
|
||||
if (typeof match === 'function' && match(item.description, trimmedQuery)) {
|
||||
return item
|
||||
}
|
||||
|
||||
// 匹配关键词
|
||||
if (item.keywords.some(keyword => typeof match === 'function' && match(keyword, trimmedQuery))) {
|
||||
return item
|
||||
}
|
||||
|
||||
return null
|
||||
}).filter((item): item is SearchItem => item !== null)
|
||||
.sort((a, b) => {
|
||||
// 标题匹配优先
|
||||
if (a.matchRange && !b.matchRange) return -1
|
||||
if (!a.matchRange && b.matchRange) return 1
|
||||
|
||||
// 如果都是标题匹配,按匹配位置排序
|
||||
if (a.matchRange && b.matchRange) {
|
||||
return a.matchRange[0] - b.matchRange[0]
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user