feat: Add base project structure with API and web components
This commit is contained in:
5
web/src/utils/auth.ts
Normal file
5
web/src/utils/auth.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const clearAuthData = () => {
|
||||
console.log("Clearing auth data and redirecting to login");
|
||||
sessionStorage.clear();
|
||||
localStorage.clear()
|
||||
}
|
||||
28
web/src/utils/common.ts
Normal file
28
web/src/utils/common.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const randomString = (length: number = 12, isHasSpecialChars: boolean = true) => {
|
||||
// 定义字符集:大写字母、小写字母、数字和特殊字符
|
||||
const uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const lowercaseChars = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const numberChars = '0123456789';
|
||||
const specialChars = '!@#$%^&*_+-=|;:,.?';
|
||||
|
||||
// 合并所有字符集
|
||||
let allChars = uppercaseChars + lowercaseChars + numberChars;
|
||||
|
||||
// 确保至少包含每种类型的字符
|
||||
let str =
|
||||
uppercaseChars[Math.floor(Math.random() * uppercaseChars.length)] +
|
||||
lowercaseChars[Math.floor(Math.random() * lowercaseChars.length)] +
|
||||
numberChars[Math.floor(Math.random() * numberChars.length)]
|
||||
if (isHasSpecialChars) {
|
||||
allChars+= specialChars;
|
||||
str+= specialChars[Math.floor(Math.random() * specialChars.length)];
|
||||
}
|
||||
|
||||
// 填充剩余的字符,使总长度为12
|
||||
for (let i = 4; i < length; i++) {
|
||||
str += allChars[Math.floor(Math.random() * allChars.length)];
|
||||
}
|
||||
|
||||
// 打乱密码字符顺序
|
||||
return str.split('').sort(() => Math.random() - 0.5).join('');
|
||||
}
|
||||
32
web/src/utils/format.ts
Normal file
32
web/src/utils/format.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param value 时间戳(毫秒)或日期字符串
|
||||
* @param format 目标格式,支持 YYYY-MM-DD HH:mm:ss、YYYY/MM/DD HH:mm:ss、HH:mm 等
|
||||
* @returns 格式化后的日期时间字符串
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
// 扩展dayjs插件
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
export const formatDateTime = (
|
||||
value: string | number | null | undefined,
|
||||
format: string = 'YYYY-MM-DD HH:mm:ss'
|
||||
): string => {
|
||||
if (!value) return '';
|
||||
|
||||
// 检查日期是否有效
|
||||
if (!dayjs(value).isValid()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 每次调用都获取最新的时区设置
|
||||
const currentTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai';
|
||||
dayjs.tz.setDefault(currentTimeZone);
|
||||
|
||||
// 使用最新时区格式化日期
|
||||
return dayjs.tz(value).format(format);
|
||||
};
|
||||
|
||||
298
web/src/utils/request.ts
Normal file
298
web/src/utils/request.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import axios from 'axios';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { clearAuthData } from './auth';
|
||||
import { message } from 'antd';
|
||||
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
export interface ResponseData {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: data | Record<string, string | number | boolean | object | null | undefined>[] | object | any[];
|
||||
error: string;
|
||||
time: number;
|
||||
}
|
||||
interface data {
|
||||
"items": Record<string, string | number | boolean | object | null | undefined>[];
|
||||
"page": {
|
||||
"page": number;
|
||||
"pagesize": number;
|
||||
"total": number;
|
||||
"hasnext": boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: '/api', // 与vite.config.ts中的代理配置对应
|
||||
// timeout: 10000, // 请求超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
|
||||
// 是否正在刷新token
|
||||
let isRefreshing = false;
|
||||
// 存储待重试的请求队列
|
||||
interface RequestQueueItem {
|
||||
config: AxiosRequestConfig;
|
||||
resolve: (token: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
let requests: RequestQueueItem[] = [];
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
if (!config.headers.Authorization) {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
console.error('请求错误:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 刷新token的函数
|
||||
const tokenRefresh = async (): Promise<string> => {
|
||||
try {
|
||||
const refresh_token = localStorage.getItem('refresh_token');
|
||||
if (window.location.hash.includes('#/invite-register')) {
|
||||
throw new Error(i18n.t('common.refreshTokenNotExist'));
|
||||
}
|
||||
if (!refresh_token) {
|
||||
throw new Error(i18n.t('common.refreshTokenNotExist'));
|
||||
}
|
||||
// 使用原生axios调用refresh接口,避免触发拦截器导致的循环调用
|
||||
const response: any = await refreshToken();
|
||||
const newToken = response.access_token;
|
||||
localStorage.setItem('token', newToken);
|
||||
return newToken;
|
||||
} catch (error) {
|
||||
// 如果refresh接口也返回401,则退出登录
|
||||
clearAuthData();
|
||||
message.warning(i18n.t('common.loginExpired'));
|
||||
// 这里可以添加重定向到登录页的逻辑
|
||||
if (!window.location.hash.includes('#/login')) {
|
||||
window.location.href = `/#/login`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
// 对响应数据做点什么
|
||||
const { data: responseData } = response;
|
||||
|
||||
// 如果响应数据不是对象,直接返回
|
||||
if (!responseData || typeof responseData !== 'object') {
|
||||
return responseData;
|
||||
}
|
||||
|
||||
const { data, code } = responseData;
|
||||
|
||||
switch (code) {
|
||||
case 0:
|
||||
case 200:
|
||||
return data !== undefined ? data : responseData;
|
||||
case 401:
|
||||
// 处理未授权情况
|
||||
return handle401Error(response.config);
|
||||
default:
|
||||
if (code === undefined) {
|
||||
return responseData;
|
||||
}
|
||||
if (responseData.error || responseData.msg) {
|
||||
message.warning(responseData.error || responseData.msg)
|
||||
}
|
||||
return Promise.reject(responseData);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 处理网络错误、超时等
|
||||
let msg = error.response?.data?.error || error.response?.error;
|
||||
const status = error?.response ? error.response.status : error;
|
||||
// 服务器响应了但状态码不在2xx范围
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 处理未授权情况
|
||||
return handle401Error(error.config);
|
||||
case 403:
|
||||
msg = i18n.t('common.permissionDenied');
|
||||
break;
|
||||
case 404:
|
||||
msg = i18n.t('common.apiNotFound');
|
||||
break;
|
||||
case 429:
|
||||
msg = i18n.t('common.tooManyRequests');
|
||||
break;
|
||||
case 500:
|
||||
case 502:
|
||||
msg = msg || i18n.t('common.serviceUpgrading');
|
||||
break;
|
||||
case 504:
|
||||
msg = msg || i18n.t('common.serverError');
|
||||
break;
|
||||
default:
|
||||
if (!msg && Array.isArray(error.response?.data?.detail)) {
|
||||
msg = error.response?.data?.detail?.map(item => item.msg).join(';')
|
||||
} else {
|
||||
msg = msg || i18n.t('common.unknownError');
|
||||
}
|
||||
break;
|
||||
}
|
||||
message.warning(msg);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 处理401错误的函数
|
||||
const handle401Error = async (config: AxiosRequestConfig): Promise<unknown> => {
|
||||
// 如果是refresh接口本身返回401,则直接退出登录
|
||||
if (config.url === refreshTokenUrl) {
|
||||
clearAuthData();
|
||||
message.warning(i18n.t('common.loginExpired'));
|
||||
return Promise.reject(new Error(i18n.t('common.loginExpired')));
|
||||
}
|
||||
if (config.url === loginUrl) {
|
||||
return Promise.reject(new Error(i18n.t('common.loginApiCannotRefreshToken')));
|
||||
}
|
||||
if (config.url === logoutUrl) {
|
||||
window.location.href = `/#/login`;
|
||||
return Promise.reject(new Error(i18n.t('common.logoutApiCannotRefreshToken')));
|
||||
}
|
||||
if (config.url?.includes('/public')) {
|
||||
return Promise.reject(new Error(i18n.t('common.publicApiCannotRefreshToken')));
|
||||
}
|
||||
|
||||
// 如果正在刷新token,则将当前请求加入队列
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
requests.push({ config, resolve, reject });
|
||||
}).then((token) => {
|
||||
// 使用新token重新发送请求
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
return service(config);
|
||||
});
|
||||
}
|
||||
|
||||
// 开始刷新token
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const newToken = await tokenRefresh();
|
||||
|
||||
// 更新队列中所有请求的token并重新发送
|
||||
requests.forEach(({ config, resolve }) => {
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
resolve(newToken);
|
||||
});
|
||||
|
||||
// 清空队列
|
||||
requests = [];
|
||||
|
||||
// 使用新token重新发送当前请求
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
return service(config);
|
||||
} catch (error) {
|
||||
// 刷新token失败,清空队列并拒绝所有请求
|
||||
requests.forEach(({ reject }) => {
|
||||
reject(error as Error);
|
||||
});
|
||||
requests = [];
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
interface ObjectWithPush {
|
||||
_push?: boolean;
|
||||
[key: string]: string | number | boolean | object | null | undefined;
|
||||
}
|
||||
|
||||
function paramFilter(params: Record<string, string | number | boolean | ObjectWithPush | null | undefined> = {}) {
|
||||
|
||||
Object.keys(params).forEach(key => {
|
||||
const val = params[key];
|
||||
if (val && typeof(val) === 'object'){
|
||||
const objVal = val as ObjectWithPush;
|
||||
if(objVal._push){
|
||||
delete objVal._push;
|
||||
}else{
|
||||
delete params[key];
|
||||
}
|
||||
} else if(val || val === 0 || val === false){
|
||||
if(typeof(val) === 'string'){
|
||||
params[key] = val.trim();
|
||||
}
|
||||
}else{
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
// 封装请求方法
|
||||
export const request = {
|
||||
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.get(url, {
|
||||
params: paramFilter(data as Record<string, string | number | boolean | ObjectWithPush | null | undefined>),
|
||||
...config || {}
|
||||
});
|
||||
},
|
||||
|
||||
post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.post(url, data, config);
|
||||
},
|
||||
|
||||
put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.put(url, data, config);
|
||||
},
|
||||
|
||||
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.delete(url, config);
|
||||
},
|
||||
|
||||
patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.patch(url, data, config);
|
||||
},
|
||||
uploadFile<T>(url: string, formData?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
...config
|
||||
});
|
||||
},
|
||||
downloadFile(url: string, fileName: string, data?: unknown) {
|
||||
service.post(url, data, {
|
||||
responseType: "blob",
|
||||
})
|
||||
.then(res =>{
|
||||
const link = document.createElement("a");
|
||||
const blob = new Blob([res.data], { type: "application/vnd.ms-excel" });
|
||||
link.style.display = "none";
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.setAttribute("download", decodeURI(res.headers['filename'] || fileName));
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default service;
|
||||
48
web/src/utils/stream.ts
Normal file
48
web/src/utils/stream.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { message } from 'antd';
|
||||
import i18n from '@/i18n'
|
||||
const API_PREFIX = '/api'
|
||||
|
||||
export const handleSSE = async (url: string, data: any, onMessage?: (data: string) => void, config = {}) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`${API_PREFIX}${url}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...config.headers,
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const { status } = response
|
||||
|
||||
switch(status) {
|
||||
case 401:
|
||||
if (url?.includes('/public')) {
|
||||
return message.warning(i18n.t('common.publicApiCannotRefreshToken'));
|
||||
}
|
||||
window.location.href = `/#/login`;
|
||||
break;
|
||||
default:
|
||||
if (!response.body) throw new Error('No response body');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
if (onMessage) {
|
||||
onMessage(chunk);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
168
web/src/utils/timezones.ts
Normal file
168
web/src/utils/timezones.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import en_US from 'antd/locale/en_US';
|
||||
import en_GB from 'antd/locale/en_GB';
|
||||
import de_DE from 'antd/locale/de_DE';
|
||||
import ru_RU from 'antd/locale/ru_RU';
|
||||
import hi_IN from 'antd/locale/hi_IN';
|
||||
import zh_CN from 'antd/locale/zh_CN';
|
||||
import type { Locale } from 'antd/es/locale';
|
||||
import 'dayjs/locale/zh'
|
||||
import 'dayjs/locale/hi'
|
||||
import 'dayjs/locale/ru'
|
||||
import 'dayjs/locale/de'
|
||||
import 'dayjs/locale/en-gb'
|
||||
import 'dayjs/locale/en'
|
||||
|
||||
// 全世界主要时区列表
|
||||
export const timezones = [
|
||||
|
||||
'America/Los_Angeles', // 美国洛杉矶
|
||||
'America/New_York', // 美国纽约
|
||||
'Europe/London', // 英国伦敦
|
||||
'Europe/Berlin', // 德国柏林
|
||||
'Europe/Moscow', // 俄罗斯莫斯科
|
||||
'Asia/Kolkata', // 印度加尔各答
|
||||
'Asia/Shanghai', // 中国上海
|
||||
|
||||
// 亚洲
|
||||
// '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', // 土耳其伊斯坦布尔
|
||||
|
||||
// 欧洲
|
||||
// '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', // 塞尔维亚贝尔格莱德
|
||||
|
||||
// 北美洲
|
||||
// 'America/Chicago', // 美国芝加哥
|
||||
// 'America/Denver', // 美国丹佛
|
||||
// 'America/Toronto', // 加拿大多伦多
|
||||
// 'America/Vancouver', // 加拿大温哥华
|
||||
// 'America/Mexico_City', // 墨西哥墨西哥城
|
||||
|
||||
// 南美洲
|
||||
// 'America/Sao_Paulo', // 巴西圣保罗
|
||||
// 'America/Buenos_Aires', // 阿根廷布宜诺斯艾利斯
|
||||
// 'America/Santiago', // 智利圣地亚哥
|
||||
// 'America/Lima', // 秘鲁利马
|
||||
// 'America/Bogota', // 哥伦比亚波哥大
|
||||
// 'America/Caracas', // 委内瑞拉加拉加斯
|
||||
|
||||
// // 大洋洲
|
||||
// 'Australia/Sydney', // 澳大利亚悉尼
|
||||
// 'Australia/Melbourne', // 澳大利亚墨尔本
|
||||
// 'Australia/Brisbane', // 澳大利亚布里斯班
|
||||
// 'Australia/Perth', // 澳大利亚珀斯
|
||||
// 'New_Zealand/Auckland', // 新西兰奥克兰
|
||||
|
||||
// // 非洲
|
||||
// 'Africa/Cairo', // 埃及开罗
|
||||
// 'Africa/Johannesburg', // 南非约翰内斯堡
|
||||
// 'Africa/Lagos', // 尼日利亚拉各斯
|
||||
// 'Africa/Casablanca', // 摩洛哥卡萨布兰卡
|
||||
// 'Africa/Nairobi', // 肯尼亚内罗毕
|
||||
// 'Africa/Addis_Ababa', // 埃塞俄比亚亚的斯亚贝巴
|
||||
|
||||
// // 其他
|
||||
// 'UTC', // 协调世界时
|
||||
];
|
||||
|
||||
// 注意:时区显示名称已移至i18n翻译文件中(zh.ts和en.ts)
|
||||
// 请使用i18n.t('timezones.时区名称')来获取本地化的时区显示名称
|
||||
|
||||
// 时区与antd本地化文件的映射
|
||||
// 键为时区,值为antd本地化文件的名称
|
||||
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/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', // 土耳其伊斯坦布尔 - 土耳其语
|
||||
|
||||
// // 欧洲
|
||||
// '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', // 塞尔维亚贝尔格莱德 - 塞尔维亚语
|
||||
|
||||
// // 北美洲
|
||||
// 'America/Chicago': 'en_US', // 美国芝加哥 - 英语(美国)
|
||||
// 'America/Denver': 'en_US', // 美国丹佛 - 英语(美国)
|
||||
// 'America/Toronto': 'en_CA', // 加拿大多伦多 - 英语(加拿大)
|
||||
// 'America/Vancouver': 'en_CA', // 加拿大温哥华 - 英语(加拿大)
|
||||
// 'America/Mexico_City': 'es_MX', // 墨西哥墨西哥城 - 西班牙语(墨西哥)
|
||||
|
||||
// // 南美洲
|
||||
// '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', // 委内瑞拉加拉加斯 - 西班牙语(委内瑞拉)
|
||||
|
||||
// // 大洋洲
|
||||
// 'Australia/Sydney': 'en_AU', // 澳大利亚悉尼 - 英语(澳大利亚)
|
||||
// 'Australia/Melbourne': 'en_AU', // 澳大利亚墨尔本 - 英语(澳大利亚)
|
||||
// 'Australia/Brisbane': 'en_AU', // 澳大利亚布里斯班 - 英语(澳大利亚)
|
||||
// 'Australia/Perth': 'en_AU', // 澳大利亚珀斯 - 英语(澳大利亚)
|
||||
// 'New_Zealand/Auckland': 'en_NZ', // 新西兰奥克兰 - 英语(新西兰)
|
||||
|
||||
// // 非洲
|
||||
// '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', // 埃塞俄比亚亚的斯亚贝巴 - 阿姆哈拉语
|
||||
|
||||
// // 其他
|
||||
// 'UTC': 'en_US', // 协调世界时 - 默认英语(美国)
|
||||
};
|
||||
Reference in New Issue
Block a user