docs: add comments to the src/routes & src/store & src/utils directory
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
@@ -87,15 +122,15 @@ function parseDataContent(dataContent: string): string | object {
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/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) ?? {});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user