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 { lazy, type LazyExoticComponent, type ComponentType, type ReactNode } from 'react';
|
||||||
import { createHashRouter, createRoutesFromElements, Route } from 'react-router-dom';
|
import { createHashRouter, createRoutesFromElements, Route } from 'react-router-dom';
|
||||||
|
|
||||||
// 导入路由配置JSON
|
/** Import route configuration JSON */
|
||||||
import routesConfig from './routes.json';
|
import routesConfig from './routes.json';
|
||||||
import Ontology from '@/views/Ontology';
|
|
||||||
|
|
||||||
|
|
||||||
// 递归函数,用于生成路由元素
|
/** Recursively collect all element names from routes */
|
||||||
|
|
||||||
// 递归收集所有路由中的element
|
|
||||||
function collectElements(routes: RouteConfig[]): Set<string> {
|
function collectElements(routes: RouteConfig[]): Set<string> {
|
||||||
const elements = new Set<string>();
|
const elements = new Set<string>();
|
||||||
|
|
||||||
function traverse(routeList: RouteConfig[]) {
|
function traverse(routeList: RouteConfig[]) {
|
||||||
routeList.forEach(route => {
|
routeList.forEach(route => {
|
||||||
// 添加当前路由的element
|
/** Add current route's element */
|
||||||
elements.add(route.element);
|
elements.add(route.element);
|
||||||
|
|
||||||
// 递归处理子路由
|
/** Recursively process child routes */
|
||||||
if (route.children && route.children.length > 0) {
|
if (route.children && route.children.length > 0) {
|
||||||
traverse(route.children);
|
traverse(route.children);
|
||||||
}
|
}
|
||||||
@@ -28,15 +44,15 @@ function collectElements(routes: RouteConfig[]): Set<string> {
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接定义组件映射表,避免动态路径解析问题
|
/** Component mapping table - maps element names to lazy-loaded components */
|
||||||
const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> = {
|
const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> = {
|
||||||
// 布局组件
|
/** Layout components */
|
||||||
AuthLayout: lazy(() => import('@/components/Layout/AuthLayout')),
|
AuthLayout: lazy(() => import('@/components/Layout/AuthLayout')),
|
||||||
AuthSpaceLayout: lazy(() => import('@/components/Layout/AuthSpaceLayout')),
|
AuthSpaceLayout: lazy(() => import('@/components/Layout/AuthSpaceLayout')),
|
||||||
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
|
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
|
||||||
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
|
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
|
||||||
NoAuthLayout: lazy(() => import('@/components/Layout/NoAuthLayout')),
|
NoAuthLayout: lazy(() => import('@/components/Layout/NoAuthLayout')),
|
||||||
// 视图组件
|
/** View components */
|
||||||
Index: lazy(() => import('@/views/Index')),
|
Index: lazy(() => import('@/views/Index')),
|
||||||
Home: lazy(() => import('@/views/Home')),
|
Home: lazy(() => import('@/views/Home')),
|
||||||
UserMemory: lazy(() => import('@/views/UserMemory')),
|
UserMemory: lazy(() => import('@/views/UserMemory')),
|
||||||
@@ -78,7 +94,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
|||||||
NotFound: lazy(() => import('@/views/NotFound'))
|
NotFound: lazy(() => import('@/views/NotFound'))
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查并报告缺失的组件
|
/** Check and report missing components */
|
||||||
const allElements = collectElements(routesConfig);
|
const allElements = collectElements(routesConfig);
|
||||||
allElements.forEach(elementName => {
|
allElements.forEach(elementName => {
|
||||||
if (!componentMap[elementName]) {
|
if (!componentMap[elementName]) {
|
||||||
@@ -86,23 +102,27 @@ allElements.forEach(elementName => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确保NotFound组件总是存在作为兜底
|
/** Ensure NotFound component always exists as fallback */
|
||||||
if (!componentMap['NotFound']) {
|
if (!componentMap['NotFound']) {
|
||||||
componentMap['NotFound'] = lazy(() => import('@/views/NotFound/index.tsx'));
|
componentMap['NotFound'] = lazy(() => import('@/views/NotFound/index.tsx'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路由配置类型定义
|
/** Route configuration type definition */
|
||||||
interface RouteConfig {
|
interface RouteConfig {
|
||||||
|
/** Route path */
|
||||||
path?: string;
|
path?: string;
|
||||||
|
/** Component element name */
|
||||||
element: string;
|
element: string;
|
||||||
|
/** Component file path (optional) */
|
||||||
componentPath?: string;
|
componentPath?: string;
|
||||||
|
/** Child routes */
|
||||||
children?: RouteConfig[];
|
children?: RouteConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归函数,用于生成路由元素
|
/** Recursively generate route elements from configuration */
|
||||||
const generateRoutes = (routes: RouteConfig[]): ReactNode => {
|
const generateRoutes = (routes: RouteConfig[]): ReactNode => {
|
||||||
return routes.map((route, index) => {
|
return routes.map((route, index) => {
|
||||||
// 获取组件
|
/** Get component from mapping */
|
||||||
const componentKey = route.element as keyof typeof componentMap;
|
const componentKey = route.element as keyof typeof componentMap;
|
||||||
const Component = componentMap[componentKey];
|
const Component = componentMap[componentKey];
|
||||||
|
|
||||||
@@ -111,7 +131,7 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有子路由
|
/** If has child routes, create nested route */
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
return (
|
return (
|
||||||
<Route key={index} element={<Component />}>
|
<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) {
|
if (route.path) {
|
||||||
return <Route key={index} path={route.path} element={<Component />} />;
|
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(
|
const router = createHashRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
generateRoutes(routesConfig)
|
generateRoutes(routesConfig)
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* @Description:
|
* @Author: ZhaoYing
|
||||||
* @Version: 0.0.1
|
* @Date: 2026-02-02 16:33:22
|
||||||
* @Author: yujiangping
|
* @Last Modified by: ZhaoYing
|
||||||
* @Date: 2026-01-05 17:22:23
|
* @Last Modified time: 2026-02-02 16:33:22
|
||||||
* @LastEditors: yujiangping
|
|
||||||
* @LastEditTime: 2026-01-15 21:02:43
|
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 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 { create } from 'zustand'
|
||||||
import enUS from 'antd/locale/en_US';
|
import enUS from 'antd/locale/en_US';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
@@ -14,13 +25,12 @@ import dayjs from 'dayjs'
|
|||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import { timezoneToAntdLocaleMap } from '@/utils/timezones';
|
|
||||||
|
|
||||||
// 扩展dayjs插件
|
/** Extend dayjs with timezone plugins */
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
// 自定义中文 locale,修改 Tour 组件的按钮文字
|
/** Custom Chinese locale with modified Tour component button text */
|
||||||
const customZhCN: Locale = {
|
const customZhCN: Locale = {
|
||||||
...zhCN,
|
...zhCN,
|
||||||
Tour: {
|
Tour: {
|
||||||
@@ -31,7 +41,7 @@ const customZhCN: Locale = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义英文 locale,修改 Tour 组件的按钮文字
|
/** Custom English locale with modified Tour component button text */
|
||||||
const customEnUS: Locale = {
|
const customEnUS: Locale = {
|
||||||
...enUS,
|
...enUS,
|
||||||
Tour: {
|
Tour: {
|
||||||
@@ -43,19 +53,27 @@ const customEnUS: Locale = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** Internationalization state interface */
|
||||||
interface I18nState {
|
interface I18nState {
|
||||||
|
/** Current language code */
|
||||||
language: string;
|
language: string;
|
||||||
|
/** Ant Design locale object */
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
|
/** Current timezone */
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
|
/** Change application language */
|
||||||
changeLanguage: (language: string) => void;
|
changeLanguage: (language: string) => void;
|
||||||
|
/** Change timezone (triggers page reload) */
|
||||||
changeTimeZone: (timeZone: string) => void;
|
changeTimeZone: (timeZone: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initialize from localStorage or use defaults */
|
||||||
const initialTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai'
|
const initialTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai'
|
||||||
const initialLanguage = localStorage.getItem('language') || 'en'
|
const initialLanguage = localStorage.getItem('language') || 'en'
|
||||||
const initialLocale = initialLanguage === 'en' ? customEnUS : customZhCN
|
const initialLocale = initialLanguage === 'en' ? customEnUS : customZhCN
|
||||||
i18n.changeLanguage(initialLanguage)
|
i18n.changeLanguage(initialLanguage)
|
||||||
|
|
||||||
|
/** Internationalization store */
|
||||||
export const useI18n = create<I18nState>((set, get) => ({
|
export const useI18n = create<I18nState>((set, get) => ({
|
||||||
language: initialLanguage,
|
language: initialLanguage,
|
||||||
locale: initialLocale,
|
locale: initialLocale,
|
||||||
@@ -68,6 +86,7 @@ export const useI18n = create<I18nState>((set, get) => ({
|
|||||||
changeTimeZone: (timeZone: string) => {
|
changeTimeZone: (timeZone: string) => {
|
||||||
const { timeZone: lastTimeZone } = get()
|
const { timeZone: lastTimeZone } = get()
|
||||||
set({ timeZone })
|
set({ timeZone })
|
||||||
|
/** Reload page if timezone changed */
|
||||||
if (lastTimeZone !== timeZone) {
|
if (lastTimeZone !== timeZone) {
|
||||||
window.location.reload()
|
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 { create } from 'zustand'
|
||||||
import AllMenus from './menu.json'
|
import AllMenus from './menu.json'
|
||||||
|
|
||||||
|
/** Menu item interface */
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
id: number;
|
id: number;
|
||||||
parent: number;
|
parent: number;
|
||||||
@@ -22,20 +42,32 @@ export interface MenuItem {
|
|||||||
master?: string | null;
|
master?: string | null;
|
||||||
disposable?: boolean;
|
disposable?: boolean;
|
||||||
appSystem?: string | null;
|
appSystem?: string | null;
|
||||||
subs: MenuItem[] | null;
|
subs?: MenuItem[] | null;
|
||||||
onClick?: (e?: React.MouseEvent) => void | boolean;
|
onClick?: (e?: React.MouseEvent) => void | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Menu state interface */
|
||||||
interface MenuState {
|
interface MenuState {
|
||||||
|
/** Sidebar collapsed state */
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
/** Toggle sidebar collapse */
|
||||||
toggleSider: () => void;
|
toggleSider: () => void;
|
||||||
|
/** All menus by context */
|
||||||
allMenus: Record<'space' | 'manage', MenuItem[]>;
|
allMenus: Record<'space' | 'manage', MenuItem[]>;
|
||||||
|
/** All breadcrumbs by context */
|
||||||
allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>;
|
allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>;
|
||||||
|
/** Load menus for specific context */
|
||||||
loadMenus: (source: 'space' | 'manage') => void;
|
loadMenus: (source: 'space' | 'manage') => void;
|
||||||
|
/** Update breadcrumbs based on key path */
|
||||||
updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void;
|
updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void;
|
||||||
|
/** Set custom breadcrumbs */
|
||||||
setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void;
|
setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initialize breadcrumbs from localStorage */
|
||||||
const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]'
|
const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]'
|
||||||
|
|
||||||
|
/** Menu store */
|
||||||
export const useMenu = create<MenuState>((set, get) => ({
|
export const useMenu = create<MenuState>((set, get) => ({
|
||||||
collapsed: localStorage.getItem('collapsed') === 'true',
|
collapsed: localStorage.getItem('collapsed') === 'true',
|
||||||
allMenus: {
|
allMenus: {
|
||||||
@@ -61,7 +93,7 @@ export const useMenu = create<MenuState>((set, get) => ({
|
|||||||
console.log('updateBreadcrumbs paths:', paths);
|
console.log('updateBreadcrumbs paths:', paths);
|
||||||
|
|
||||||
if (paths.length === 3) {
|
if (paths.length === 3) {
|
||||||
// 三级菜单:[subSubPath, subId, menuId]
|
/** Three-level menu: [subSubPath, subId, menuId] */
|
||||||
const menuId = paths[2];
|
const menuId = paths[2];
|
||||||
const subId = paths[1];
|
const subId = paths[1];
|
||||||
const subSubPath = paths[0];
|
const subSubPath = paths[0];
|
||||||
@@ -81,7 +113,7 @@ export const useMenu = create<MenuState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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]);
|
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
|
||||||
|
|
||||||
if (matchedMenu) {
|
if (matchedMenu) {
|
||||||
|
|||||||
@@ -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 { create } from 'zustand'
|
||||||
import { clearAuthData } from '@/utils/auth';
|
import { clearAuthData } from '@/utils/auth';
|
||||||
import type { User } from '@/views/UserManagement/types'
|
import type { User } from '@/views/UserManagement/types'
|
||||||
@@ -5,6 +24,7 @@ import { getUsers, refreshToken, logout } from '@/api/user'
|
|||||||
import { getWorkspaceStorageType } from '@/api/workspaces';
|
import { getWorkspaceStorageType } from '@/api/workspaces';
|
||||||
import { cookieUtils } from '@/utils/request'
|
import { cookieUtils } from '@/utils/request'
|
||||||
|
|
||||||
|
/** Login information interface */
|
||||||
export interface LoginInfo {
|
export interface LoginInfo {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
@@ -12,24 +32,37 @@ export interface LoginInfo {
|
|||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
token_type: 'bearer'
|
token_type: 'bearer'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** User state interface */
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
|
/** Current user information */
|
||||||
user: User;
|
user: User;
|
||||||
|
/** Login token information */
|
||||||
loginInfo: LoginInfo;
|
loginInfo: LoginInfo;
|
||||||
|
/** Workspace storage type */
|
||||||
storageType: string | null;
|
storageType: string | null;
|
||||||
|
/** Update login information */
|
||||||
updateLoginInfo: (values: LoginInfo) => void;
|
updateLoginInfo: (values: LoginInfo) => void;
|
||||||
|
/** Get user information */
|
||||||
getUserInfo: (flag?: boolean) => void;
|
getUserInfo: (flag?: boolean) => void;
|
||||||
|
/** Clear user information */
|
||||||
clearUserInfo: () => void;
|
clearUserInfo: () => void;
|
||||||
|
/** Logout user */
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
/** Get workspace storage type */
|
||||||
getStorageType: () => void;
|
getStorageType: () => void;
|
||||||
|
/** Check and redirect if workspace not set */
|
||||||
checkJump: () => void;
|
checkJump: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pages that don't require workspace */
|
||||||
export const whitePage = [
|
export const whitePage = [
|
||||||
'/conversation',
|
'/conversation',
|
||||||
'/login',
|
'/login',
|
||||||
'/invite-register'
|
'/invite-register'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** User store */
|
||||||
export const useUser = create<UserState>((set, get) => ({
|
export const useUser = create<UserState>((set, get) => ({
|
||||||
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User,
|
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User,
|
||||||
loginInfo: {} as LoginInfo,
|
loginInfo: {} as LoginInfo,
|
||||||
@@ -101,6 +134,7 @@ export const useUser = create<UserState>((set, get) => ({
|
|||||||
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
|
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
|
||||||
const hash = window.location.hash;
|
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))) {
|
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)))
|
console.log('whitePage', whitePage.find(vo => hash.includes(vo)))
|
||||||
window.location.href = '/#/index'
|
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 = {
|
const API_KEY_PATTERNS = {
|
||||||
service: /sk-service-[A-Za-z0-9_-]+/g,
|
service: /sk-service-[A-Za-z0-9_-]+/g,
|
||||||
agent: /sk-agent-[A-Za-z0-9_-]+/g,
|
agent: /sk-agent-[A-Za-z0-9_-]+/g,
|
||||||
multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g,
|
multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g,
|
||||||
workflow: /sk-workflow-[A-Za-z0-9_-]+/g
|
workflow: /sk-workflow-[A-Za-z0-9_-]+/g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** API key prefix definitions */
|
||||||
const API_KEY_PREFIX = {
|
const API_KEY_PREFIX = {
|
||||||
service: 'sk-service-',
|
service: 'sk-service-',
|
||||||
agent: 'sk-agent-',
|
agent: 'sk-agent-',
|
||||||
@@ -16,9 +30,9 @@ const API_KEY_PREFIX = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 替换文本中的API密钥为*号
|
* Replace API keys in text with asterisks
|
||||||
* @param text 原始文本
|
* @param text - Original text
|
||||||
* @returns 替换后的文本
|
* @returns Text with masked API keys
|
||||||
*/
|
*/
|
||||||
export const maskApiKeys = (text: string): string => {
|
export const maskApiKeys = (text: string): string => {
|
||||||
if (!text) return text
|
if (!text) return text
|
||||||
@@ -37,9 +51,9 @@ export const maskApiKeys = (text: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测文本中是否包含API密钥
|
* Detect if text contains API keys
|
||||||
* @param text 待检测文本
|
* @param text - Text to check
|
||||||
* @returns 是否包含API密钥
|
* @returns Whether text contains API keys
|
||||||
*/
|
*/
|
||||||
export const hasApiKeys = (text: string): boolean => {
|
export const hasApiKeys = (text: string): boolean => {
|
||||||
return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text))
|
return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text))
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* @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'
|
import { cookieUtils } from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all authentication data and cookies
|
||||||
|
* Removes user info, breadcrumbs, and all cookies
|
||||||
|
*/
|
||||||
export const clearAuthData = () => {
|
export const clearAuthData = () => {
|
||||||
console.log("Clearing auth data and redirecting to login");
|
console.log("Clearing auth data and redirecting to login");
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
|
|||||||
@@ -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) => {
|
export const randomString = (length: number = 12, isHasSpecialChars: boolean = true) => {
|
||||||
// 定义字符集:大写字母、小写字母、数字和特殊字符
|
/** Define character sets: uppercase, lowercase, numbers, and special characters */
|
||||||
const uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
const uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
const lowercaseChars = 'abcdefghijklmnopqrstuvwxyz';
|
const lowercaseChars = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
const numberChars = '0123456789';
|
const numberChars = '0123456789';
|
||||||
const specialChars = '!@#$%^&*_+-=|;:,.?';
|
const specialChars = '!@#$%^&*_+-=|;:,.?';
|
||||||
|
|
||||||
// 合并所有字符集
|
/** Combine all character sets */
|
||||||
let allChars = uppercaseChars + lowercaseChars + numberChars;
|
let allChars = uppercaseChars + lowercaseChars + numberChars;
|
||||||
|
|
||||||
// 确保至少包含每种类型的字符
|
/** Ensure at least one character of each type */
|
||||||
let str =
|
let str =
|
||||||
uppercaseChars[Math.floor(Math.random() * uppercaseChars.length)] +
|
uppercaseChars[Math.floor(Math.random() * uppercaseChars.length)] +
|
||||||
lowercaseChars[Math.floor(Math.random() * lowercaseChars.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)];
|
str+= specialChars[Math.floor(Math.random() * specialChars.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填充剩余的字符,使总长度为12
|
/** Fill remaining characters to reach desired length */
|
||||||
for (let i = 4; i < length; i++) {
|
for (let i = 4; i < length; i++) {
|
||||||
str += allChars[Math.floor(Math.random() * allChars.length)];
|
str += allChars[Math.floor(Math.random() * allChars.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打乱密码字符顺序
|
/** Shuffle the string characters */
|
||||||
return str.split('').sort(() => Math.random() - 0.5).join('');
|
return str.split('').sort(() => Math.random() - 0.5).join('');
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,32 +1,46 @@
|
|||||||
/**
|
/*
|
||||||
* 格式化日期时间
|
* @Author: ZhaoYing
|
||||||
* @param value 时间戳(毫秒)或日期字符串
|
* @Date: 2026-02-02 16:34:43
|
||||||
* @param format 目标格式,支持 YYYY-MM-DD HH:mm:ss、YYYY/MM/DD HH:mm:ss、HH:mm 等
|
* @Last Modified by: ZhaoYing
|
||||||
* @returns 格式化后的日期时间字符串
|
* @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 dayjs from 'dayjs';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
|
||||||
// 扩展dayjs插件
|
/** Extend dayjs with timezone plugins */
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
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 = (
|
export const formatDateTime = (
|
||||||
value: string | number | null | undefined,
|
value: string | number | null | undefined,
|
||||||
format: string = 'YYYY-MM-DD HH:mm:ss'
|
format: string = 'YYYY-MM-DD HH:mm:ss'
|
||||||
): string => {
|
): string => {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
|
||||||
// 检查日期是否有效
|
/** Check if date is valid */
|
||||||
if (!dayjs(value).isValid()) {
|
if (!dayjs(value).isValid()) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每次调用都获取最新的时区设置
|
/** Get current timezone setting from localStorage */
|
||||||
const currentTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai';
|
const currentTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai';
|
||||||
dayjs.tz.setDefault(currentTimeZone);
|
dayjs.tz.setDefault(currentTimeZone);
|
||||||
|
|
||||||
// 使用最新时区格式化日期
|
/** Format date with current timezone */
|
||||||
return dayjs.tz(value).format(format);
|
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 axios from 'axios';
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
import { clearAuthData } from './auth';
|
import { clearAuthData } from './auth';
|
||||||
@@ -5,6 +24,9 @@ import { message } from 'antd';
|
|||||||
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
|
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard API response structure
|
||||||
|
*/
|
||||||
export interface ResponseData {
|
export interface ResponseData {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
@@ -12,6 +34,10 @@ export interface ResponseData {
|
|||||||
error: string;
|
error: string;
|
||||||
time: number;
|
time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated data structure
|
||||||
|
*/
|
||||||
interface data {
|
interface data {
|
||||||
"items": Record<string, string | number | boolean | object | null | undefined>[];
|
"items": Record<string, string | number | boolean | object | null | undefined>[];
|
||||||
"page": {
|
"page": {
|
||||||
@@ -22,21 +48,22 @@ interface data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const API_PREFIX = '/api'
|
export const API_PREFIX = '/api'
|
||||||
// 创建axios实例
|
|
||||||
|
// Create axios instance
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: API_PREFIX, // 与vite.config.ts中的代理配置对应
|
baseURL: API_PREFIX, // Corresponds to proxy config in vite.config.ts
|
||||||
// timeout: 10000, // 请求超时时间
|
// timeout: 10000, // Request timeout
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 是否正在刷新token
|
// Token refresh state
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
// 存储待重试的请求队列
|
|
||||||
|
// Queue for pending requests during token refresh
|
||||||
interface RequestQueueItem {
|
interface RequestQueueItem {
|
||||||
config: AxiosRequestConfig;
|
config: AxiosRequestConfig;
|
||||||
resolve: (token: string) => void;
|
resolve: (token: string) => void;
|
||||||
@@ -44,7 +71,7 @@ interface RequestQueueItem {
|
|||||||
}
|
}
|
||||||
let requests: RequestQueueItem[] = [];
|
let requests: RequestQueueItem[] = [];
|
||||||
|
|
||||||
// 请求拦截器
|
// Request interceptor
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
if (!config.headers.Authorization) {
|
if (!config.headers.Authorization) {
|
||||||
@@ -59,13 +86,16 @@ service.interceptors.request.use(
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 对请求错误做些什么
|
// Handle request errors
|
||||||
console.error('请求错误:', error);
|
console.error('Request error:', error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 刷新token的函数
|
/**
|
||||||
|
* Refresh authentication token
|
||||||
|
* @returns New access token
|
||||||
|
*/
|
||||||
const tokenRefresh = async (): Promise<string> => {
|
const tokenRefresh = async (): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const refresh_token = cookieUtils.get('refreshToken');
|
const refresh_token = cookieUtils.get('refreshToken');
|
||||||
@@ -75,16 +105,16 @@ const tokenRefresh = async (): Promise<string> => {
|
|||||||
if (!refresh_token) {
|
if (!refresh_token) {
|
||||||
throw new Error(i18n.t('common.refreshTokenNotExist'));
|
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 response: any = await refreshToken();
|
||||||
const newToken = response.access_token;
|
const newToken = response.access_token;
|
||||||
cookieUtils.set('authToken', newToken);
|
cookieUtils.set('authToken', newToken);
|
||||||
return newToken;
|
return newToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果refresh接口也返回401,则退出登录
|
// If refresh API also returns 401, logout
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
message.warning(i18n.t('common.loginExpired'));
|
message.warning(i18n.t('common.loginExpired'));
|
||||||
// 这里可以添加重定向到登录页的逻辑
|
// Redirect to login page
|
||||||
if (!window.location.hash.includes('#/login')) {
|
if (!window.location.hash.includes('#/login')) {
|
||||||
window.location.href = `/#/login`;
|
window.location.href = `/#/login`;
|
||||||
}
|
}
|
||||||
@@ -92,13 +122,13 @@ const tokenRefresh = async (): Promise<string> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 响应拦截器
|
// Response interceptor
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
// 对响应数据做点什么
|
// Process response data
|
||||||
const { data: responseData } = response;
|
const { data: responseData } = response;
|
||||||
|
|
||||||
// 如果响应数据不是对象,直接返回
|
// If response data is not an object, return directly
|
||||||
if (!responseData || typeof responseData !== 'object') {
|
if (!responseData || typeof responseData !== 'object') {
|
||||||
return responseData;
|
return responseData;
|
||||||
}
|
}
|
||||||
@@ -110,7 +140,7 @@ service.interceptors.response.use(
|
|||||||
case 200:
|
case 200:
|
||||||
return data !== undefined ? data : responseData;
|
return data !== undefined ? data : responseData;
|
||||||
case 401:
|
case 401:
|
||||||
// 处理未授权情况
|
// Handle unauthorized
|
||||||
return handle401Error(response.config);
|
return handle401Error(response.config);
|
||||||
default:
|
default:
|
||||||
if (code === undefined) {
|
if (code === undefined) {
|
||||||
@@ -123,18 +153,18 @@ service.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 如果是取消请求,不显示错误提示
|
// If request was cancelled, don't show error message
|
||||||
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理网络错误、超时等
|
// Handle network errors, timeouts, etc.
|
||||||
let msg = error.response?.data?.error || error.response?.error;
|
let msg = error.response?.data?.error || error.response?.error;
|
||||||
const status = error?.response ? error.response.status : error;
|
const status = error?.response ? error.response.status : error;
|
||||||
// 服务器响应了但状态码不在2xx范围
|
// Server responded but status code is not in 2xx range
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
// 处理未授权情况
|
// Handle unauthorized
|
||||||
return handle401Error(error.config);
|
return handle401Error(error.config);
|
||||||
case 403:
|
case 403:
|
||||||
msg = i18n.t('common.permissionDenied');
|
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> => {
|
const handle401Error = async (config: AxiosRequestConfig): Promise<unknown> => {
|
||||||
// 如果是refresh接口本身返回401,则直接退出登录
|
// If refresh API itself returns 401, logout directly
|
||||||
if (config.url === refreshTokenUrl) {
|
if (config.url === refreshTokenUrl) {
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
message.warning(i18n.t('common.loginExpired'));
|
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')));
|
return Promise.reject(new Error(i18n.t('common.publicApiCannotRefreshToken')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果正在刷新token,则将当前请求加入队列
|
// If token refresh is in progress, queue the request
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
requests.push({ config, resolve, reject });
|
requests.push({ config, resolve, reject });
|
||||||
}).then((token) => {
|
}).then((token) => {
|
||||||
// 使用新token重新发送请求
|
// Retry request with new token
|
||||||
config.headers = config.headers || {};
|
config.headers = config.headers || {};
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
return service(config);
|
return service(config);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始刷新token
|
// Start token refresh
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
try {
|
try {
|
||||||
const newToken = await tokenRefresh();
|
const newToken = await tokenRefresh();
|
||||||
|
|
||||||
// 更新队列中所有请求的token并重新发送
|
// Update token for all queued requests and resolve them
|
||||||
requests.forEach(({ config, resolve }) => {
|
requests.forEach(({ config, resolve }) => {
|
||||||
config.headers = config.headers || {};
|
config.headers = config.headers || {};
|
||||||
config.headers.Authorization = `Bearer ${newToken}`;
|
config.headers.Authorization = `Bearer ${newToken}`;
|
||||||
resolve(newToken);
|
resolve(newToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空队列
|
// Clear queue
|
||||||
requests = [];
|
requests = [];
|
||||||
|
|
||||||
// 使用新token重新发送当前请求
|
// Retry current request with new token
|
||||||
config.headers = config.headers || {};
|
config.headers = config.headers || {};
|
||||||
config.headers.Authorization = `Bearer ${newToken}`;
|
config.headers.Authorization = `Bearer ${newToken}`;
|
||||||
return service(config);
|
return service(config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 刷新token失败,清空队列并拒绝所有请求
|
// Token refresh failed, clear queue and reject all requests
|
||||||
requests.forEach(({ reject }) => {
|
requests.forEach(({ reject }) => {
|
||||||
reject(error as Error);
|
reject(error as Error);
|
||||||
});
|
});
|
||||||
@@ -232,6 +266,12 @@ interface ObjectWithPush {
|
|||||||
[key: string]: string | number | boolean | object | null | undefined;
|
[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> = {}) {
|
function paramFilter(params: Record<string, string | number | boolean | ObjectWithPush | null | undefined> = {}) {
|
||||||
|
|
||||||
Object.keys(params).forEach(key => {
|
Object.keys(params).forEach(key => {
|
||||||
@@ -255,7 +295,9 @@ function paramFilter(params: Record<string, string | number | boolean | ObjectWi
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封装请求方法
|
/**
|
||||||
|
* HTTP request methods wrapper
|
||||||
|
*/
|
||||||
export const request = {
|
export const request = {
|
||||||
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||||
return service.get(url, {
|
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 getParentDomain = () => {
|
||||||
const hostname = window.location.hostname
|
const hostname = window.location.hostname
|
||||||
// 检查是否为IP地址
|
// Check if it's an IP address
|
||||||
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
||||||
return hostname
|
return hostname
|
||||||
}
|
}
|
||||||
@@ -318,7 +363,9 @@ const getParentDomain = () => {
|
|||||||
return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname
|
return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie操作工具
|
/**
|
||||||
|
* Cookie utility functions
|
||||||
|
*/
|
||||||
export const cookieUtils = {
|
export const cookieUtils = {
|
||||||
set: (name: string, value: string, domain = getParentDomain()) => {
|
set: (name: string, value: string, domain = getParentDomain()) => {
|
||||||
document.cookie = `${name}=${value}; domain=${domain}; path=/; secure; samesite=strict`
|
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 { message } from 'antd';
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { cookieUtils } from './request'
|
import { cookieUtils } from './request'
|
||||||
@@ -9,7 +27,10 @@ const API_PREFIX = '/api'
|
|||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
let refreshPromise: Promise<string> | null = null;
|
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> => {
|
const refreshTokenForSSE = async (): Promise<string> => {
|
||||||
if (isRefreshing && refreshPromise) {
|
if (isRefreshing && refreshPromise) {
|
||||||
return refreshPromise;
|
return refreshPromise;
|
||||||
@@ -42,10 +63,19 @@ const refreshTokenForSSE = async (): Promise<string> => {
|
|||||||
return refreshPromise;
|
return refreshPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE message structure
|
||||||
|
*/
|
||||||
export interface SSEMessage {
|
export interface SSEMessage {
|
||||||
event?: string
|
event?: string
|
||||||
data?: string | object
|
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) {
|
export function parseSSEToJSON(sseString: string) {
|
||||||
const events: SSEMessage[] = []
|
const events: SSEMessage[] = []
|
||||||
const lines = sseString.trim().split('\n')
|
const lines = sseString.trim().split('\n')
|
||||||
@@ -77,9 +107,14 @@ export function parseSSEToJSON(sseString: string) {
|
|||||||
return events
|
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 {
|
function parseDataContent(dataContent: string): string | object {
|
||||||
try {
|
try {
|
||||||
// 第一层解码:HTML实体
|
// First layer: HTML entity decoding
|
||||||
let unescaped = dataContent
|
let unescaped = dataContent
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -87,15 +122,15 @@ function parseDataContent(dataContent: string): string | object {
|
|||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/'/g, "'")
|
.replace(/'/g, "'")
|
||||||
|
|
||||||
// 解析第一层JSON
|
// Parse first layer JSON
|
||||||
const firstParse = JSON.parse(unescaped)
|
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("{")) {
|
if (firstParse.data && typeof firstParse.data === 'string' && firstParse.data.includes("{")) {
|
||||||
try {
|
try {
|
||||||
firstParse.data = JSON.parse(firstParse.data)
|
firstParse.data = JSON.parse(firstParse.data)
|
||||||
} catch {
|
} 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: {} }) => {
|
const makeSSERequest = async (url: string, data: any, token: string, config = { headers: {} }) => {
|
||||||
return fetch(`${API_PREFIX}${url}`, {
|
return fetch(`${API_PREFIX}${url}`, {
|
||||||
method: 'POST',
|
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: {} }) => {
|
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => {
|
||||||
try {
|
try {
|
||||||
let token = cookieUtils.get('authToken');
|
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 reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = ''; // 添加缓冲区来处理不完整的消息
|
let buffer = ''; // Buffer for handling incomplete messages
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
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 });
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
buffer += chunk;
|
buffer += chunk;
|
||||||
|
|
||||||
// 处理完整的事件
|
// Process complete events
|
||||||
const events = buffer.split('\n\n');
|
const events = buffer.split('\n\n');
|
||||||
buffer = events.pop() || ''; // 保留最后一个可能不完整的事件
|
buffer = events.pop() || ''; // Keep last potentially incomplete event
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.trim() && onMessage) {
|
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) {
|
if (buffer.trim() && onMessage) {
|
||||||
onMessage(parseSSEToJSON(buffer) ?? {});
|
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_US from 'antd/locale/en_US';
|
||||||
import en_GB from 'antd/locale/en_GB';
|
import en_GB from 'antd/locale/en_GB';
|
||||||
import de_DE from 'antd/locale/de_DE';
|
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-gb'
|
||||||
import 'dayjs/locale/en'
|
import 'dayjs/locale/en'
|
||||||
|
|
||||||
// 全世界主要时区列表
|
/**
|
||||||
|
* Major world timezones list
|
||||||
|
*/
|
||||||
export const timezones = [
|
export const timezones = [
|
||||||
|
|
||||||
'America/Los_Angeles', // 美国洛杉矶
|
'America/Los_Angeles', // Los Angeles, USA
|
||||||
'America/New_York', // 美国纽约
|
'America/New_York', // New York, USA
|
||||||
'Europe/London', // 英国伦敦
|
'Europe/London', // London, UK
|
||||||
'Europe/Berlin', // 德国柏林
|
'Europe/Berlin', // Berlin, Germany
|
||||||
'Europe/Moscow', // 俄罗斯莫斯科
|
'Europe/Moscow', // Moscow, Russia
|
||||||
'Asia/Kolkata', // 印度加尔各答
|
'Asia/Kolkata', // Kolkata, India
|
||||||
'Asia/Shanghai', // 中国上海
|
'Asia/Shanghai', // Shanghai, China
|
||||||
|
|
||||||
// 亚洲
|
// Asia
|
||||||
// 'Asia/Tokyo', // 日本东京
|
// 'Asia/Tokyo', // Tokyo, Japan
|
||||||
// 'Asia/Singapore', // 新加坡
|
// 'Asia/Singapore', // Singapore
|
||||||
// 'Asia/Hong_Kong', // 中国香港
|
// 'Asia/Hong_Kong', // Hong Kong, China
|
||||||
// 'Asia/Taipei', // 中国台北
|
// 'Asia/Taipei', // Taipei, Taiwan
|
||||||
// 'Asia/Seoul', // 韩国首尔
|
// 'Asia/Seoul', // Seoul, South Korea
|
||||||
// 'Asia/Bangkok', // 泰国曼谷
|
// 'Asia/Bangkok', // Bangkok, Thailand
|
||||||
// 'Asia/Jakarta', // 印度尼西亚雅加达
|
// 'Asia/Jakarta', // Jakarta, Indonesia
|
||||||
// 'Asia/Manila', // 菲律宾马尼拉
|
// 'Asia/Manila', // Manila, Philippines
|
||||||
// 'Asia/Dubai', // 阿联酋迪拜
|
// 'Asia/Dubai', // Dubai, UAE
|
||||||
// 'Asia/Tashkent', // 乌兹别克斯坦塔什干
|
// 'Asia/Tashkent', // Tashkent, Uzbekistan
|
||||||
// 'Asia/Riyadh', // 沙特阿拉伯利雅得
|
// 'Asia/Riyadh', // Riyadh, Saudi Arabia
|
||||||
// 'Asia/Baku', // 阿塞拜疆巴库
|
// 'Asia/Baku', // Baku, Azerbaijan
|
||||||
// 'Asia/Istanbul', // 土耳其伊斯坦布尔
|
// 'Asia/Istanbul', // Istanbul, Turkey
|
||||||
|
|
||||||
// 欧洲
|
// Europe
|
||||||
// 'Europe/Paris', // 法国巴黎
|
// 'Europe/Paris', // Paris, France
|
||||||
// 'Europe/Rome', // 意大利罗马
|
// 'Europe/Rome', // Rome, Italy
|
||||||
// 'Europe/Madrid', // 西班牙马德里
|
// 'Europe/Madrid', // Madrid, Spain
|
||||||
// 'Europe/Amsterdam', // 荷兰阿姆斯特丹
|
// 'Europe/Amsterdam', // Amsterdam, Netherlands
|
||||||
// 'Europe/Vienna', // 奥地利维也纳
|
// 'Europe/Vienna', // Vienna, Austria
|
||||||
// 'Europe/Stockholm', // 瑞典斯德哥尔摩
|
// 'Europe/Stockholm', // Stockholm, Sweden
|
||||||
// 'Europe/Oslo', // 挪威奥斯陆
|
// 'Europe/Oslo', // Oslo, Norway
|
||||||
// 'Europe/Copenhagen', // 丹麦哥本哈根
|
// 'Europe/Copenhagen', // Copenhagen, Denmark
|
||||||
// 'Europe/Zurich', // 瑞士苏黎世
|
// 'Europe/Zurich', // Zurich, Switzerland
|
||||||
// 'Europe/Athens', // 希腊雅典
|
// 'Europe/Athens', // Athens, Greece
|
||||||
// 'Europe/Warsaw', // 波兰华沙
|
// 'Europe/Warsaw', // Warsaw, Poland
|
||||||
// 'Europe/Prague', // 捷克布拉格
|
// 'Europe/Prague', // Prague, Czech Republic
|
||||||
// 'Europe/Budapest', // 匈牙利布达佩斯
|
// 'Europe/Budapest', // Budapest, Hungary
|
||||||
// 'Europe/Belgrade', // 塞尔维亚贝尔格莱德
|
// 'Europe/Belgrade', // Belgrade, Serbia
|
||||||
|
|
||||||
// 北美洲
|
// North America
|
||||||
// 'America/Chicago', // 美国芝加哥
|
// 'America/Chicago', // Chicago, USA
|
||||||
// 'America/Denver', // 美国丹佛
|
// 'America/Denver', // Denver, USA
|
||||||
// 'America/Toronto', // 加拿大多伦多
|
// 'America/Toronto', // Toronto, Canada
|
||||||
// 'America/Vancouver', // 加拿大温哥华
|
// 'America/Vancouver', // Vancouver, Canada
|
||||||
// 'America/Mexico_City', // 墨西哥墨西哥城
|
// 'America/Mexico_City', // Mexico City, Mexico
|
||||||
|
|
||||||
// 南美洲
|
// South America
|
||||||
// 'America/Sao_Paulo', // 巴西圣保罗
|
// 'America/Sao_Paulo', // São Paulo, Brazil
|
||||||
// 'America/Buenos_Aires', // 阿根廷布宜诺斯艾利斯
|
// 'America/Buenos_Aires', // Buenos Aires, Argentina
|
||||||
// 'America/Santiago', // 智利圣地亚哥
|
// 'America/Santiago', // Santiago, Chile
|
||||||
// 'America/Lima', // 秘鲁利马
|
// 'America/Lima', // Lima, Peru
|
||||||
// 'America/Bogota', // 哥伦比亚波哥大
|
// 'America/Bogota', // Bogotá, Colombia
|
||||||
// 'America/Caracas', // 委内瑞拉加拉加斯
|
// 'America/Caracas', // Caracas, Venezuela
|
||||||
|
|
||||||
// // 大洋洲
|
// Oceania
|
||||||
// 'Australia/Sydney', // 澳大利亚悉尼
|
// 'Australia/Sydney', // Sydney, Australia
|
||||||
// 'Australia/Melbourne', // 澳大利亚墨尔本
|
// 'Australia/Melbourne', // Melbourne, Australia
|
||||||
// 'Australia/Brisbane', // 澳大利亚布里斯班
|
// 'Australia/Brisbane', // Brisbane, Australia
|
||||||
// 'Australia/Perth', // 澳大利亚珀斯
|
// 'Australia/Perth', // Perth, Australia
|
||||||
// 'New_Zealand/Auckland', // 新西兰奥克兰
|
// 'New_Zealand/Auckland', // Auckland, New Zealand
|
||||||
|
|
||||||
// // 非洲
|
// Africa
|
||||||
// 'Africa/Cairo', // 埃及开罗
|
// 'Africa/Cairo', // Cairo, Egypt
|
||||||
// 'Africa/Johannesburg', // 南非约翰内斯堡
|
// 'Africa/Johannesburg', // Johannesburg, South Africa
|
||||||
// 'Africa/Lagos', // 尼日利亚拉各斯
|
// 'Africa/Lagos', // Lagos, Nigeria
|
||||||
// 'Africa/Casablanca', // 摩洛哥卡萨布兰卡
|
// 'Africa/Casablanca', // Casablanca, Morocco
|
||||||
// 'Africa/Nairobi', // 肯尼亚内罗毕
|
// 'Africa/Nairobi', // Nairobi, Kenya
|
||||||
// 'Africa/Addis_Ababa', // 埃塞俄比亚亚的斯亚贝巴
|
// 'Africa/Addis_Ababa', // Addis Ababa, Ethiopia
|
||||||
|
|
||||||
// // 其他
|
// Other
|
||||||
// 'UTC', // 协调世界时
|
// 'UTC', // Coordinated Universal Time
|
||||||
];
|
];
|
||||||
|
|
||||||
// 注意:时区显示名称已移至i18n翻译文件中(zh.ts和en.ts)
|
/**
|
||||||
// 请使用i18n.t('timezones.时区名称')来获取本地化的时区显示名称
|
* Timezone to Ant Design locale mapping
|
||||||
|
* Key: timezone identifier
|
||||||
// 时区与antd本地化文件的映射
|
* Value: Ant Design locale object
|
||||||
// 键为时区,值为antd本地化文件的名称
|
*/
|
||||||
export const timezoneToAntdLocaleMap: Record<string, Locale> = {
|
export const timezoneToAntdLocaleMap: Record<string, Locale> = {
|
||||||
// 亚洲
|
// Asia
|
||||||
'Asia/Shanghai': zh_CN, // 中国上海 - 中文(中国大陆)
|
'Asia/Shanghai': zh_CN, // Shanghai, China - Chinese (Mainland)
|
||||||
'Asia/Kolkata': hi_IN, // 印度加尔各答 - 印地语
|
'Asia/Kolkata': hi_IN, // Kolkata, India - Hindi
|
||||||
'Europe/Moscow': ru_RU, // 俄罗斯莫斯科 - 俄语
|
'Europe/Moscow': ru_RU, // Moscow, Russia - Russian
|
||||||
'Europe/Berlin': de_DE, // 德国柏林 - 德语
|
'Europe/Berlin': de_DE, // Berlin, Germany - German
|
||||||
'Europe/London': en_GB, // 英国伦敦 - 英语(英国)
|
'Europe/London': en_GB, // London, UK - English (UK)
|
||||||
'America/New_York': en_US, // 美国纽约 - 英语(美国)
|
'America/New_York': en_US, // New York, USA - English (US)
|
||||||
'America/Los_Angeles': en_US, // 美国洛杉矶 - 英语(美国)
|
'America/Los_Angeles': en_US, // Los Angeles, USA - English (US)
|
||||||
|
|
||||||
// 'Asia/Tokyo': 'ja_JP', // 日本东京 - 日语
|
// 'Asia/Tokyo': 'ja_JP', // Tokyo, Japan - Japanese
|
||||||
// 'Asia/Singapore': 'en_SG', // 新加坡 - 英语(新加坡)
|
// 'Asia/Singapore': 'en_SG', // Singapore - English (Singapore)
|
||||||
// 'Asia/Hong_Kong': 'zh_HK', // 中国香港 - 中文(香港)
|
// 'Asia/Hong_Kong': 'zh_HK', // Hong Kong, China - Chinese (Hong Kong)
|
||||||
// 'Asia/Taipei': 'zh_TW', // 中国台北 - 中文(台湾)
|
// 'Asia/Taipei': 'zh_TW', // Taipei, Taiwan - Chinese (Taiwan)
|
||||||
// 'Asia/Seoul': 'ko_KR', // 韩国首尔 - 韩语
|
// 'Asia/Seoul': 'ko_KR', // Seoul, South Korea - Korean
|
||||||
// 'Asia/Bangkok': 'th_TH', // 泰国曼谷 - 泰语
|
// 'Asia/Bangkok': 'th_TH', // Bangkok, Thailand - Thai
|
||||||
// 'Asia/Jakarta': 'id_ID', // 印度尼西亚雅加达 - 印尼语
|
// 'Asia/Jakarta': 'id_ID', // Jakarta, Indonesia - Indonesian
|
||||||
// 'Asia/Manila': 'en_PH', // 菲律宾马尼拉 - 英语(菲律宾)
|
// 'Asia/Manila': 'en_PH', // Manila, Philippines - English (Philippines)
|
||||||
// 'Asia/Dubai': 'ar_AE', // 阿联酋迪拜 - 阿拉伯语
|
// 'Asia/Dubai': 'ar_AE', // Dubai, UAE - Arabic
|
||||||
// 'Asia/Tashkent': 'uz_UZ', // 乌兹别克斯坦塔什干 - 乌兹别克语
|
// 'Asia/Tashkent': 'uz_UZ', // Tashkent, Uzbekistan - Uzbek
|
||||||
// 'Asia/Riyadh': 'ar_SA', // 沙特阿拉伯利雅得 - 阿拉伯语
|
// 'Asia/Riyadh': 'ar_SA', // Riyadh, Saudi Arabia - Arabic
|
||||||
// 'Asia/Baku': 'az_AZ', // 阿塞拜疆巴库 - 阿塞拜疆语
|
// 'Asia/Baku': 'az_AZ', // Baku, Azerbaijan - Azerbaijani
|
||||||
// 'Asia/Istanbul': 'tr_TR', // 土耳其伊斯坦布尔 - 土耳其语
|
// 'Asia/Istanbul': 'tr_TR', // Istanbul, Turkey - Turkish
|
||||||
|
|
||||||
// // 欧洲
|
// Europe
|
||||||
// 'Europe/Paris': 'fr_FR', // 法国巴黎 - 法语
|
// 'Europe/Paris': 'fr_FR', // Paris, France - French
|
||||||
// 'Europe/Rome': 'it_IT', // 意大利罗马 - 意大利语
|
// 'Europe/Rome': 'it_IT', // Rome, Italy - Italian
|
||||||
// 'Europe/Madrid': 'es_ES', // 西班牙马德里 - 西班牙语
|
// 'Europe/Madrid': 'es_ES', // Madrid, Spain - Spanish
|
||||||
// 'Europe/Amsterdam': 'nl_NL', // 荷兰阿姆斯特丹 - 荷兰语
|
// 'Europe/Amsterdam': 'nl_NL', // Amsterdam, Netherlands - Dutch
|
||||||
// 'Europe/Vienna': 'de_AT', // 奥地利维也纳 - 德语(奥地利)
|
// 'Europe/Vienna': 'de_AT', // Vienna, Austria - German (Austria)
|
||||||
// 'Europe/Stockholm': 'sv_SE', // 瑞典斯德哥尔摩 - 瑞典语
|
// 'Europe/Stockholm': 'sv_SE', // Stockholm, Sweden - Swedish
|
||||||
// 'Europe/Oslo': 'nb_NO', // 挪威奥斯陆 - 挪威语
|
// 'Europe/Oslo': 'nb_NO', // Oslo, Norway - Norwegian
|
||||||
// 'Europe/Copenhagen': 'da_DK', // 丹麦哥本哈根 - 丹麦语
|
// 'Europe/Copenhagen': 'da_DK', // Copenhagen, Denmark - Danish
|
||||||
// 'Europe/Zurich': 'de_CH', // 瑞士苏黎世 - 德语(瑞士)
|
// 'Europe/Zurich': 'de_CH', // Zurich, Switzerland - German (Switzerland)
|
||||||
// 'Europe/Athens': 'el_GR', // 希腊雅典 - 希腊语
|
// 'Europe/Athens': 'el_GR', // Athens, Greece - Greek
|
||||||
// 'Europe/Warsaw': 'pl_PL', // 波兰华沙 - 波兰语
|
// 'Europe/Warsaw': 'pl_PL', // Warsaw, Poland - Polish
|
||||||
// 'Europe/Prague': 'cs_CZ', // 捷克布拉格 - 捷克语
|
// 'Europe/Prague': 'cs_CZ', // Prague, Czech Republic - Czech
|
||||||
// 'Europe/Budapest': 'hu_HU', // 匈牙利布达佩斯 - 匈牙利语
|
// 'Europe/Budapest': 'hu_HU', // Budapest, Hungary - Hungarian
|
||||||
// 'Europe/Belgrade': 'sr_RS', // 塞尔维亚贝尔格莱德 - 塞尔维亚语
|
// 'Europe/Belgrade': 'sr_RS', // Belgrade, Serbia - Serbian
|
||||||
|
|
||||||
// // 北美洲
|
// North America
|
||||||
// 'America/Chicago': 'en_US', // 美国芝加哥 - 英语(美国)
|
// 'America/Chicago': 'en_US', // Chicago, USA - English (US)
|
||||||
// 'America/Denver': 'en_US', // 美国丹佛 - 英语(美国)
|
// 'America/Denver': 'en_US', // Denver, USA - English (US)
|
||||||
// 'America/Toronto': 'en_CA', // 加拿大多伦多 - 英语(加拿大)
|
// 'America/Toronto': 'en_CA', // Toronto, Canada - English (Canada)
|
||||||
// 'America/Vancouver': 'en_CA', // 加拿大温哥华 - 英语(加拿大)
|
// 'America/Vancouver': 'en_CA', // Vancouver, Canada - English (Canada)
|
||||||
// 'America/Mexico_City': 'es_MX', // 墨西哥墨西哥城 - 西班牙语(墨西哥)
|
// 'America/Mexico_City': 'es_MX', // Mexico City, Mexico - Spanish (Mexico)
|
||||||
|
|
||||||
// // 南美洲
|
// South America
|
||||||
// 'America/Sao_Paulo': 'pt_BR', // 巴西圣保罗 - 葡萄牙语(巴西)
|
// 'America/Sao_Paulo': 'pt_BR', // São Paulo, Brazil - Portuguese (Brazil)
|
||||||
// 'America/Buenos_Aires': 'es_AR', // 阿根廷布宜诺斯艾利斯 - 西班牙语(阿根廷)
|
// 'America/Buenos_Aires': 'es_AR', // Buenos Aires, Argentina - Spanish (Argentina)
|
||||||
// 'America/Santiago': 'es_CL', // 智利圣地亚哥 - 西班牙语(智利)
|
// 'America/Santiago': 'es_CL', // Santiago, Chile - Spanish (Chile)
|
||||||
// 'America/Lima': 'es_PE', // 秘鲁利马 - 西班牙语(秘鲁)
|
// 'America/Lima': 'es_PE', // Lima, Peru - Spanish (Peru)
|
||||||
// 'America/Bogota': 'es_CO', // 哥伦比亚波哥大 - 西班牙语(哥伦比亚)
|
// 'America/Bogota': 'es_CO', // Bogotá, Colombia - Spanish (Colombia)
|
||||||
// 'America/Caracas': 'es_VE', // 委内瑞拉加拉加斯 - 西班牙语(委内瑞拉)
|
// 'America/Caracas': 'es_VE', // Caracas, Venezuela - Spanish (Venezuela)
|
||||||
|
|
||||||
// // 大洋洲
|
// Oceania
|
||||||
// 'Australia/Sydney': 'en_AU', // 澳大利亚悉尼 - 英语(澳大利亚)
|
// 'Australia/Sydney': 'en_AU', // Sydney, Australia - English (Australia)
|
||||||
// 'Australia/Melbourne': 'en_AU', // 澳大利亚墨尔本 - 英语(澳大利亚)
|
// 'Australia/Melbourne': 'en_AU', // Melbourne, Australia - English (Australia)
|
||||||
// 'Australia/Brisbane': 'en_AU', // 澳大利亚布里斯班 - 英语(澳大利亚)
|
// 'Australia/Brisbane': 'en_AU', // Brisbane, Australia - English (Australia)
|
||||||
// 'Australia/Perth': 'en_AU', // 澳大利亚珀斯 - 英语(澳大利亚)
|
// 'Australia/Perth': 'en_AU', // Perth, Australia - English (Australia)
|
||||||
// 'New_Zealand/Auckland': 'en_NZ', // 新西兰奥克兰 - 英语(新西兰)
|
// 'New_Zealand/Auckland': 'en_NZ', // Auckland, New Zealand - English (New Zealand)
|
||||||
|
|
||||||
// // 非洲
|
// Africa
|
||||||
// 'Africa/Cairo': 'ar_EG', // 埃及开罗 - 阿拉伯语(埃及)
|
// 'Africa/Cairo': 'ar_EG', // Cairo, Egypt - Arabic (Egypt)
|
||||||
// 'Africa/Johannesburg': 'en_ZA', // 南非约翰内斯堡 - 英语(南非)
|
// 'Africa/Johannesburg': 'en_ZA', // Johannesburg, South Africa - English (South Africa)
|
||||||
// 'Africa/Lagos': 'en_NG', // 尼日利亚拉各斯 - 英语(尼日利亚)
|
// 'Africa/Lagos': 'en_NG', // Lagos, Nigeria - English (Nigeria)
|
||||||
// 'Africa/Casablanca': 'fr_MA', // 摩洛哥卡萨布兰卡 - 法语(摩洛哥)
|
// 'Africa/Casablanca': 'fr_MA', // Casablanca, Morocco - French (Morocco)
|
||||||
// 'Africa/Nairobi': 'en_KE', // 肯尼亚内罗毕 - 英语(肯尼亚)
|
// 'Africa/Nairobi': 'en_KE', // Nairobi, Kenya - English (Kenya)
|
||||||
// 'Africa/Addis_Ababa': 'am_ET', // 埃塞俄比亚亚的斯亚贝巴 - 阿姆哈拉语
|
// 'Africa/Addis_Ababa': 'am_ET', // Addis Ababa, Ethiopia - Amharic
|
||||||
|
|
||||||
// // 其他
|
// Other
|
||||||
// 'UTC': 'en_US', // 协调世界时 - 默认英语(美国)
|
// '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';
|
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') => {
|
export const exportToYaml = (data: unknown, filename: string = 'export.yaml') => {
|
||||||
const yamlStr = yaml.dump(data);
|
const yamlStr = yaml.dump(data);
|
||||||
const blob = new Blob([yamlStr], { type: 'text/yaml' });
|
const blob = new Blob([yamlStr], { type: 'text/yaml' });
|
||||||
|
|||||||
Reference in New Issue
Block a user