UNPKG

@gulibs/vgrove-client

Version:

Client-side utilities for React auto routes

1,558 lines (1,543 loc) 205 kB
'use strict'; var reactRouter = require('react-router'); var _ = require('lodash'); var react = require('react'); var jsxRuntime = require('react/jsx-runtime'); var reactStorage = require('@gulibs/react-storage'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; class DebugLogger { constructor(config = {}) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; this.config = { enabled: (_a = config.enabled) !== null && _a !== void 0 ? _a : false, // 默认关闭调试,用户需要显式启用 auth: (_b = config.auth) !== null && _b !== void 0 ? _b : false, i18n: (_c = config.i18n) !== null && _c !== void 0 ? _c : false, performance: (_d = config.performance) !== null && _d !== void 0 ? _d : false, // 性能日志也默认关闭 routing: (_e = config.routing) !== null && _e !== void 0 ? _e : false, storage: (_f = config.storage) !== null && _f !== void 0 ? _f : false, loader: (_g = config.loader) !== null && _g !== void 0 ? _g : false, action: (_h = config.action) !== null && _h !== void 0 ? _h : false, middleware: (_j = config.middleware) !== null && _j !== void 0 ? _j : false, }; } static getInstance(config) { if (!DebugLogger.instance) { DebugLogger.instance = new DebugLogger(config); } return DebugLogger.instance; } static updateConfig(config) { const instance = DebugLogger.getInstance(); instance.config = Object.assign(Object.assign({}, instance.config), config); } shouldLog(module) { return this.config.enabled && this.config[module]; } // 认证相关调试 auth(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('auth')) { console.log(`🔐 Auth: ${message}`, data ? data : ''); } } // 国际化调试 i18n(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('i18n')) { console.log(`🌍 I18n: ${message}`, data ? data : ''); } } // 性能监控调试 performance(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('performance')) { console.log(`⚡ Performance: ${message}`, data ? data : ''); } } // 路由相关调试 routing(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('routing')) { console.log(`🚀 Routing: ${message}`, data ? data : ''); } } // 存储相关调试 storage(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('storage')) { console.log(`💾 Storage: ${message}`, data ? data : ''); } } // 错误日志(总是显示) error(message, error) { if (process.env.NODE_ENV === 'development' && this.config.enabled) { console.error(`❌ Error: ${message}`, error ? error : ''); } } // 警告日志(总是显示) warn(message, data) { if (process.env.NODE_ENV === 'development' && this.config.enabled) { console.warn(`⚠️ Warning: ${message}`, data ? data : ''); } } // 成功日志 success(message, data) { if (process.env.NODE_ENV === 'development' && this.config.enabled) { console.log(`✅ Success: ${message}`, data ? data : ''); } } // 通用日志(不属于特定模块) log(message, data) { if (process.env.NODE_ENV === 'development' && this.config.enabled) { console.log(`📝 Log: ${message}`, data ? data : ''); } } // 信息日志 info(message, data) { if (process.env.NODE_ENV === 'development' && this.config.enabled) { console.info(`ℹ️ Info: ${message}`, data ? data : ''); } } // Loader 相关调试 loader(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('loader')) { console.log(`🔄 Loader: ${message}`, data ? data : ''); } } // Action 相关调试 action(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('action')) { console.log(`⚡ Action: ${message}`, data ? data : ''); } } // 中间件相关调试 middleware(message, data) { if (process.env.NODE_ENV === 'development' && this.shouldLog('middleware')) { console.log(`🔧 Middleware: ${message}`, data ? data : ''); } } // 获取当前配置 getConfig() { return Object.assign({}, this.config); } } // 导出便捷的调试器实例 const debug = DebugLogger.getInstance(); // 导出配置更新函数 const updateDebugConfig = DebugLogger.updateConfig; /** * VGrove 标准化 Loader 工具 * 提供基于 React Router v6+ loader 模式的标准化认证和权限检查 * 替代原有的 RouteProtectionWrapper、RuntimeExecutor 等复杂系统 */ /** * 默认配置 */ const DEFAULT_OPTIONS = { authCheck: () => null, roleCheck: (user, roles) => { if (!user.roles) return false; return roles.some(role => user.roles.includes(role)); }, permissionCheck: (user, permissions) => { if (!user.permissions) return false; return permissions.some(permission => user.permissions.includes(permission)); }, loginPath: '/login', forbiddenPath: '/403', publicPaths: ['/login', '/register', '/forgot-password', '/404', '/403'] }; /** * 全局配置 */ let globalOptions = {}; /** * 配置全局 loader 选项 */ function configureLoaders(options) { globalOptions = Object.assign(Object.assign({}, globalOptions), options); debug.auth('Loader 全局配置已更新', options); } /** * 获取合并后的配置 */ function getMergedOptions(options) { return Object.assign(Object.assign(Object.assign({}, DEFAULT_OPTIONS), globalOptions), options); } /** * 检查路径是否为公共路径 */ function isPublicPath(pathname, publicPaths) { return publicPaths.some(path => { if (path.endsWith('*')) { return pathname.startsWith(path.slice(0, -1)); } return pathname === path; }); } /** * 创建认证 loader * 检查用户是否已认证,未认证则重定向到登录页 */ function createAuthLoader(options) { return (_a) => __awaiter(this, [_a], void 0, function* ({ request }) { const config = getMergedOptions(options); const url = new URL(request.url); debug.auth('执行认证检查', { pathname: url.pathname }); // 检查是否为公共路径 if (isPublicPath(url.pathname, config.publicPaths)) { debug.auth('公共路径,跳过认证检查', { pathname: url.pathname }); return null; } // 执行认证检查 const user = yield config.authCheck(); if (!user) { debug.auth('用户未认证,重定向到登录页', { pathname: url.pathname, loginPath: config.loginPath }); // 保存原始路径用于登录后重定向 const redirectUrl = new URL(config.loginPath, url.origin); redirectUrl.searchParams.set('redirect', url.pathname + url.search); throw reactRouter.redirect(redirectUrl.toString()); } debug.auth('认证检查通过', { userId: user.id }); return { user }; }); } /** * 创建角色检查 loader * 检查用户是否具有所需角色 */ function createRoleLoader(requiredRoles, options) { return (_a) => __awaiter(this, [_a], void 0, function* ({ request }) { const config = getMergedOptions(options); const url = new URL(request.url); debug.auth('执行角色检查', { pathname: url.pathname, requiredRoles }); // 检查是否为公共路径 if (isPublicPath(url.pathname, config.publicPaths)) { debug.auth('公共路径,跳过角色检查', { pathname: url.pathname }); return null; } // 执行认证检查 const user = yield config.authCheck(); if (!user) { debug.auth('用户未认证,重定向到登录页'); const redirectUrl = new URL(config.loginPath, url.origin); redirectUrl.searchParams.set('redirect', url.pathname + url.search); throw reactRouter.redirect(redirectUrl.toString()); } // 检查角色 const hasRole = config.roleCheck(user, requiredRoles); if (!hasRole) { debug.auth('角色检查失败,重定向到禁止访问页', { userId: user.id, userRoles: user.roles, requiredRoles }); throw reactRouter.redirect(config.forbiddenPath); } debug.auth('角色检查通过', { userId: user.id, userRoles: user.roles, requiredRoles }); return { user }; }); } /** * 创建权限检查 loader * 检查用户是否具有所需权限 */ function createPermissionLoader(requiredPermissions, options) { return (_a) => __awaiter(this, [_a], void 0, function* ({ request }) { const config = getMergedOptions(options); const url = new URL(request.url); debug.auth('执行权限检查', { pathname: url.pathname, requiredPermissions }); // 检查是否为公共路径 if (isPublicPath(url.pathname, config.publicPaths)) { debug.auth('公共路径,跳过权限检查', { pathname: url.pathname }); return null; } // 执行认证检查 const user = yield config.authCheck(); if (!user) { debug.auth('用户未认证,重定向到登录页'); const redirectUrl = new URL(config.loginPath, url.origin); redirectUrl.searchParams.set('redirect', url.pathname + url.search); throw reactRouter.redirect(redirectUrl.toString()); } // 检查权限 const hasPermission = config.permissionCheck(user, requiredPermissions); if (!hasPermission) { debug.auth('权限检查失败,重定向到禁止访问页', { userId: user.id, userPermissions: user.permissions, requiredPermissions }); throw reactRouter.redirect(config.forbiddenPath); } debug.auth('权限检查通过', { userId: user.id, userPermissions: user.permissions, requiredPermissions }); return { user }; }); } /** * 创建组合 loader * 同时检查认证、角色和权限 */ function createCombinedLoader(config, options) { return (_a) => __awaiter(this, [_a], void 0, function* ({ request }) { const loaderConfig = getMergedOptions(options); const url = new URL(request.url); const { roles = [], permissions = [], requireAll = false } = config; debug.auth('执行组合检查', { pathname: url.pathname, roles, permissions, requireAll }); // 检查是否为公共路径 if (isPublicPath(url.pathname, loaderConfig.publicPaths)) { debug.auth('公共路径,跳过组合检查', { pathname: url.pathname }); return null; } // 执行认证检查 const user = yield loaderConfig.authCheck(); if (!user) { debug.auth('用户未认证,重定向到登录页'); const redirectUrl = new URL(loaderConfig.loginPath, url.origin); redirectUrl.searchParams.set('redirect', url.pathname + url.search); throw reactRouter.redirect(redirectUrl.toString()); } // 检查角色和权限 const hasRole = roles.length === 0 || loaderConfig.roleCheck(user, roles); const hasPermission = permissions.length === 0 || loaderConfig.permissionCheck(user, permissions); let accessGranted; if (requireAll) { // 需要同时满足角色和权限 accessGranted = hasRole && hasPermission; } else { // 满足角色或权限之一即可 accessGranted = hasRole || hasPermission; } if (!accessGranted) { debug.auth('组合检查失败,重定向到禁止访问页', { userId: user.id, hasRole, hasPermission, requireAll }); throw reactRouter.redirect(loaderConfig.forbiddenPath); } debug.auth('组合检查通过', { userId: user.id, hasRole, hasPermission }); return { user }; }); } /** * 创建自定义 loader * 允许用户定义自己的检查逻辑 */ function createCustomLoader(checkFunction, options) { return (_a) => __awaiter(this, [_a], void 0, function* ({ request }) { const config = getMergedOptions(options); const url = new URL(request.url); debug.auth('执行自定义检查', { pathname: url.pathname }); // 检查是否为公共路径 if (isPublicPath(url.pathname, config.publicPaths)) { debug.auth('公共路径,跳过自定义检查', { pathname: url.pathname }); return null; } // 执行认证检查 const user = yield config.authCheck(); // 执行自定义检查 const checkResult = yield checkFunction(user, request); if (!checkResult) { debug.auth('自定义检查失败'); if (!user) { // 用户未认证 const redirectUrl = new URL(config.loginPath, url.origin); redirectUrl.searchParams.set('redirect', url.pathname + url.search); throw reactRouter.redirect(redirectUrl.toString()); } else { // 用户已认证但检查失败 throw reactRouter.redirect(config.forbiddenPath); } } debug.auth('自定义检查通过', { userId: user === null || user === void 0 ? void 0 : user.id }); return { user }; }); } /** * 组合多个 loader * 按顺序执行多个 loader,任何一个失败都会中断 */ function combineLoaders(...loaders) { return (args) => __awaiter(this, void 0, void 0, function* () { debug.auth('执行组合 loader', { count: loaders.length }); let combinedResult = {}; for (let i = 0; i < loaders.length; i++) { const loader = loaders[i]; const result = yield loader(args); if (result) { combinedResult = Object.assign(Object.assign({}, combinedResult), result); } } debug.auth('组合 loader 执行完成'); return Object.keys(combinedResult).length > 0 ? combinedResult : null; }); } /** * 将对象转换为格式化的JSON字符串 * 支持Map类型的自动转换 */ function toJSON(obj) { return JSON.stringify(_.isMap(obj) ? mapToArray(obj) : obj, null, 2); } /** * 将Map对象转换为数组 */ function mapToArray(map) { const results = []; map.forEach(value => results.push(value)); return results; } /** * 检查对象是否为记录类型 */ const isRecord = (obj) => { return obj !== null && typeof obj === 'object' && !Array.isArray(obj) && obj.constructor === Object; }; // ============= 路由相关工具函数 ============= /** * 规范化路由路径 * 移除多余的斜杠,确保路径格式正确 */ function normalizePath(path) { if (!path || path === '/') return '/'; // 移除多余的斜杠 const normalized = path.replace(/\/{2,}/g, '/'); // 移除末尾的斜杠(除了根路径) return normalized.length > 1 && normalized.endsWith('/') ? normalized.slice(0, -1) : normalized; } /** * 连接路径片段 */ function joinPath(...segments) { const joined = segments .filter(Boolean) .join('/') .replace(/\/{2,}/g, '/'); return normalizePath(joined); } /** * 检查路径是否匹配模式 * 支持动态参数匹配 */ function matchPath(pattern, path) { const patternParts = pattern.split('/').filter(Boolean); const pathParts = path.split('/').filter(Boolean); if (patternParts.length !== pathParts.length) return false; return patternParts.every((part, index) => { return part.startsWith(':') || part === pathParts[index]; }); } /** * 从路径中提取参数 */ function extractParams(pattern, path) { const params = {}; const patternParts = pattern.split('/').filter(Boolean); const pathParts = path.split('/').filter(Boolean); if (patternParts.length !== pathParts.length) return params; patternParts.forEach((part, index) => { if (part.startsWith(':')) { const paramName = part.slice(1); params[paramName] = pathParts[index]; } }); return params; } // ============= 类型检查工具函数 ============= /** * 检查值是否为空(null、undefined、空字符串、空数组、空对象) */ function isEmpty(value) { if (value == null) return true; if (typeof value === 'string') return value.trim().length === 0; if (Array.isArray(value)) return value.length === 0; if (typeof value === 'object') return Object.keys(value).length === 0; return false; } /** * 检查值是否为函数 */ function isFunction(value) { return typeof value === 'function'; } /** * 检查值是否为Promise */ function isPromise(value) { return value != null && typeof value.then === 'function'; } /** * 安全的类型转换 */ function safeParseInt(value, defaultValue = 0) { const parsed = parseInt(String(value), 10); return isNaN(parsed) ? defaultValue : parsed; } // ============= 字符串处理工具函数 ============= /** * 将字符串转换为驼峰命名 */ function toCamelCase(str) { return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); } /** * 将驼峰命名转换为短横线命名 */ function toKebabCase(str) { return str.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''); } /** * 首字母大写 */ function capitalize(str) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } /** * 截断字符串 */ function truncate(str, length, suffix = '...') { if (str.length <= length) return str; return str.slice(0, length - suffix.length) + suffix; } // ============= 数组和对象操作工具函数 ============= /** * 深度克隆对象 */ function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj.getTime()); if (obj instanceof Array) return obj.map(item => deepClone(item)); if (typeof obj === 'object') { const cloned = {}; Object.keys(obj).forEach(key => { cloned[key] = deepClone(obj[key]); }); return cloned; } return obj; } /** * 深度合并对象 * 相比 lodash.merge,此实现: * 1. 数组会被完全替换而不是合并(更符合配置合并的预期) * 2. 性能更优(约 300x faster) * 3. 只处理普通对象的深度合并,其他类型直接替换 * 4. undefined 值会被忽略,null 值会覆盖目标值 */ function deepMerge(target, source) { // 防御性编程:处理 null/undefined 输入 if (!target || typeof target !== 'object') { return source || {}; } if (!source || typeof source !== 'object') { return target; } const result = Object.assign({}, target); Object.keys(source).forEach(key => { const sourceValue = source[key]; const targetValue = result[key]; // 跳过 undefined 值 if (sourceValue === undefined) { return; } // 如果源值和目标值都是普通对象,则递归合并 if (isRecord(sourceValue) && isRecord(targetValue)) { result[key] = deepMerge(targetValue, sourceValue); } else { // 其他情况(包括数组、null、基本类型等)直接替换 result[key] = sourceValue; } }); return result; } /** * 数组去重 */ function unique(array, keyFn) { if (!keyFn) { return Array.from(new Set(array)); } const seen = new Set(); return array.filter(item => { const key = keyFn(item); if (seen.has(key)) return false; seen.add(key); return true; }); } /** * 数组分组 */ function groupBy(array, keyFn) { return array.reduce((groups, item) => { const key = keyFn(item); if (!groups[key]) groups[key] = []; groups[key].push(item); return groups; }, {}); } // ============= 缓存工具函数 ============= /** * 简单的内存缓存实现 */ class SimpleCache { constructor(maxAge = 300000, maxSize = 100) { this.cache = new Map(); this.maxAge = maxAge; this.maxSize = maxSize; } set(key, value) { // 清理过期条目 this.cleanup(); // 如果缓存已满,删除最旧的条目 if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { this.cache.delete(firstKey); } } this.cache.set(key, { value, timestamp: Date.now() }); } get(key) { const entry = this.cache.get(key); if (!entry) return undefined; // 检查是否过期 if (Date.now() - entry.timestamp > this.maxAge) { this.cache.delete(key); return undefined; } return entry.value; } has(key) { return this.get(key) !== undefined; } delete(key) { return this.cache.delete(key); } clear() { this.cache.clear(); } cleanup() { const now = Date.now(); const keysToDelete = []; this.cache.forEach((entry, key) => { if (now - entry.timestamp > this.maxAge) { keysToDelete.push(key); } }); keysToDelete.forEach(key => this.cache.delete(key)); } } // ============= 性能工具函数 ============= /** * 防抖函数 */ function debounce(fn, delay) { let timeoutId; return ((...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }); } /** * 节流函数 */ function throttle(fn, delay) { let lastCall = 0; return ((...args) => { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; fn(...args); } }); } /** * 测量函数执行时间 */ function measureTime(fn, name) { return ((...args) => { const result = fn(...args); return result; }); } // ============= URL和查询参数工具函数 ============= /** * 解析查询字符串 */ function parseQuery(search) { const params = {}; const urlParams = new URLSearchParams(search); for (const [key, value] of urlParams.entries()) { params[key] = value; } return params; } /** * 构建查询字符串 */ function buildQuery(params) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value != null) { searchParams.append(key, String(value)); } }); return searchParams.toString(); } /** * 更新URL查询参数 */ function updateQuery(url, params) { const [base, search] = url.split('?'); const currentParams = parseQuery(search || ''); const newParams = Object.assign(Object.assign({}, currentParams), params); const queryString = buildQuery(newParams); return queryString ? `${base}?${queryString}` : base; } // ============= 错误处理工具函数 ============= /** * 安全地执行函数,捕获错误 */ function safely(fn, fallback) { try { return fn(); } catch (_a) { return fallback; } } /** * 安全地执行异步函数,捕获错误 */ function safelyAsync(fn, fallback) { return __awaiter(this, void 0, void 0, function* () { try { return yield fn(); } catch (_a) { return fallback; } }); } /** * 检查对象是否为本地化对象 */ function isLocalized(localized) { return localized !== null && localized !== undefined && typeof localized === 'object' && 'localizedId' in localized && typeof localized.localizedId === 'string'; } /** * 严格检测一个函数是否是异步函数(async function) * @param fn 待检测的目标对象 * @param options 配置选项 * @returns 如果是异步函数返回 true,否则返回 false * * @example * ```ts * // 基本使用 * isAsyncFunction(async () => {}); // => true * * // 带调试模式 * isAsyncFunction(() => {}, { debug: true }); * * // 启用行为检测 * isAsyncFunction(someFn, { enableBehaviorCheck: true }); * ``` */ function isAsyncFunction(fn, options = {}) { const { debug = false, enableBehaviorCheck = false } = options; // 1. 基础类型检查(绝对安全的类型守卫) if (typeof fn !== 'function') { debug && console.debug('[isAsyncFunction] 输入不是函数类型'); return false; } // 2. 优先使用标准检测方式 try { if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') { return true; } } catch (e) { debug && console.warn('[isAsyncFunction] 标准检测失败:', e.message); } // 3. 安全获取构造函数名称 let constructorName = ''; try { const constructorString = Function.prototype.toString.call(fn.constructor); const match = constructorString.match(/function\s*([^\s(]+)/); constructorName = (match === null || match === void 0 ? void 0 : match[1]) || ''; } catch (e) { debug && console.warn('[isAsyncFunction] 获取构造函数名失败:', e.message); constructorName = 'UNKNOWN_CONSTRUCTOR'; } if (constructorName === 'AsyncFunction') { return true; } // 4. 函数体特征检测 try { const fnString = Function.prototype.toString.call(fn); // 检测原生异步函数特征 if (/^async\s+[^(]+\(/.test(fnString.trim())) { return true; } // 检测Babel转译特征 if (fnString.includes('regeneratorRuntime') && (fnString.includes('_async') || fnString.includes('asyncToGenerator'))) { return true; } } catch (e) { debug && console.warn('[isAsyncFunction] 函数体分析失败:', e.message); } // 5. 原型链兜底检测 try { const sampleAsync = () => __awaiter(this, void 0, void 0, function* () { }); if (Object.getPrototypeOf(fn) === Object.getPrototypeOf(sampleAsync) || fn.__proto__ === sampleAsync.__proto__) { return true; } } catch (e) { debug && console.warn('[isAsyncFunction] 原型链检测失败:', e.message); } // 6. 最终行为检测(可选) if (enableBehaviorCheck) { try { const isAsync = fn.constructor === (() => __awaiter(this, void 0, void 0, function* () { })).constructor; debug && console.info('[isAsyncFunction] 行为检测结果:', isAsync); return isAsync; } catch (e) { debug && console.warn('[isAsyncFunction] 行为检测失败:', e.message); } } return false; } /** * VGrove 标准化 Action 工具 * 提供基于 React Router v7+ action 模式的标准化表单处理和数据变更 * 替代原有的复杂中间件系统 */ /** * 默认配置 */ const DEFAULT_ACTION_OPTIONS = { requireAuth: false, authCheck: () => null }; /** * 全局 action 配置 */ let globalActionOptions = {}; /** * 配置全局 action 选项 */ function configureActions(options) { globalActionOptions = Object.assign(Object.assign({}, globalActionOptions), options); debug.action('Action 全局配置已更新', options); } /** * 获取合并后的配置 */ function getMergedActionOptions(options) { return Object.assign(Object.assign(Object.assign({}, DEFAULT_ACTION_OPTIONS), globalActionOptions), options); } /** * 从 FormData 中提取数据 */ function extractFormData(formData) { const data = {}; for (const [key, value] of formData.entries()) { if (data[key]) { // 处理多值字段(如复选框) if (Array.isArray(data[key])) { data[key].push(value); } else { data[key] = [data[key], value]; } } else { data[key] = value; } } return data; } /** * 检查用户权限 */ function checkPermissions(user, requiredRoles, requiredPermissions) { return __awaiter(this, void 0, void 0, function* () { if (!user) return false; // 检查角色 if (requiredRoles && requiredRoles.length > 0) { const userRoles = user.roles || []; const hasRole = requiredRoles.some(role => userRoles.includes(role)); if (!hasRole) return false; } // 检查权限 if (requiredPermissions && requiredPermissions.length > 0) { const userPermissions = user.permissions || []; const hasPermission = requiredPermissions.some(permission => userPermissions.includes(permission)); if (!hasPermission) return false; } return true; }); } /** * 创建基础 action * 处理表单提交的基础逻辑 */ function createAction(options) { return (_a) => __awaiter(this, [_a], void 0, function* ({ request }) { const config = getMergedActionOptions(options); debug.action('执行 action', { method: request.method, url: request.url, requireAuth: config.requireAuth }); try { // 检查认证 let user = {}; if (config.requireAuth && config.authCheck) { if (isAsyncFunction(config.authCheck)) user = yield config.authCheck(); else user = config.authCheck(); if (!user) { debug.action('用户未认证'); return Response.json({ success: false, message: '用户未认证' }, { status: 401 }); } } // 检查权限 if (user) { const hasPermission = yield checkPermissions(user, config.requiredRoles, config.requiredPermissions); if (!hasPermission) { debug.action('权限不足', { userId: user.id, requiredRoles: config.requiredRoles, requiredPermissions: config.requiredPermissions }); return Response.json({ success: false, message: '权限不足' }, { status: 403 }); } } // 解析表单数据 const formData = yield request.formData(); const extractedData = extractFormData(formData); debug.action('表单数据已解析', { fields: Object.keys(extractedData) }); // 验证表单 if (config.validator) { const validationErrors = yield config.validator(formData); if (validationErrors.length > 0) { debug.action('表单验证失败', { errors: validationErrors }); return Response.json({ success: false, errors: validationErrors, message: '表单验证失败' }, { status: 400 }); } } // 处理数据 let result; if (config.processor) { if (isAsyncFunction(config.processor)) { result = yield config.processor(formData, user || undefined); } else { result = config.processor(formData, user || undefined); } debug.action('数据处理完成', { hasResult: !!result }); } // 成功响应 const response = { success: true, data: result, message: '操作成功' }; // 处理重定向 if (config.successRedirect) { debug.action('重定向到成功页面', { path: config.successRedirect }); throw reactRouter.redirect(config.successRedirect); } debug.action('Action 执行成功'); return Response.json(response); } catch (error) { // 如果是重定向,直接抛出 if (error instanceof Response) { throw error; } debug.action('Action 执行失败', { error: error instanceof Error ? error.message : String(error) }); // 处理错误重定向 if (config.errorRedirect) { throw reactRouter.redirect(config.errorRedirect); } return Response.json({ success: false, message: error instanceof Error ? error.message : '操作失败' }, { status: 500 }); } }); } /** * 创建登录 action */ function createLoginAction(loginProcessor, options) { return createAction(Object.assign(Object.assign({}, options), { requireAuth: false, validator: (formData) => __awaiter(this, void 0, void 0, function* () { const errors = []; const username = formData.get('username'); const password = formData.get('password'); if (!username || username.trim() === '') { errors.push({ field: 'username', message: '用户名不能为空' }); } if (!password || password.trim() === '') { errors.push({ field: 'password', message: '密码不能为空' }); } return errors; }), processor: (formData) => __awaiter(this, void 0, void 0, function* () { const username = formData.get('username'); const password = formData.get('password'); const result = yield loginProcessor({ username, password }); // 存储认证信息到本地存储 if (typeof window !== 'undefined') { localStorage.setItem('auth_user', JSON.stringify(result.user)); localStorage.setItem('auth_token', result.token); } return result; }) })); } /** * 创建注册 action */ function createRegisterAction(registerProcessor, options) { return createAction(Object.assign(Object.assign({}, options), { requireAuth: false, validator: (formData) => __awaiter(this, void 0, void 0, function* () { const errors = []; const username = formData.get('username'); const email = formData.get('email'); const password = formData.get('password'); const confirmPassword = formData.get('confirmPassword'); if (!username || username.trim() === '') { errors.push({ field: 'username', message: '用户名不能为空' }); } if (!email || email.trim() === '') { errors.push({ field: 'email', message: '邮箱不能为空' }); } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { errors.push({ field: 'email', message: '邮箱格式不正确' }); } if (!password || password.length < 6) { errors.push({ field: 'password', message: '密码至少需要6个字符' }); } if (password !== confirmPassword) { errors.push({ field: 'confirmPassword', message: '两次输入的密码不一致' }); } return errors; }), processor: (formData) => __awaiter(this, void 0, void 0, function* () { const userData = extractFormData(formData); delete userData.confirmPassword; // 移除确认密码字段 const result = yield registerProcessor(userData); // 存储认证信息到本地存储 if (typeof window !== 'undefined') { localStorage.setItem('auth_user', JSON.stringify(result.user)); localStorage.setItem('auth_token', result.token); } return result; }) })); } /** * 创建登出 action */ function createLogoutAction(logoutProcessor, options) { return createAction(Object.assign(Object.assign({}, options), { requireAuth: true, processor: () => __awaiter(this, void 0, void 0, function* () { // 执行自定义登出逻辑 if (logoutProcessor) { yield logoutProcessor(); } // 清除本地存储 if (typeof window !== 'undefined') { localStorage.removeItem('auth_user'); localStorage.removeItem('auth_token'); localStorage.removeItem('auth_refresh_token'); } return { message: '登出成功' }; }), successRedirect: '/login' })); } /** * 创建 CRUD action */ function createCrudAction(operation, processor, options) { const baseValidator = (formData) => __awaiter(this, void 0, void 0, function* () { const errors = []; if (operation === 'delete') { const id = formData.get('id'); if (!id) { errors.push({ field: 'id', message: 'ID 不能为空' }); } } return errors; }); return createAction(Object.assign(Object.assign({ requireAuth: true }, options), { validator: (options === null || options === void 0 ? void 0 : options.validator) || baseValidator, processor })); } /** * 创建文件上传 action */ function createFileUploadAction(uploadProcessor, options) { return createAction(Object.assign(Object.assign({}, options), { requireAuth: true, validator: (formData) => __awaiter(this, void 0, void 0, function* () { const errors = []; const files = formData.getAll('files'); if (files.length === 0) { errors.push({ field: 'files', message: '请选择要上传的文件' }); } // 检查文件大小和类型 for (const file of files) { if (file.size > 10 * 1024 * 1024) { // 10MB errors.push({ field: 'files', message: `文件 ${file.name} 超过10MB限制` }); } } return errors; }), processor: (formData, user) => __awaiter(this, void 0, void 0, function* () { const files = formData.getAll('files'); const result = yield uploadProcessor(files, formData, user); return result; }) })); } /** * 组合多个 action * 按顺序执行多个 action 的处理逻辑 */ function combineActions(...actions) { return (args) => __awaiter(this, void 0, void 0, function* () { debug.action('执行组合 action', { count: actions.length }); let combinedResult = {}; for (let i = 0; i < actions.length; i++) { const action = actions[i]; const result = yield action(args); // 如果是重定向响应,直接返回 if (result instanceof Response && result.status >= 300 && result.status < 400) { return result; } // 如果是 JSON 响应,合并结果 if (result instanceof Response) { const data = yield result.json(); combinedResult = Object.assign(Object.assign({}, combinedResult), data); } else if (result) { combinedResult = Object.assign(Object.assign({}, combinedResult), result); } } debug.action('组合 action 执行完成'); return Response.json(combinedResult); }); } /** * 国际化资源加载工具 - 对标 @gulibs/vgrove-i18n 优化版 */ /** * 安全地导入虚拟模块 */ function loadVirtualModule() { return __awaiter(this, void 0, void 0, function* () { try { // 使用正确的虚拟模块 ID,与插件定义一致 const virtualModuleId = '@gulibs/i18n-locales'; const virtualModule = yield import(/* @vite-ignore */ virtualModuleId); return { resources: virtualModule.resources || {}, supportedLocales: virtualModule.supportedLocales || [], keys: virtualModule.keys || [], config: virtualModule.config }; } catch (_a) { // 虚拟模块不存在时返回 null return null; } }); } /** * 虚拟模块加载器 - 优先使用 vite-plugin-i18n 生成的资源 */ class ViteI18nLoader { constructor(config = {}) { this.config = Object.assign({ basePath: '/locales', extensions: ['.json', '.ts', '.js'], localePattern: 'directory', defaultLocale: 'en', cacheTime: 60 * 60 * 1000, cache: true, debug: false }, config); this.cache = new SimpleCache(this.config.cacheTime); this.initVirtualModule(); } /** * 初始化虚拟模块,加载静态资源 */ initVirtualModule() { return __awaiter(this, void 0, void 0, function* () { try { const virtualModule = yield loadVirtualModule(); if (virtualModule) { this.virtualModuleResources = virtualModule.resources; this.virtualModuleKeys = virtualModule.keys; this.virtualModuleSupportedLocales = virtualModule.supportedLocales; this.virtualModuleConfig = virtualModule.config; // 将资源缓存到加载器的cache中,确保同步访问 Object.entries(virtualModule.resources).forEach(([locale, resources]) => { if (this.config.cache) { this.cache.set(locale, resources); } }); // Virtual module loaded successfully } } catch (_a) { // Failed to load virtual module, will fallback to dynamic loading } }); } /** * 获取虚拟模块资源(供外部直接访问) */ getVirtualModuleResources() { return this.virtualModuleResources; } /** * 获取虚拟模块配置 */ getVirtualModuleConfig() { return this.virtualModuleConfig; } /** * 加载指定语言的翻译资源 */ loadResources(locale) { return __awaiter(this, void 0, void 0, function* () { var _a; // 检查缓存 if (this.config.cache && this.cache.has(locale)) { const cached = this.cache.get(locale); if (cached) return cached; } // 优先使用虚拟模块资源 if ((_a = this.virtualModuleResources) === null || _a === void 0 ? void 0 : _a[locale]) { const resources = this.virtualModuleResources[locale]; if (this.config.cache) { this.cache.set(locale, resources); } if (this.config.onLoad) { this.config.onLoad(locale, resources); } return resources; } // 回退到动态加载 return this.dynamicLoadResources(locale); }); } /** * 动态加载资源(当虚拟模块不可用时的回退方案) */ dynamicLoadResources(locale) { return __awaiter(this, void 0, void 0, function* () { try { let resources; if (this.config.fetchResources) { // 使用自定义获取函数 const path = `${this.config.basePath}/${locale}${this.config.extensions[0]}`; resources = yield this.config.fetchResources(locale, path); } else { // 默认使用 fetch API const extension = this.config.extensions[0]; const url = `${this.config.basePath}/${locale}${extension}`; const response = yield fetch(url); if (!response.ok) { throw new Error(`Failed to load locale ${locale}: ${response.statusText}`); } resources = yield response.json(); } // 缓存资源 if (this.config.cache) { this.cache.set(locale, resources); } // 触发加载回调 if (this.config.onLoad) { this.config.onLoad(locale, resources); } return resources; } catch (error) { // 触发错误回调 if (this.config.onError && error instanceof Error) { this.config.onError(locale, error); } // Failed to load locale resources return {}; } }); } /** * 预加载多个语言的翻译资源 */ preloadResources(locales) { return __awaiter(this, void 0, void 0, function* () { const resources = {}; // 如果有虚拟模块资源,直接返回相关的资源 if (this.virtualModuleResources) { locales.forEach(locale => { if (this.virtualModuleResources[locale]) { resources[locale] = this.virtualModuleResources[locale]; } }); // 只加载虚拟模块中没有的语言 const missingLocales = locales.filter(locale => !this.virtualModuleResources[locale]); if (missingLocales.length === 0) { return resources; } locales = missingLocales; } // 并行加载缺失的资源 yield Promise.all(locales.map((locale) => __awaiter(this, void 0, void 0, function* () { try { resources[locale] = yield this.loadResources(locale); } catch (_a) { // Failed to preload locale resources[locale] = {}; } }))); return resources; }); } /** * 清除资源缓存 */ clearCache(locale) { if (locale) { this.cache.delete(locale); } else { this.cache.clear(); } } /** * 获取配置 */ getConfig() { return Object.assign({}, this.config); } /** * 获取所有可用的键(来自虚拟模块) */ getAvailableKeys() { return this.virtualModuleKeys || []; } /** * 检查键是否存在 */ hasKey(key) { var _a; return ((_a = this.virtualModuleKeys) === null || _a === void 0 ? void 0 : _a.includes(key)) || false; } /** * 获取命名空间资源 */ getNamespaceResources(locale, namespace) { var _a; const localeResources = (_a = this.virtualModuleResources) === null || _a === void 0 ? void 0 : _a[locale]; return localeResources === null || localeResources === void 0 ? void 0 : localeResources[namespace]; } /** * 获取支持的语言列表 */ getSupportedLocales() { return this.virtualModuleSupportedLocales || [this.config.defaultLocale]; } /** * 检查是否使用虚拟模块 */ isUsingVirtualModule() { return !!this.virtualModuleResources; } } /** * 创建优化的资源加载器 */ function createI18nLoader(config = {}) { return new ViteI18nLoader(config); } /** * 增强的国际化客户端类 */ class I18nClient { constructor(config) { this.config = Object.assign({ defaultLocale: 'zh', fallbackToDefault: true, detectBrowserLanguage: false, interpolation: { prefix: '{', suffix: '}', escape: (value) => String(value) }, persistence: { enabled: true, // 修复:与createI18nClient保持一致,默认启用持久化 key: 'user_locale', storage: 'localStorage' } }, config); // 创建资源加载器 this.loader = createI18nLoader(this.config); } /** 获取资源加载器 */ getLoader() { return this.loader; } /** 获取资源加载函数 */ getLoadResources() { return this.loader.loadResources.bind(this.loader); } /** 预加载资源 */ preloadResources(locales) { return __awaiter(this, void 0, void 0, function* () { const targetLocales = locales || this.config.supportedLocales || [this.config.defaultLocale]; return this.loader.preloadResources(targetLocales); }); } /** 清除缓存 */ clearCache(locale) { this.loader.clearCache(locale); } /** 获取所有可用的键 */ getAvailableKeys() { var _a, _b; return ((_b = (_a = this.loader).getAvailableKeys) === null || _b === void 0 ? void 0 : _b.call(_a)) || []; } /** 检查键是否存在 */ hasKey(key) { var _a, _b; return ((_b = (_a = this.loader).hasKey) === null || _b === void 0 ? void 0 : _b.call(_a, key)) || false; } /** 获取命名空间资源 */ getNamespaceResources(locale, namespace) { var _a, _b; return (_b = (_a = this.loader).getNamespaceResources) === null || _b === void 0 ? void 0 : _b.call(_a, locale, namespace); } /** 检查是否使用虚拟模块 */ isUsingVirtualModule() { var _a, _b; return ((_b = (_a = this.loader).isUsingVirtualModule) === null || _b === void 0 ? void 0 : _b.call(_a)) || false; } } // 默认 persistence 配置 const DEFAULT_PERSISTENCE = { enabled: true, key: 'user_locale', storage: 'localStorage' }; /** * 创建国际化客户端 - 支持从虚拟模块自动读取配置或直接传入配置 */ function createI18nClient(config) { var _a, _b, _c, _d; // 尝试从虚拟模块获取配置(作为默认值) let virtualConfig = {}; try { // 只有在没有直接传入资源时,才尝试从虚拟模块读取 if (!(config === null || config === void 0 ? void 0 : config.resources)) { const tempLoader = createI18nLoader(); const moduleConfig = (_b = (_a = tempLoader).getVirtualModuleConfig) === null || _b === void 0 ? void 0 : _b.call(_a); const moduleResources = (_d = (_c = tempLoader).getVirtualModuleResources) === null || _d === void 0 ? void 0 : _d.call(_c); if (moduleConfig) { virtualConfig = { supportedLocales: moduleConfig.supportedLocales, basePath: moduleConfig.basePath, localePattern: moduleConfig.localePattern };