implement comprehensive timezone selection with auto-detection
This commit is contained in:
parent
33545be45b
commit
1fa97fa8c5
@ -102,15 +102,30 @@ export function formatTimeInTimezone(
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时区偏移信息
|
||||
* 获取时区偏移信息(简化高效版本)
|
||||
* @param timezone 时区
|
||||
*/
|
||||
export function getTimezoneOffset(timezone: string): string {
|
||||
try {
|
||||
const now = new Date()
|
||||
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000)
|
||||
const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: timezone }))
|
||||
const offset = (targetTime.getTime() - utc.getTime()) / 3600000
|
||||
// 使用 Intl API 直接获取偏移量,更高效
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: timezone,
|
||||
timeZoneName: 'shortOffset'
|
||||
})
|
||||
const parts = formatter.formatToParts(now)
|
||||
const offsetPart = parts.find(part => part.type === 'timeZoneName')
|
||||
|
||||
if (offsetPart?.value) {
|
||||
// 格式化为标准 UTC+08:00 格式
|
||||
const offset = offsetPart.value.replace('GMT', 'UTC')
|
||||
return offset.startsWith('UTC') ? offset : `UTC${offset}`
|
||||
}
|
||||
|
||||
// 降级方案:手动计算
|
||||
const utcTime = now.getTime() + (now.getTimezoneOffset() * 60000)
|
||||
const localTime = new Date(now.toLocaleString('en-US', { timeZone: timezone }))
|
||||
const offset = (localTime.getTime() - utcTime) / 3600000
|
||||
|
||||
const sign = offset >= 0 ? '+' : '-'
|
||||
const hours = Math.abs(Math.floor(offset))
|
||||
@ -121,3 +136,193 @@ export function getTimezoneOffset(timezone: string): string {
|
||||
return 'UTC+00:00'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时区的友好显示名称(符合业内最佳实践)
|
||||
* @param timezone 时区标识符
|
||||
* @param offset UTC 偏移量(可选,避免重复计算)
|
||||
*/
|
||||
function getTimezoneDisplayName(timezone: string, offset?: string): string {
|
||||
try {
|
||||
// 获取 UTC 偏移量(如果未提供则计算)
|
||||
const timezoneOffset = offset || getTimezoneOffset(timezone)
|
||||
|
||||
// 业内最佳实践格式:洲/城市 (UTC+08:00)
|
||||
// 例如:Asia/Shanghai (UTC+08:00)
|
||||
// 优点:包含完整IANA时区标识符,避免同名城市混淆,符合标准
|
||||
return `${timezone} (${timezoneOffset})`
|
||||
} catch (error) {
|
||||
return timezone
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存时区选项,避免重复计算
|
||||
let cachedTimezoneOptions: Array<{ label: string; value: string }> | null = null
|
||||
let cachedGroupedTimezoneOptions: Array<{ type: string; label: string; key: string; children: Array<{ label: string; value: string }> }> | null = null
|
||||
|
||||
/**
|
||||
* 获取所有标准的 IANA 时区列表(带缓存,高效实现)
|
||||
* 使用浏览器标准的 Intl API 获取所有支持的时区,覆盖所有国家
|
||||
* @returns 时区选项数组,包含 label 和 value
|
||||
* @throws 如果浏览器不支持 Intl.supportedValuesOf,抛出错误
|
||||
*/
|
||||
export function getAllTimezoneOptions(): Array<{ label: string; value: string }> {
|
||||
// 返回缓存的结果,避免重复计算
|
||||
if (cachedTimezoneOptions) {
|
||||
return cachedTimezoneOptions
|
||||
}
|
||||
|
||||
// 检查浏览器是否支持 Intl.supportedValuesOf API
|
||||
if (typeof Intl === 'undefined' || !('supportedValuesOf' in Intl)) {
|
||||
throw new Error(
|
||||
'Browser does not support Intl.supportedValuesOf API. ' +
|
||||
'This indicates an implementation issue. ' +
|
||||
'Please use a modern browser that supports ECMAScript Intl API.'
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用标准的 Intl.supportedValuesOf API 获取所有支持的时区
|
||||
const timezones = (Intl as any).supportedValuesOf('timeZone') as string[]
|
||||
|
||||
if (!timezones || timezones.length === 0) {
|
||||
throw new Error('Failed to get timezone list from Intl.supportedValuesOf')
|
||||
}
|
||||
|
||||
// 对时区进行排序:UTC 优先,然后按偏移量排序,最后按地区分组
|
||||
const timezoneData = timezones.map(timezone => {
|
||||
const offset = getTimezoneOffset(timezone)
|
||||
// 提取偏移量数值用于排序
|
||||
const offsetMatch = offset.match(/UTC([+-])(\d{2}):(\d{2})/)
|
||||
const offsetValue = offsetMatch
|
||||
? (offsetMatch[1] === '+' ? 1 : -1) * (parseInt(offsetMatch[2]) * 60 + parseInt(offsetMatch[3]))
|
||||
: 0
|
||||
|
||||
return { timezone, offset, offsetValue }
|
||||
})
|
||||
|
||||
// 排序:UTC 优先,然后按偏移量,最后按地区
|
||||
timezoneData.sort((a, b) => {
|
||||
if (a.timezone === 'UTC') return -1
|
||||
if (b.timezone === 'UTC') return 1
|
||||
|
||||
// 按偏移量排序
|
||||
if (a.offsetValue !== b.offsetValue) {
|
||||
return a.offsetValue - b.offsetValue
|
||||
}
|
||||
|
||||
// 相同偏移量按地区分组
|
||||
const aRegion = a.timezone.split('/')[0]
|
||||
const bRegion = b.timezone.split('/')[0]
|
||||
if (aRegion !== bRegion) {
|
||||
return aRegion.localeCompare(bRegion)
|
||||
}
|
||||
|
||||
// 同一地区内按城市名排序
|
||||
return a.timezone.localeCompare(b.timezone)
|
||||
})
|
||||
|
||||
// 生成选项列表(传递 offset 避免重复计算)
|
||||
cachedTimezoneOptions = timezoneData.map(({ timezone, offset }) => ({
|
||||
label: getTimezoneDisplayName(timezone, offset),
|
||||
value: timezone
|
||||
}))
|
||||
|
||||
return cachedTimezoneOptions
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to get timezone options: ${error instanceof Error ? error.message : String(error)}. ` +
|
||||
'This indicates an implementation issue.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组的时区选项(符合业内最佳实践)
|
||||
* 按 UTC 偏移量分组,便于用户快速找到相同时区的不同城市
|
||||
* @returns 分组的时区选项数组,适用于支持分组的 UI 组件
|
||||
*/
|
||||
export function getGroupedTimezoneOptions(): Array<{ type: string; label: string; key: string; children: Array<{ label: string; value: string }> }> {
|
||||
// 返回缓存的结果
|
||||
if (cachedGroupedTimezoneOptions) {
|
||||
return cachedGroupedTimezoneOptions
|
||||
}
|
||||
|
||||
// 先获取所有时区选项(这会填充缓存)
|
||||
const allOptions = getAllTimezoneOptions()
|
||||
|
||||
// 分离 UTC 和其他时区
|
||||
const utcOptions: Array<{ label: string; value: string }> = []
|
||||
const otherOptions: Array<{ label: string; value: string }> = []
|
||||
|
||||
allOptions.forEach(option => {
|
||||
if (option.value === 'UTC') {
|
||||
utcOptions.push(option)
|
||||
} else {
|
||||
otherOptions.push(option)
|
||||
}
|
||||
})
|
||||
|
||||
// 按 UTC 偏移量分组
|
||||
const grouped = new Map<string, Array<{ label: string; value: string }>>()
|
||||
|
||||
otherOptions.forEach(option => {
|
||||
// 从 label 中提取 UTC 偏移量,格式:Asia/Shanghai (UTC+08:00)
|
||||
const offsetMatch = option.label.match(/\((UTC[+-]\d{2}:\d{2})\)$/)
|
||||
const offset = offsetMatch ? offsetMatch[1] : 'UTC+00:00'
|
||||
|
||||
if (!grouped.has(offset)) {
|
||||
grouped.set(offset, [])
|
||||
}
|
||||
grouped.get(offset)!.push(option)
|
||||
})
|
||||
|
||||
// 对每个分组内的时区按字母顺序排序
|
||||
grouped.forEach((children) => {
|
||||
children.sort((a, b) => {
|
||||
// 提取时区标识符进行比较(去掉 UTC 偏移量后缀)
|
||||
// 格式:Asia/Shanghai (UTC+08:00)
|
||||
const aTimezone = a.label.replace(/\s+\(UTC[+-]\d{2}:\d{2}\)$/, '')
|
||||
const bTimezone = b.label.replace(/\s+\(UTC[+-]\d{2}:\d{2}\)$/, '')
|
||||
return aTimezone.localeCompare(bTimezone)
|
||||
})
|
||||
})
|
||||
|
||||
// 转换为分组格式,按偏移量排序(从负到正)
|
||||
const offsetGroups = Array.from(grouped.entries())
|
||||
.sort((a, b) => {
|
||||
// 提取偏移量数值用于排序
|
||||
const aMatch = a[0].match(/UTC([+-])(\d{2}):(\d{2})/)
|
||||
const bMatch = b[0].match(/UTC([+-])(\d{2}):(\d{2})/)
|
||||
const aValue = aMatch
|
||||
? (aMatch[1] === '+' ? 1 : -1) * (parseInt(aMatch[2]) * 60 + parseInt(aMatch[3]))
|
||||
: 0
|
||||
const bValue = bMatch
|
||||
? (bMatch[1] === '+' ? 1 : -1) * (parseInt(bMatch[2]) * 60 + parseInt(bMatch[3]))
|
||||
: 0
|
||||
return aValue - bValue
|
||||
})
|
||||
.map(([offset, children]) => ({
|
||||
type: 'group',
|
||||
label: offset,
|
||||
key: offset,
|
||||
children
|
||||
}))
|
||||
|
||||
// UTC 单独作为第一个分组(如果有)
|
||||
const result: Array<{ type: string; label: string; key: string; children: Array<{ label: string; value: string }> }> = []
|
||||
|
||||
if (utcOptions.length > 0) {
|
||||
result.push({
|
||||
type: 'group',
|
||||
label: 'UTC',
|
||||
key: 'UTC',
|
||||
children: utcOptions
|
||||
})
|
||||
}
|
||||
|
||||
result.push(...offsetGroups)
|
||||
|
||||
cachedGroupedTimezoneOptions = result
|
||||
return cachedGroupedTimezoneOptions
|
||||
}
|
||||
|
||||
@ -28,12 +28,16 @@
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Timezone')">
|
||||
<n-alert v-if="timezoneError" type="error" style="margin-bottom: 8px">
|
||||
{{ timezoneError }}
|
||||
</n-alert>
|
||||
<n-select
|
||||
v-model:value="systemSettings.timezone"
|
||||
:options="timezoneOptions"
|
||||
style="width: 250px"
|
||||
filterable
|
||||
:placeholder="t('Select timezone')"
|
||||
:disabled="timezoneOptions.length === 0"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
@ -210,7 +214,7 @@ import { Icon } from '@iconify/vue'
|
||||
import { getCurrentLocale, setLocale, locales, initLocale, t } from '../../shared/i18n'
|
||||
import { useAuthStore } from '../../shared/stores/auth'
|
||||
import { getEnvironmentConfig, updateEnvironmentConfig, restartEnvironment, type EnvironmentConfig } from '../../shared/api/system'
|
||||
import { getCurrentTimezone } from '../../shared/utils/timezone'
|
||||
import { getCurrentTimezone, getGroupedTimezoneOptions } from '../../shared/utils/timezone'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
@ -254,20 +258,9 @@ const pageSizeOptions = [
|
||||
{ label: '100', value: 100 }
|
||||
]
|
||||
|
||||
// 时区选项
|
||||
const timezoneOptions = [
|
||||
{ label: 'Asia/Shanghai (中国标准时间)', value: 'Asia/Shanghai' },
|
||||
{ label: 'Asia/Hong_Kong (中国香港时间)', value: 'Asia/Hong_Kong' },
|
||||
{ label: 'Asia/Singapore (新加坡时间)', value: 'Asia/Singapore' },
|
||||
{ label: 'UTC (协调世界时)', value: 'UTC' },
|
||||
{ label: 'America/New_York (美国东部时间)', value: 'America/New_York' },
|
||||
{ label: 'America/Los_Angeles (美国西部时间)', value: 'America/Los_Angeles' },
|
||||
{ label: 'Europe/London (英国时间)', value: 'Europe/London' },
|
||||
{ label: 'Europe/Paris (法国时间)', value: 'Europe/Paris' },
|
||||
{ label: 'Europe/Berlin (德国时间)', value: 'Europe/Berlin' },
|
||||
{ label: 'Australia/Sydney (澳大利亚东部时间)', value: 'Australia/Sydney' },
|
||||
{ label: 'Pacific/Auckland (新西兰时间)', value: 'Pacific/Auckland' }
|
||||
]
|
||||
// 时区选项 - 使用标准的 IANA 时区列表,按 UTC 偏移量分组(符合业内最佳实践)
|
||||
const timezoneOptions = ref<Array<{ type: string; label: string; key: string; children: Array<{ label: string; value: string }> }>>([])
|
||||
const timezoneError = ref<string | null>(null)
|
||||
|
||||
// 数据库类型选项
|
||||
const dbTypeOptions = [
|
||||
@ -404,6 +397,15 @@ onMounted(async () => {
|
||||
initLocale()
|
||||
systemSettings.language = getCurrentLocale()
|
||||
|
||||
// 初始化时区选项(使用分组显示,符合业内最佳实践)
|
||||
try {
|
||||
timezoneOptions.value = getGroupedTimezoneOptions()
|
||||
} catch (error) {
|
||||
timezoneError.value = error instanceof Error ? error.message : String(error)
|
||||
message.error(t('Failed to load timezone options') + ': ' + timezoneError.value)
|
||||
console.error('Failed to load timezone options:', error)
|
||||
}
|
||||
|
||||
// 如果是系统管理员,加载环境配置(静默加载,不显示消息)
|
||||
if (isAdmin.value) {
|
||||
await loadEnvironmentConfig(false)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user