docs: add comments to the src/routes & src/store & src/utils directory

This commit is contained in:
zhaoying
2026-02-02 16:37:32 +08:00
parent 6194222289
commit 9a38e8a4a0
13 changed files with 546 additions and 435 deletions

View File

@@ -1,23 +1,39 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:33:11
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:33:11
*/
/**
* Route Configuration
*
* Manages application routing with:
* - Dynamic route generation from JSON configuration
* - Lazy loading of components for code splitting
* - Nested route support
* - Component mapping validation
* - Hash-based routing
*
* @module routes
*/
import { lazy, type LazyExoticComponent, type ComponentType, type ReactNode } from 'react';
import { createHashRouter, createRoutesFromElements, Route } from 'react-router-dom';
// 导入路由配置JSON
/** Import route configuration JSON */
import routesConfig from './routes.json';
import Ontology from '@/views/Ontology';
// 递归函数,用于生成路由元素
// 递归收集所有路由中的element
/** Recursively collect all element names from routes */
function collectElements(routes: RouteConfig[]): Set<string> {
const elements = new Set<string>();
function traverse(routeList: RouteConfig[]) {
routeList.forEach(route => {
// 添加当前路由的element
/** Add current route's element */
elements.add(route.element);
// 递归处理子路由
/** Recursively process child routes */
if (route.children && route.children.length > 0) {
traverse(route.children);
}
@@ -28,15 +44,15 @@ function collectElements(routes: RouteConfig[]): Set<string> {
return elements;
}
// 直接定义组件映射表,避免动态路径解析问题
/** Component mapping table - maps element names to lazy-loaded components */
const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> = {
// 布局组件
/** Layout components */
AuthLayout: lazy(() => import('@/components/Layout/AuthLayout')),
AuthSpaceLayout: lazy(() => import('@/components/Layout/AuthSpaceLayout')),
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
NoAuthLayout: lazy(() => import('@/components/Layout/NoAuthLayout')),
// 视图组件
/** View components */
Index: lazy(() => import('@/views/Index')),
Home: lazy(() => import('@/views/Home')),
UserMemory: lazy(() => import('@/views/UserMemory')),
@@ -78,7 +94,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
NotFound: lazy(() => import('@/views/NotFound'))
};
// 检查并报告缺失的组件
/** Check and report missing components */
const allElements = collectElements(routesConfig);
allElements.forEach(elementName => {
if (!componentMap[elementName]) {
@@ -86,23 +102,27 @@ allElements.forEach(elementName => {
}
});
// 确保NotFound组件总是存在作为兜底
/** Ensure NotFound component always exists as fallback */
if (!componentMap['NotFound']) {
componentMap['NotFound'] = lazy(() => import('@/views/NotFound/index.tsx'));
}
// 路由配置类型定义
/** Route configuration type definition */
interface RouteConfig {
/** Route path */
path?: string;
/** Component element name */
element: string;
/** Component file path (optional) */
componentPath?: string;
/** Child routes */
children?: RouteConfig[];
}
// 递归函数,用于生成路由元素
/** Recursively generate route elements from configuration */
const generateRoutes = (routes: RouteConfig[]): ReactNode => {
return routes.map((route, index) => {
// 获取组件
/** Get component from mapping */
const componentKey = route.element as keyof typeof componentMap;
const Component = componentMap[componentKey];
@@ -111,7 +131,7 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
return null;
}
// 如果有子路由
/** If has child routes, create nested route */
if (route.children) {
return (
<Route key={index} element={<Component />}>
@@ -120,7 +140,7 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
);
}
// 如果有path属性则为普通路由
/** If has path property, create regular route */
if (route.path) {
return <Route key={index} path={route.path} element={<Component />} />;
}
@@ -129,7 +149,7 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
});
};
// 创建路由
/** Create hash router from route configuration */
const router = createHashRouter(
createRoutesFromElements(
generateRoutes(routesConfig)

View File

@@ -1,11 +1,22 @@
/*
* @Description:
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2026-01-05 17:22:23
* @LastEditors: yujiangping
* @LastEditTime: 2026-01-15 21:02:43
* @Author: ZhaoYing
* @Date: 2026-02-02 16:33:22
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:33:22
*/
/**
* Locale Store
*
* Manages internationalization (i18n) and localization with:
* - Language switching (English/Chinese)
* - Timezone management
* - Ant Design locale configuration
* - Custom Tour component translations
* - Day.js timezone support
*
* @store
*/
import { create } from 'zustand'
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
@@ -14,13 +25,12 @@ import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import i18n from '@/i18n';
import { timezoneToAntdLocaleMap } from '@/utils/timezones';
// 扩展dayjs插件
/** Extend dayjs with timezone plugins */
dayjs.extend(utc);
dayjs.extend(timezone);
// 自定义中文 locale修改 Tour 组件的按钮文字
/** Custom Chinese locale with modified Tour component button text */
const customZhCN: Locale = {
...zhCN,
Tour: {
@@ -31,7 +41,7 @@ const customZhCN: Locale = {
},
};
// 自定义英文 locale修改 Tour 组件的按钮文字
/** Custom English locale with modified Tour component button text */
const customEnUS: Locale = {
...enUS,
Tour: {
@@ -43,19 +53,27 @@ const customEnUS: Locale = {
};
/** Internationalization state interface */
interface I18nState {
/** Current language code */
language: string;
/** Ant Design locale object */
locale: Locale;
/** Current timezone */
timeZone: string;
/** Change application language */
changeLanguage: (language: string) => void;
/** Change timezone (triggers page reload) */
changeTimeZone: (timeZone: string) => void;
}
/** Initialize from localStorage or use defaults */
const initialTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai'
const initialLanguage = localStorage.getItem('language') || 'en'
const initialLocale = initialLanguage === 'en' ? customEnUS : customZhCN
i18n.changeLanguage(initialLanguage)
/** Internationalization store */
export const useI18n = create<I18nState>((set, get) => ({
language: initialLanguage,
locale: initialLocale,
@@ -68,8 +86,9 @@ export const useI18n = create<I18nState>((set, get) => ({
changeTimeZone: (timeZone: string) => {
const { timeZone: lastTimeZone } = get()
set({ timeZone })
/** Reload page if timezone changed */
if (lastTimeZone !== timeZone) {
window.location.reload()
}
},
}))
}))

View File

@@ -1,6 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:33:34
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:33:34
*/
/**
* Menu Store
*
* Manages application menu and breadcrumb navigation with:
* - Menu loading from JSON configuration
* - Sidebar collapse state
* - Breadcrumb generation from menu paths
* - Custom breadcrumb support
* - Separate menu contexts (space/manage)
*
* @store
*/
import { create } from 'zustand'
import AllMenus from './menu.json'
/** Menu item interface */
export interface MenuItem {
id: number;
parent: number;
@@ -22,20 +42,32 @@ export interface MenuItem {
master?: string | null;
disposable?: boolean;
appSystem?: string | null;
subs: MenuItem[] | null;
subs?: MenuItem[] | null;
onClick?: (e?: React.MouseEvent) => void | boolean;
}
/** Menu state interface */
interface MenuState {
/** Sidebar collapsed state */
collapsed: boolean;
/** Toggle sidebar collapse */
toggleSider: () => void;
/** All menus by context */
allMenus: Record<'space' | 'manage', MenuItem[]>;
/** All breadcrumbs by context */
allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>;
/** Load menus for specific context */
loadMenus: (source: 'space' | 'manage') => void;
/** Update breadcrumbs based on key path */
updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void;
/** Set custom breadcrumbs */
setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void;
}
/** Initialize breadcrumbs from localStorage */
const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]'
/** Menu store */
export const useMenu = create<MenuState>((set, get) => ({
collapsed: localStorage.getItem('collapsed') === 'true',
allMenus: {
@@ -61,7 +93,7 @@ export const useMenu = create<MenuState>((set, get) => ({
console.log('updateBreadcrumbs paths:', paths);
if (paths.length === 3) {
// 三级菜单:[subSubPath, subId, menuId]
/** Three-level menu: [subSubPath, subId, menuId] */
const menuId = paths[2];
const subId = paths[1];
const subSubPath = paths[0];
@@ -81,7 +113,7 @@ export const useMenu = create<MenuState>((set, get) => ({
}
}
} else {
// 原有逻辑处理一级和二级菜单
/** Original logic for one-level and two-level menus */
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
if (matchedMenu) {
@@ -107,4 +139,4 @@ export const useMenu = create<MenuState>((set, get) => ({
set({ allBreadcrumbs })
localStorage.setItem('breadcrumbs', JSON.stringify(allBreadcrumbs))
},
}))
}))

View File

@@ -1,3 +1,22 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:33:54
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:33:54
*/
/**
* User Store
*
* Manages user authentication and profile with:
* - User information storage
* - Login/logout functionality
* - Token management (access & refresh)
* - Workspace storage type
* - Navigation guards for workspace access
*
* @store
*/
import { create } from 'zustand'
import { clearAuthData } from '@/utils/auth';
import type { User } from '@/views/UserManagement/types'
@@ -5,6 +24,7 @@ import { getUsers, refreshToken, logout } from '@/api/user'
import { getWorkspaceStorageType } from '@/api/workspaces';
import { cookieUtils } from '@/utils/request'
/** Login information interface */
export interface LoginInfo {
access_token: string;
expires_at: string;
@@ -12,24 +32,37 @@ export interface LoginInfo {
refresh_token: string;
token_type: 'bearer'
}
/** User state interface */
export interface UserState {
/** Current user information */
user: User;
/** Login token information */
loginInfo: LoginInfo;
/** Workspace storage type */
storageType: string | null;
/** Update login information */
updateLoginInfo: (values: LoginInfo) => void;
/** Get user information */
getUserInfo: (flag?: boolean) => void;
/** Clear user information */
clearUserInfo: () => void;
/** Logout user */
logout: () => void;
/** Get workspace storage type */
getStorageType: () => void;
/** Check and redirect if workspace not set */
checkJump: () => void;
}
/** Pages that don't require workspace */
export const whitePage = [
'/conversation',
'/login',
'/invite-register'
]
/** User store */
export const useUser = create<UserState>((set, get) => ({
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User,
loginInfo: {} as LoginInfo,
@@ -101,9 +134,10 @@ export const useUser = create<UserState>((set, get) => ({
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
const hash = window.location.hash;
/** Redirect to index if user has no workspace and not on whitelist page */
if (localUser.id && (!localUser.current_workspace_id || localUser.current_workspace_id === '') && !whitePage.find(vo => hash.includes(vo))) {
console.log('whitePage', whitePage.find(vo => hash.includes(vo)))
window.location.href = '/#/index'
}
},
}))
}))

View File

@@ -1,13 +1,27 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:34:04
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:34:04
*/
/**
* API密钥替换工具
* API Key Replacer Utility
*
* Provides functions to mask and detect API keys in text for security purposes.
* Supports multiple API key formats (service, agent, multi-agent, workflow).
*
* @module apiKeyReplacer
*/
/** API key pattern definitions for different types */
const API_KEY_PATTERNS = {
service: /sk-service-[A-Za-z0-9_-]+/g,
agent: /sk-agent-[A-Za-z0-9_-]+/g,
multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g,
workflow: /sk-workflow-[A-Za-z0-9_-]+/g
}
/** API key prefix definitions */
const API_KEY_PREFIX = {
service: 'sk-service-',
agent: 'sk-agent-',
@@ -16,9 +30,9 @@ const API_KEY_PREFIX = {
}
/**
* 替换文本中的API密钥为*号
* @param text 原始文本
* @returns 替换后的文本
* Replace API keys in text with asterisks
* @param text - Original text
* @returns Text with masked API keys
*/
export const maskApiKeys = (text: string): string => {
if (!text) return text
@@ -37,10 +51,10 @@ export const maskApiKeys = (text: string): string => {
}
/**
* 检测文本中是否包含API密钥
* @param text 待检测文本
* @returns 是否包含API密钥
* Detect if text contains API keys
* @param text - Text to check
* @returns Whether text contains API keys
*/
export const hasApiKeys = (text: string): boolean => {
return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text))
}
}

View File

@@ -1,7 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:34:12
*/
/**
* Authentication Utility
*
* Provides functions to clear authentication data and redirect to login.
*
* @module auth
*/
import { cookieUtils } from './request'
/**
* Clear all authentication data and cookies
* Removes user info, breadcrumbs, and all cookies
*/
export const clearAuthData = () => {
console.log("Clearing auth data and redirecting to login");
localStorage.removeItem('user')
localStorage.removeItem('breadcrumbs')
cookieUtils.clear();
}
}

View File

@@ -1,14 +1,34 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:34:23
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:34:23
*/
/**
* Common Utility Functions
*
* Provides general-purpose utility functions.
*
* @module common
*/
/**
* Generate a random string with specified length and character types
* @param length - Length of the string (default: 12)
* @param isHasSpecialChars - Whether to include special characters (default: true)
* @returns Random string
*/
export const randomString = (length: number = 12, isHasSpecialChars: boolean = true) => {
// 定义字符集:大写字母、小写字母、数字和特殊字符
/** Define character sets: uppercase, lowercase, numbers, and special characters */
const uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const lowercaseChars = 'abcdefghijklmnopqrstuvwxyz';
const numberChars = '0123456789';
const specialChars = '!@#$%^&*_+-=|;:,.?';
// 合并所有字符集
/** Combine all character sets */
let allChars = uppercaseChars + lowercaseChars + numberChars;
// 确保至少包含每种类型的字符
/** Ensure at least one character of each type */
let str =
uppercaseChars[Math.floor(Math.random() * uppercaseChars.length)] +
lowercaseChars[Math.floor(Math.random() * lowercaseChars.length)] +
@@ -18,11 +38,11 @@ export const randomString = (length: number = 12, isHasSpecialChars: boolean = t
str+= specialChars[Math.floor(Math.random() * specialChars.length)];
}
// 填充剩余的字符使总长度为12
/** Fill remaining characters to reach desired length */
for (let i = 4; i < length; i++) {
str += allChars[Math.floor(Math.random() * allChars.length)];
}
// 打乱密码字符顺序
/** Shuffle the string characters */
return str.split('').sort(() => Math.random() - 0.5).join('');
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,32 +1,46 @@
/**
* 格式化日期时间
* @param value 时间戳(毫秒)或日期字符串
* @param format 目标格式,支持 YYYY-MM-DD HH:mm:ss、YYYY/MM/DD HH:mm:ss、HH:mm 等
* @returns 格式化后的日期时间字符串
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:34:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:34:43
*/
/**
* Format Utility
*
* Provides date/time formatting functions with timezone support.
*
* @module format
*/
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
// 扩展dayjs插件
/** Extend dayjs with timezone plugins */
dayjs.extend(utc);
dayjs.extend(timezone);
/**
* Format date/time with timezone support
* @param value - Timestamp (milliseconds) or date string
* @param format - Target format, supports YYYY-MM-DD HH:mm:ss, YYYY/MM/DD HH:mm:ss, HH:mm, etc.
* @returns Formatted date/time string
*/
export const formatDateTime = (
value: string | number | null | undefined,
format: string = 'YYYY-MM-DD HH:mm:ss'
): string => {
if (!value) return '';
// 检查日期是否有效
/** Check if date is valid */
if (!dayjs(value).isValid()) {
return '';
}
// 每次调用都获取最新的时区设置
/** Get current timezone setting from localStorage */
const currentTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai';
dayjs.tz.setDefault(currentTimeZone);
// 使用最新时区格式化日期
/** Format date with current timezone */
return dayjs.tz(value).format(format);
};

View File

@@ -1,3 +1,22 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:35:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:35:15
*/
/**
* HTTP Request Utility Module
*
* Provides axios-based HTTP client with:
* - Automatic token refresh on 401 errors
* - Request/response interceptors
* - Cookie-based authentication
* - Error handling and user notifications
* - File upload/download support
*
* @module request
*/
import axios from 'axios';
import type { AxiosRequestConfig } from 'axios';
import { clearAuthData } from './auth';
@@ -5,6 +24,9 @@ import { message } from 'antd';
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
import i18n from '@/i18n'
/**
* Standard API response structure
*/
export interface ResponseData {
code: number;
msg: string;
@@ -12,6 +34,10 @@ export interface ResponseData {
error: string;
time: number;
}
/**
* Paginated data structure
*/
interface data {
"items": Record<string, string | number | boolean | object | null | undefined>[];
"page": {
@@ -22,21 +48,22 @@ interface data {
}
}
export const API_PREFIX = '/api'
// 创建axios实例
// Create axios instance
const service = axios.create({
baseURL: API_PREFIX, // vite.config.ts中的代理配置对应
// timeout: 10000, // 请求超时时间
baseURL: API_PREFIX, // Corresponds to proxy config in vite.config.ts
// timeout: 10000, // Request timeout
withCredentials: false,
headers: {
'Content-Type': 'application/json'
},
});
// 是否正在刷新token
// Token refresh state
let isRefreshing = false;
// 存储待重试的请求队列
// Queue for pending requests during token refresh
interface RequestQueueItem {
config: AxiosRequestConfig;
resolve: (token: string) => void;
@@ -44,7 +71,7 @@ interface RequestQueueItem {
}
let requests: RequestQueueItem[] = [];
// 请求拦截器
// Request interceptor
service.interceptors.request.use(
(config) => {
if (!config.headers.Authorization) {
@@ -59,13 +86,16 @@ service.interceptors.request.use(
return config;
},
(error) => {
// 对请求错误做些什么
console.error('请求错误:', error);
// Handle request errors
console.error('Request error:', error);
return Promise.reject(error);
}
);
// 刷新token的函数
/**
* Refresh authentication token
* @returns New access token
*/
const tokenRefresh = async (): Promise<string> => {
try {
const refresh_token = cookieUtils.get('refreshToken');
@@ -75,16 +105,16 @@ const tokenRefresh = async (): Promise<string> => {
if (!refresh_token) {
throw new Error(i18n.t('common.refreshTokenNotExist'));
}
// 使用原生axios调用refresh接口避免触发拦截器导致的循环调用
// Use native axios to call refresh API, avoiding interceptor circular calls
const response: any = await refreshToken();
const newToken = response.access_token;
cookieUtils.set('authToken', newToken);
return newToken;
} catch (error) {
// 如果refresh接口也返回401则退出登录
// If refresh API also returns 401, logout
clearAuthData();
message.warning(i18n.t('common.loginExpired'));
// 这里可以添加重定向到登录页的逻辑
// Redirect to login page
if (!window.location.hash.includes('#/login')) {
window.location.href = `/#/login`;
}
@@ -92,13 +122,13 @@ const tokenRefresh = async (): Promise<string> => {
}
};
// 响应拦截器
// Response interceptor
service.interceptors.response.use(
(response) => {
// 对响应数据做点什么
// Process response data
const { data: responseData } = response;
// 如果响应数据不是对象,直接返回
// If response data is not an object, return directly
if (!responseData || typeof responseData !== 'object') {
return responseData;
}
@@ -110,7 +140,7 @@ service.interceptors.response.use(
case 200:
return data !== undefined ? data : responseData;
case 401:
// 处理未授权情况
// Handle unauthorized
return handle401Error(response.config);
default:
if (code === undefined) {
@@ -123,18 +153,18 @@ service.interceptors.response.use(
}
},
(error) => {
// 如果是取消请求,不显示错误提示
// If request was cancelled, don't show error message
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
return Promise.reject(error);
}
// 处理网络错误、超时等
// Handle network errors, timeouts, etc.
let msg = error.response?.data?.error || error.response?.error;
const status = error?.response ? error.response.status : error;
// 服务器响应了但状态码不在2xx范围
// Server responded but status code is not in 2xx range
switch (status) {
case 401:
// 处理未授权情况
// Handle unauthorized
return handle401Error(error.config);
case 403:
msg = i18n.t('common.permissionDenied');
@@ -165,9 +195,13 @@ service.interceptors.response.use(
}
);
// 处理401错误的函数
/**
* Handle 401 unauthorized errors with token refresh
* @param config - Original request configuration
* @returns Retried request with new token
*/
const handle401Error = async (config: AxiosRequestConfig): Promise<unknown> => {
// 如果是refresh接口本身返回401则直接退出登录
// If refresh API itself returns 401, logout directly
if (config.url === refreshTokenUrl) {
clearAuthData();
message.warning(i18n.t('common.loginExpired'));
@@ -184,39 +218,39 @@ const handle401Error = async (config: AxiosRequestConfig): Promise<unknown> => {
return Promise.reject(new Error(i18n.t('common.publicApiCannotRefreshToken')));
}
// 如果正在刷新token则将当前请求加入队列
// If token refresh is in progress, queue the request
if (isRefreshing) {
return new Promise((resolve, reject) => {
requests.push({ config, resolve, reject });
}).then((token) => {
// 使用新token重新发送请求
// Retry request with new token
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
return service(config);
});
}
// 开始刷新token
// Start token refresh
isRefreshing = true;
try {
const newToken = await tokenRefresh();
// 更新队列中所有请求的token并重新发送
// Update token for all queued requests and resolve them
requests.forEach(({ config, resolve }) => {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${newToken}`;
resolve(newToken);
});
// 清空队列
// Clear queue
requests = [];
// 使用新token重新发送当前请求
// Retry current request with new token
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${newToken}`;
return service(config);
} catch (error) {
// 刷新token失败,清空队列并拒绝所有请求
// Token refresh failed, clear queue and reject all requests
requests.forEach(({ reject }) => {
reject(error as Error);
});
@@ -232,6 +266,12 @@ interface ObjectWithPush {
[key: string]: string | number | boolean | object | null | undefined;
}
/**
* Filter and clean request parameters
* - Removes null/undefined values
* - Trims string values
* - Handles objects with _push flag
*/
function paramFilter(params: Record<string, string | number | boolean | ObjectWithPush | null | undefined> = {}) {
Object.keys(params).forEach(key => {
@@ -255,7 +295,9 @@ function paramFilter(params: Record<string, string | number | boolean | ObjectWi
return params;
}
// 封装请求方法
/**
* HTTP request methods wrapper
*/
export const request = {
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return service.get(url, {
@@ -307,10 +349,13 @@ export const request = {
// 获取父级域名
/**
* Get parent domain for cookie setting
* @returns Parent domain or IP address
*/
const getParentDomain = () => {
const hostname = window.location.hostname
// 检查是否为IP地址
// Check if it's an IP address
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return hostname
}
@@ -318,7 +363,9 @@ const getParentDomain = () => {
return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname
}
// Cookie操作工具
/**
* Cookie utility functions
*/
export const cookieUtils = {
set: (name: string, value: string, domain = getParentDomain()) => {
document.cookie = `${name}=${value}; domain=${domain}; path=/; secure; samesite=strict`

View File

@@ -1,3 +1,21 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:35:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:35:43
*/
/**
* Server-Sent Events (SSE) Stream Utility Module
*
* Provides SSE handling with:
* - Automatic token refresh on 401 errors
* - SSE message parsing and JSON decoding
* - HTML entity decoding
* - Stream buffering for incomplete messages
*
* @module stream
*/
import { message } from 'antd';
import i18n from '@/i18n'
import { cookieUtils } from './request'
@@ -9,7 +27,10 @@ const API_PREFIX = '/api'
let isRefreshing = false;
let refreshPromise: Promise<string> | null = null;
// Refresh token function for SSE
/**
* Refresh authentication token for SSE requests
* @returns New access token
*/
const refreshTokenForSSE = async (): Promise<string> => {
if (isRefreshing && refreshPromise) {
return refreshPromise;
@@ -42,10 +63,19 @@ const refreshTokenForSSE = async (): Promise<string> => {
return refreshPromise;
};
/**
* SSE message structure
*/
export interface SSEMessage {
event?: string
data?: string | object
}
/**
* Parse SSE string format to JSON objects
* @param sseString - Raw SSE string data
* @returns Array of parsed SSE messages
*/
export function parseSSEToJSON(sseString: string) {
const events: SSEMessage[] = []
const lines = sseString.trim().split('\n')
@@ -77,9 +107,14 @@ export function parseSSEToJSON(sseString: string) {
return events
}
/**
* Parse SSE data content with HTML entity decoding
* @param dataContent - Raw data content string
* @returns Parsed object or original string
*/
function parseDataContent(dataContent: string): string | object {
try {
// 第一层解码HTML实体
// First layer: HTML entity decoding
let unescaped = dataContent
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
@@ -87,15 +122,15 @@ function parseDataContent(dataContent: string): string | object {
.replace(/&gt;/g, '>')
.replace(/&#39;/g, "'")
// 解析第一层JSON
// Parse first layer JSON
const firstParse = JSON.parse(unescaped)
// 如果data字段是字符串且包含JSON解析data层但保持chunk为字符串
// If data field is a string containing JSON, parse data layer but keep chunk as string
if (firstParse.data && typeof firstParse.data === 'string' && firstParse.data.includes("{")) {
try {
firstParse.data = JSON.parse(firstParse.data)
} catch {
// 保持原字符串
// Keep original string
}
}
@@ -105,6 +140,14 @@ function parseDataContent(dataContent: string): string | object {
}
}
/**
* Make SSE request with authentication
* @param url - API endpoint
* @param data - Request payload
* @param token - Authentication token
* @param config - Additional request configuration
* @returns Fetch response
*/
const makeSSERequest = async (url: string, data: any, token: string, config = { headers: {} }) => {
return fetch(`${API_PREFIX}${url}`, {
method: 'POST',
@@ -117,6 +160,13 @@ const makeSSERequest = async (url: string, data: any, token: string, config = {
});
};
/**
* Handle SSE stream with automatic token refresh and message parsing
* @param url - API endpoint
* @param data - Request payload
* @param onMessage - Callback for each parsed message
* @param config - Additional request configuration
*/
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => {
try {
let token = cookieUtils.get('authToken');
@@ -153,7 +203,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = ''; // 添加缓冲区来处理不完整的消息
let buffer = ''; // Buffer for handling incomplete messages
while (true) {
const { done, value } = await reader.read();
@@ -162,9 +212,9 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 处理完整的事件
// Process complete events
const events = buffer.split('\n\n');
buffer = events.pop() || ''; // 保留最后一个可能不完整的事件
buffer = events.pop() || ''; // Keep last potentially incomplete event
for (const event of events) {
if (event.trim() && onMessage) {
@@ -173,7 +223,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
}
}
// 处理剩余的缓冲区内容
// Process remaining buffer content
if (buffer.trim() && onMessage) {
onMessage(parseSSEToJSON(buffer) ?? {});
}

View File

@@ -1,3 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:37:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:37:10
*/
/**
* Timezone Configuration Module
*
* Provides:
* - Major world timezone list
* - Timezone to Ant Design locale mapping
* - Dayjs locale imports
*
* Note: Timezone display names are in i18n translation files (zh.ts and en.ts)
* Use i18n.t('timezones.timezone_name') to get localized timezone display names
*
* @module timezones
*/
import en_US from 'antd/locale/en_US';
import en_GB from 'antd/locale/en_GB';
import de_DE from 'antd/locale/de_DE';
@@ -12,157 +32,159 @@ import 'dayjs/locale/de'
import 'dayjs/locale/en-gb'
import 'dayjs/locale/en'
// 全世界主要时区列表
/**
* Major world timezones list
*/
export const timezones = [
'America/Los_Angeles', // 美国洛杉矶
'America/New_York', // 美国纽约
'Europe/London', // 英国伦敦
'Europe/Berlin', // 德国柏林
'Europe/Moscow', // 俄罗斯莫斯科
'Asia/Kolkata', // 印度加尔各答
'Asia/Shanghai', // 中国上海
'America/Los_Angeles', // Los Angeles, USA
'America/New_York', // New York, USA
'Europe/London', // London, UK
'Europe/Berlin', // Berlin, Germany
'Europe/Moscow', // Moscow, Russia
'Asia/Kolkata', // Kolkata, India
'Asia/Shanghai', // Shanghai, China
// 亚洲
// 'Asia/Tokyo', // 日本东京
// 'Asia/Singapore', // 新加坡
// 'Asia/Hong_Kong', // 中国香港
// 'Asia/Taipei', // 中国台北
// 'Asia/Seoul', // 韩国首尔
// 'Asia/Bangkok', // 泰国曼谷
// 'Asia/Jakarta', // 印度尼西亚雅加达
// 'Asia/Manila', // 菲律宾马尼拉
// 'Asia/Dubai', // 阿联酋迪拜
// 'Asia/Tashkent', // 乌兹别克斯坦塔什干
// 'Asia/Riyadh', // 沙特阿拉伯利雅得
// 'Asia/Baku', // 阿塞拜疆巴库
// 'Asia/Istanbul', // 土耳其伊斯坦布尔
// Asia
// 'Asia/Tokyo', // Tokyo, Japan
// 'Asia/Singapore', // Singapore
// 'Asia/Hong_Kong', // Hong Kong, China
// 'Asia/Taipei', // Taipei, Taiwan
// 'Asia/Seoul', // Seoul, South Korea
// 'Asia/Bangkok', // Bangkok, Thailand
// 'Asia/Jakarta', // Jakarta, Indonesia
// 'Asia/Manila', // Manila, Philippines
// 'Asia/Dubai', // Dubai, UAE
// 'Asia/Tashkent', // Tashkent, Uzbekistan
// 'Asia/Riyadh', // Riyadh, Saudi Arabia
// 'Asia/Baku', // Baku, Azerbaijan
// 'Asia/Istanbul', // Istanbul, Turkey
// 欧洲
// 'Europe/Paris', // 法国巴黎
// 'Europe/Rome', // 意大利罗马
// 'Europe/Madrid', // 西班牙马德里
// 'Europe/Amsterdam', // 荷兰阿姆斯特丹
// 'Europe/Vienna', // 奥地利维也纳
// 'Europe/Stockholm', // 瑞典斯德哥尔摩
// 'Europe/Oslo', // 挪威奥斯陆
// 'Europe/Copenhagen', // 丹麦哥本哈根
// 'Europe/Zurich', // 瑞士苏黎世
// 'Europe/Athens', // 希腊雅典
// 'Europe/Warsaw', // 波兰华沙
// 'Europe/Prague', // 捷克布拉格
// 'Europe/Budapest', // 匈牙利布达佩斯
// 'Europe/Belgrade', // 塞尔维亚贝尔格莱德
// Europe
// 'Europe/Paris', // Paris, France
// 'Europe/Rome', // Rome, Italy
// 'Europe/Madrid', // Madrid, Spain
// 'Europe/Amsterdam', // Amsterdam, Netherlands
// 'Europe/Vienna', // Vienna, Austria
// 'Europe/Stockholm', // Stockholm, Sweden
// 'Europe/Oslo', // Oslo, Norway
// 'Europe/Copenhagen', // Copenhagen, Denmark
// 'Europe/Zurich', // Zurich, Switzerland
// 'Europe/Athens', // Athens, Greece
// 'Europe/Warsaw', // Warsaw, Poland
// 'Europe/Prague', // Prague, Czech Republic
// 'Europe/Budapest', // Budapest, Hungary
// 'Europe/Belgrade', // Belgrade, Serbia
// 北美洲
// 'America/Chicago', // 美国芝加哥
// 'America/Denver', // 美国丹佛
// 'America/Toronto', // 加拿大多伦多
// 'America/Vancouver', // 加拿大温哥华
// 'America/Mexico_City', // 墨西哥墨西哥城
// North America
// 'America/Chicago', // Chicago, USA
// 'America/Denver', // Denver, USA
// 'America/Toronto', // Toronto, Canada
// 'America/Vancouver', // Vancouver, Canada
// 'America/Mexico_City', // Mexico City, Mexico
// 南美洲
// 'America/Sao_Paulo', // 巴西圣保罗
// 'America/Buenos_Aires', // 阿根廷布宜诺斯艾利斯
// 'America/Santiago', // 智利圣地亚哥
// 'America/Lima', // 秘鲁利马
// 'America/Bogota', // 哥伦比亚波哥大
// 'America/Caracas', // 委内瑞拉加拉加斯
// South America
// 'America/Sao_Paulo', // São Paulo, Brazil
// 'America/Buenos_Aires', // Buenos Aires, Argentina
// 'America/Santiago', // Santiago, Chile
// 'America/Lima', // Lima, Peru
// 'America/Bogota', // Bogotá, Colombia
// 'America/Caracas', // Caracas, Venezuela
// // 大洋洲
// 'Australia/Sydney', // 澳大利亚悉尼
// 'Australia/Melbourne', // 澳大利亚墨尔本
// 'Australia/Brisbane', // 澳大利亚布里斯班
// 'Australia/Perth', // 澳大利亚珀斯
// 'New_Zealand/Auckland', // 新西兰奥克兰
// Oceania
// 'Australia/Sydney', // Sydney, Australia
// 'Australia/Melbourne', // Melbourne, Australia
// 'Australia/Brisbane', // Brisbane, Australia
// 'Australia/Perth', // Perth, Australia
// 'New_Zealand/Auckland', // Auckland, New Zealand
// // 非洲
// 'Africa/Cairo', // 埃及开罗
// 'Africa/Johannesburg', // 南非约翰内斯堡
// 'Africa/Lagos', // 尼日利亚拉各斯
// 'Africa/Casablanca', // 摩洛哥卡萨布兰卡
// 'Africa/Nairobi', // 肯尼亚内罗毕
// 'Africa/Addis_Ababa', // 埃塞俄比亚亚的斯亚贝巴
// Africa
// 'Africa/Cairo', // Cairo, Egypt
// 'Africa/Johannesburg', // Johannesburg, South Africa
// 'Africa/Lagos', // Lagos, Nigeria
// 'Africa/Casablanca', // Casablanca, Morocco
// 'Africa/Nairobi', // Nairobi, Kenya
// 'Africa/Addis_Ababa', // Addis Ababa, Ethiopia
// // 其他
// 'UTC', // 协调世界时
// Other
// 'UTC', // Coordinated Universal Time
];
// 注意时区显示名称已移至i18n翻译文件中zh.ts和en.ts
// 请使用i18n.t('timezones.时区名称')来获取本地化的时区显示名称
// 时区与antd本地化文件的映射
// 键为时区值为antd本地化文件的名称
/**
* Timezone to Ant Design locale mapping
* Key: timezone identifier
* Value: Ant Design locale object
*/
export const timezoneToAntdLocaleMap: Record<string, Locale> = {
// 亚洲
'Asia/Shanghai': zh_CN, // 中国上海 - 中文(中国大陆)
'Asia/Kolkata': hi_IN, // 印度加尔各答 - 印地语
'Europe/Moscow': ru_RU, // 俄罗斯莫斯科 - 俄语
'Europe/Berlin': de_DE, // 德国柏林 - 德语
'Europe/London': en_GB, // 英国伦敦 - 英语(英国)
'America/New_York': en_US, // 美国纽约 - 英语(美国)
'America/Los_Angeles': en_US, // 美国洛杉矶 - 英语(美国)
// Asia
'Asia/Shanghai': zh_CN, // Shanghai, China - Chinese (Mainland)
'Asia/Kolkata': hi_IN, // Kolkata, India - Hindi
'Europe/Moscow': ru_RU, // Moscow, Russia - Russian
'Europe/Berlin': de_DE, // Berlin, Germany - German
'Europe/London': en_GB, // London, UK - English (UK)
'America/New_York': en_US, // New York, USA - English (US)
'America/Los_Angeles': en_US, // Los Angeles, USA - English (US)
// 'Asia/Tokyo': 'ja_JP', // 日本东京 - 日语
// 'Asia/Singapore': 'en_SG', // 新加坡 - 英语(新加坡)
// 'Asia/Hong_Kong': 'zh_HK', // 中国香港 - 中文(香港)
// 'Asia/Taipei': 'zh_TW', // 中国台北 - 中文(台湾)
// 'Asia/Seoul': 'ko_KR', // 韩国首尔 - 韩语
// 'Asia/Bangkok': 'th_TH', // 泰国曼谷 - 泰语
// 'Asia/Jakarta': 'id_ID', // 印度尼西亚雅加达 - 印尼语
// 'Asia/Manila': 'en_PH', // 菲律宾马尼拉 - 英语(菲律宾)
// 'Asia/Dubai': 'ar_AE', // 阿联酋迪拜 - 阿拉伯语
// 'Asia/Tashkent': 'uz_UZ', // 乌兹别克斯坦塔什干 - 乌兹别克语
// 'Asia/Riyadh': 'ar_SA', // 沙特阿拉伯利雅得 - 阿拉伯语
// 'Asia/Baku': 'az_AZ', // 阿塞拜疆巴库 - 阿塞拜疆语
// 'Asia/Istanbul': 'tr_TR', // 土耳其伊斯坦布尔 - 土耳其语
// 'Asia/Tokyo': 'ja_JP', // Tokyo, Japan - Japanese
// 'Asia/Singapore': 'en_SG', // Singapore - English (Singapore)
// 'Asia/Hong_Kong': 'zh_HK', // Hong Kong, China - Chinese (Hong Kong)
// 'Asia/Taipei': 'zh_TW', // Taipei, Taiwan - Chinese (Taiwan)
// 'Asia/Seoul': 'ko_KR', // Seoul, South Korea - Korean
// 'Asia/Bangkok': 'th_TH', // Bangkok, Thailand - Thai
// 'Asia/Jakarta': 'id_ID', // Jakarta, Indonesia - Indonesian
// 'Asia/Manila': 'en_PH', // Manila, Philippines - English (Philippines)
// 'Asia/Dubai': 'ar_AE', // Dubai, UAE - Arabic
// 'Asia/Tashkent': 'uz_UZ', // Tashkent, Uzbekistan - Uzbek
// 'Asia/Riyadh': 'ar_SA', // Riyadh, Saudi Arabia - Arabic
// 'Asia/Baku': 'az_AZ', // Baku, Azerbaijan - Azerbaijani
// 'Asia/Istanbul': 'tr_TR', // Istanbul, Turkey - Turkish
// // 欧洲
// 'Europe/Paris': 'fr_FR', // 法国巴黎 - 法语
// 'Europe/Rome': 'it_IT', // 意大利罗马 - 意大利语
// 'Europe/Madrid': 'es_ES', // 西班牙马德里 - 西班牙语
// 'Europe/Amsterdam': 'nl_NL', // 荷兰阿姆斯特丹 - 荷兰语
// 'Europe/Vienna': 'de_AT', // 奥地利维也纳 - 德语(奥地利)
// 'Europe/Stockholm': 'sv_SE', // 瑞典斯德哥尔摩 - 瑞典语
// 'Europe/Oslo': 'nb_NO', // 挪威奥斯陆 - 挪威语
// 'Europe/Copenhagen': 'da_DK', // 丹麦哥本哈根 - 丹麦语
// 'Europe/Zurich': 'de_CH', // 瑞士苏黎世 - 德语(瑞士)
// 'Europe/Athens': 'el_GR', // 希腊雅典 - 希腊语
// 'Europe/Warsaw': 'pl_PL', // 波兰华沙 - 波兰语
// 'Europe/Prague': 'cs_CZ', // 捷克布拉格 - 捷克语
// 'Europe/Budapest': 'hu_HU', // 匈牙利布达佩斯 - 匈牙利语
// 'Europe/Belgrade': 'sr_RS', // 塞尔维亚贝尔格莱德 - 塞尔维亚语
// Europe
// 'Europe/Paris': 'fr_FR', // Paris, France - French
// 'Europe/Rome': 'it_IT', // Rome, Italy - Italian
// 'Europe/Madrid': 'es_ES', // Madrid, Spain - Spanish
// 'Europe/Amsterdam': 'nl_NL', // Amsterdam, Netherlands - Dutch
// 'Europe/Vienna': 'de_AT', // Vienna, Austria - German (Austria)
// 'Europe/Stockholm': 'sv_SE', // Stockholm, Sweden - Swedish
// 'Europe/Oslo': 'nb_NO', // Oslo, Norway - Norwegian
// 'Europe/Copenhagen': 'da_DK', // Copenhagen, Denmark - Danish
// 'Europe/Zurich': 'de_CH', // Zurich, Switzerland - German (Switzerland)
// 'Europe/Athens': 'el_GR', // Athens, Greece - Greek
// 'Europe/Warsaw': 'pl_PL', // Warsaw, Poland - Polish
// 'Europe/Prague': 'cs_CZ', // Prague, Czech Republic - Czech
// 'Europe/Budapest': 'hu_HU', // Budapest, Hungary - Hungarian
// 'Europe/Belgrade': 'sr_RS', // Belgrade, Serbia - Serbian
// // 北美洲
// 'America/Chicago': 'en_US', // 美国芝加哥 - 英语(美国)
// 'America/Denver': 'en_US', // 美国丹佛 - 英语(美国)
// 'America/Toronto': 'en_CA', // 加拿大多伦多 - 英语(加拿大)
// 'America/Vancouver': 'en_CA', // 加拿大温哥华 - 英语(加拿大)
// 'America/Mexico_City': 'es_MX', // 墨西哥墨西哥城 - 西班牙语(墨西哥)
// North America
// 'America/Chicago': 'en_US', // Chicago, USA - English (US)
// 'America/Denver': 'en_US', // Denver, USA - English (US)
// 'America/Toronto': 'en_CA', // Toronto, Canada - English (Canada)
// 'America/Vancouver': 'en_CA', // Vancouver, Canada - English (Canada)
// 'America/Mexico_City': 'es_MX', // Mexico City, Mexico - Spanish (Mexico)
// // 南美洲
// 'America/Sao_Paulo': 'pt_BR', // 巴西圣保罗 - 葡萄牙语(巴西)
// 'America/Buenos_Aires': 'es_AR', // 阿根廷布宜诺斯艾利斯 - 西班牙语(阿根廷)
// 'America/Santiago': 'es_CL', // 智利圣地亚哥 - 西班牙语(智利)
// 'America/Lima': 'es_PE', // 秘鲁利马 - 西班牙语(秘鲁)
// 'America/Bogota': 'es_CO', // 哥伦比亚波哥大 - 西班牙语(哥伦比亚)
// 'America/Caracas': 'es_VE', // 委内瑞拉加拉加斯 - 西班牙语(委内瑞拉)
// South America
// 'America/Sao_Paulo': 'pt_BR', // São Paulo, Brazil - Portuguese (Brazil)
// 'America/Buenos_Aires': 'es_AR', // Buenos Aires, Argentina - Spanish (Argentina)
// 'America/Santiago': 'es_CL', // Santiago, Chile - Spanish (Chile)
// 'America/Lima': 'es_PE', // Lima, Peru - Spanish (Peru)
// 'America/Bogota': 'es_CO', // Bogotá, Colombia - Spanish (Colombia)
// 'America/Caracas': 'es_VE', // Caracas, Venezuela - Spanish (Venezuela)
// // 大洋洲
// 'Australia/Sydney': 'en_AU', // 澳大利亚悉尼 - 英语(澳大利亚)
// 'Australia/Melbourne': 'en_AU', // 澳大利亚墨尔本 - 英语(澳大利亚)
// 'Australia/Brisbane': 'en_AU', // 澳大利亚布里斯班 - 英语(澳大利亚)
// 'Australia/Perth': 'en_AU', // 澳大利亚珀斯 - 英语(澳大利亚)
// 'New_Zealand/Auckland': 'en_NZ', // 新西兰奥克兰 - 英语(新西兰)
// Oceania
// 'Australia/Sydney': 'en_AU', // Sydney, Australia - English (Australia)
// 'Australia/Melbourne': 'en_AU', // Melbourne, Australia - English (Australia)
// 'Australia/Brisbane': 'en_AU', // Brisbane, Australia - English (Australia)
// 'Australia/Perth': 'en_AU', // Perth, Australia - English (Australia)
// 'New_Zealand/Auckland': 'en_NZ', // Auckland, New Zealand - English (New Zealand)
// // 非洲
// 'Africa/Cairo': 'ar_EG', // 埃及开罗 - 阿拉伯语(埃及)
// 'Africa/Johannesburg': 'en_ZA', // 南非约翰内斯堡 - 英语(南非)
// 'Africa/Lagos': 'en_NG', // 尼日利亚拉各斯 - 英语(尼日利亚)
// 'Africa/Casablanca': 'fr_MA', // 摩洛哥卡萨布兰卡 - 法语(摩洛哥)
// 'Africa/Nairobi': 'en_KE', // 肯尼亚内罗毕 - 英语(肯尼亚)
// 'Africa/Addis_Ababa': 'am_ET', // 埃塞俄比亚亚的斯亚贝巴 - 阿姆哈拉语
// Africa
// 'Africa/Cairo': 'ar_EG', // Cairo, Egypt - Arabic (Egypt)
// 'Africa/Johannesburg': 'en_ZA', // Johannesburg, South Africa - English (South Africa)
// 'Africa/Lagos': 'en_NG', // Lagos, Nigeria - English (Nigeria)
// 'Africa/Casablanca': 'fr_MA', // Casablanca, Morocco - French (Morocco)
// 'Africa/Nairobi': 'en_KE', // Nairobi, Kenya - English (Kenya)
// 'Africa/Addis_Ababa': 'am_ET', // Addis Ababa, Ethiopia - Amharic
// // 其他
// 'UTC': 'en_US', // 协调世界时 - 默认英语(美国)
// Other
// 'UTC': 'en_US', // Coordinated Universal Time - Default English (US)
};

View File

@@ -1,6 +1,24 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 16:35:32
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 16:35:32
*/
/**
* YAML Export Utility
*
* Provides functions to export data as YAML files.
*
* @module yamlExport
*/
import yaml from 'js-yaml';
/**
* Export data to YAML file
* @param data - Data to export
* @param filename - Output filename (default: 'export.yaml')
*/
export const exportToYaml = (data: unknown, filename: string = 'export.yaml') => {
const yamlStr = yaml.dump(data);
const blob = new Blob([yamlStr], { type: 'text/yaml' });