1
0
forked from zguiy/utils
Files
utils/src/components/tools/TimezoneConverter.vue
2025-06-28 22:38:49 +08:00

654 lines
21 KiB
Vue

<template>
<div class="space-y-6">
<!-- 工具栏 -->
<div class="card p-4">
<div class="flex flex-wrap gap-2">
<button
@click="updateCurrentTime"
class="btn-primary"
>
<FontAwesomeIcon :icon="['fas', 'sync']" class="mr-2" />
刷新时间
</button>
<button
@click="copyResult"
:disabled="!selectedTime"
class="btn-secondary"
>
<FontAwesomeIcon
:icon="copied ? ['fas', 'check'] : ['fas', 'copy']"
:class="['mr-2', copied && 'text-success']"
/>
复制时间
</button>
<button
@click="resetToNow"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'clock']" class="mr-2" />
当前时间
</button>
<button
@click="addCustomTimezone"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'plus']" class="mr-2" />
添加时区
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 输入区域 -->
<div class="space-y-4">
<!-- 时间输入 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">输入时间</h3>
<div class="space-y-4">
<!-- 日期时间输入 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">选择日期时间</label>
<input
v-model="inputDateTime"
type="datetime-local"
class="input-field"
@change="convertTimezones"
>
</div>
<!-- 源时区 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">源时区</label>
<select v-model="sourceTimezone" class="select-field" @change="convertTimezones">
<option v-for="timezone in commonTimezones" :key="timezone.value" :value="timezone.value">
{{ timezone.label }}
</option>
</select>
</div>
<!-- 快速时间选择 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">快速选择</label>
<div class="grid grid-cols-2 gap-2">
<button
v-for="quickTime in quickTimes"
:key="quickTime.label"
@click="setQuickTime(quickTime)"
class="btn-sm btn-secondary text-xs"
>
{{ quickTime.label }}
</button>
</div>
</div>
</div>
</div>
<!-- 常用时区 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">常用时区</h3>
<div class="space-y-2">
<div
v-for="timezone in commonTimezones.slice(0, 8)"
:key="timezone.value"
class="flex items-center justify-between p-2 bg-block rounded"
>
<div>
<div class="font-medium text-primary">{{ timezone.name }}</div>
<div class="text-sm text-secondary">{{ timezone.value }}</div>
</div>
<div class="text-sm text-primary font-mono">
{{ getTimezoneTime(timezone.value) }}
</div>
</div>
</div>
</div>
<!-- 时区偏移计算 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">时差计算</h3>
<div class="space-y-3">
<div class="flex space-x-2">
<select v-model="timezone1" class="select-field flex-1">
<option v-for="timezone in commonTimezones" :key="timezone.value" :value="timezone.value">
{{ timezone.name }}
</option>
</select>
<span class="self-center text-secondary">vs</span>
<select v-model="timezone2" class="select-field flex-1">
<option v-for="timezone in commonTimezones" :key="timezone.value" :value="timezone.value">
{{ timezone.name }}
</option>
</select>
</div>
<div class="bg-block rounded p-3 text-center">
<div class="text-sm text-secondary">时差</div>
<div class="text-lg font-medium text-primary">{{ getTimeDifference() }}</div>
</div>
</div>
</div>
</div>
<!-- 结果区域 -->
<div class="lg:col-span-2 space-y-4">
<!-- 世界时钟 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">世界时钟</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="timezone in displayTimezones"
:key="timezone.value"
class="bg-block rounded-lg p-4"
>
<div class="flex items-center justify-between mb-2">
<div>
<div class="font-medium text-primary">{{ timezone.name }}</div>
<div class="text-xs text-tertiary">{{ timezone.value }}</div>
</div>
<button
@click="removeTimezone(timezone.value)"
class="text-error hover:bg-error hover:bg-opacity-10 p-1 rounded"
>
<FontAwesomeIcon :icon="['fas', 'times']" />
</button>
</div>
<div class="text-center">
<div class="text-2xl font-mono font-bold text-primary mb-1">
{{ getTimezoneTime(timezone.value, 'HH:mm:ss') }}
</div>
<div class="text-sm text-secondary">
{{ getTimezoneTime(timezone.value, 'yyyy-MM-dd EEEE') }}
</div>
<div class="text-xs text-tertiary mt-1">
UTC{{ getTimezoneOffset(timezone.value) }}
</div>
</div>
</div>
</div>
</div>
<!-- 转换结果 -->
<div v-if="conversionResults.length > 0" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">转换结果</h3>
<div class="space-y-3">
<div
v-for="result in conversionResults"
:key="result.timezone"
class="flex items-center justify-between p-3 bg-block rounded-lg"
>
<div class="flex-1">
<div class="font-medium text-primary">{{ result.name }}</div>
<div class="text-sm text-secondary">{{ result.timezone }}</div>
</div>
<div class="text-right">
<div class="text-lg font-mono text-primary">{{ result.time }}</div>
<div class="text-xs text-secondary">{{ result.date }}</div>
</div>
<button
@click="copySpecificTime(result)"
class="ml-3 p-2 text-secondary hover:text-primary transition-colors"
:title="'复制 ' + result.name + ' 时间'"
>
<FontAwesomeIcon :icon="['fas', 'copy']" />
</button>
</div>
</div>
</div>
<!-- 时区信息 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">时区信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- UTC时间 -->
<div class="bg-block rounded-lg p-4">
<div class="text-sm text-secondary mb-1">协调世界时 (UTC)</div>
<div class="text-xl font-mono text-primary">{{ utcTime }}</div>
<div class="text-xs text-tertiary mt-1">Coordinated Universal Time</div>
</div>
<!-- 本地时间 -->
<div class="bg-block rounded-lg p-4">
<div class="text-sm text-secondary mb-1">本地时间</div>
<div class="text-xl font-mono text-primary">{{ localTime }}</div>
<div class="text-xs text-tertiary mt-1">{{ localTimezone }}</div>
</div>
<!-- Unix时间戳 -->
<div class="bg-block rounded-lg p-4">
<div class="text-sm text-secondary mb-1">Unix时间戳</div>
<div class="text-lg font-mono text-primary">{{ unixTimestamp }}</div>
<div class="text-xs text-tertiary mt-1"> / 毫秒</div>
</div>
<!-- ISO 8601 -->
<div class="bg-block rounded-lg p-4">
<div class="text-sm text-secondary mb-1">ISO 8601</div>
<div class="text-sm font-mono text-primary break-all">{{ isoTime }}</div>
<div class="text-xs text-tertiary mt-1">国际标准时间格式</div>
</div>
</div>
</div>
<!-- 日程助手 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">会议时间建议</h3>
<div class="space-y-3">
<div class="flex space-x-2">
<select v-model="meetingTimezone1" class="select-field flex-1">
<option v-for="timezone in commonTimezones" :key="timezone.value" :value="timezone.value">
{{ timezone.name }}
</option>
</select>
<select v-model="meetingTimezone2" class="select-field flex-1">
<option v-for="timezone in commonTimezones" :key="timezone.value" :value="timezone.value">
{{ timezone.name }}
</option>
</select>
</div>
<div class="bg-block rounded p-4">
<div class="text-sm text-secondary mb-2">最佳会议时间段 (工作时间 9:00-18:00)</div>
<div class="space-y-2">
<div
v-for="suggestion in getMeetingSuggestions()"
:key="suggestion.time"
class="flex justify-between items-center text-sm"
>
<span class="text-primary">{{ suggestion.time }}</span>
<span class="text-secondary">{{ suggestion.zones }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 状态消息 -->
<div v-if="statusMessage" class="card p-4">
<div :class="[
'flex items-center space-x-2',
statusType === 'success' ? 'text-success' : 'text-error'
]">
<FontAwesomeIcon
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
/>
<span>{{ statusMessage }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
// 响应式状态
const inputDateTime = ref('')
const sourceTimezone = ref('Asia/Shanghai')
const selectedTime = ref('')
const copied = ref(false)
const statusMessage = ref('')
const statusType = ref<'success' | 'error'>('success')
const currentTime = ref(new Date())
// 时区比较
const timezone1 = ref('Asia/Shanghai')
const timezone2 = ref('America/New_York')
// 会议时间建议
const meetingTimezone1 = ref('Asia/Shanghai')
const meetingTimezone2 = ref('America/New_York')
// 显示的时区列表
const displayTimezones = ref([
{ name: '北京', value: 'Asia/Shanghai' },
{ name: '纽约', value: 'America/New_York' },
{ name: '伦敦', value: 'Europe/London' },
{ name: '东京', value: 'Asia/Tokyo' }
])
// 转换结果
const conversionResults = ref<Array<{
name: string
timezone: string
time: string
date: string
fullTime: string
}>>([])
// 常用时区
const commonTimezones = [
{ name: '北京 (CST)', value: 'Asia/Shanghai', label: 'Asia/Shanghai (UTC+8)' },
{ name: '东京 (JST)', value: 'Asia/Tokyo', label: 'Asia/Tokyo (UTC+9)' },
{ name: '首尔 (KST)', value: 'Asia/Seoul', label: 'Asia/Seoul (UTC+9)' },
{ name: '新加坡 (SGT)', value: 'Asia/Singapore', label: 'Asia/Singapore (UTC+8)' },
{ name: '香港 (HKT)', value: 'Asia/Hong_Kong', label: 'Asia/Hong_Kong (UTC+8)' },
{ name: '悉尼 (AEDT)', value: 'Australia/Sydney', label: 'Australia/Sydney (UTC+11)' },
{ name: '伦敦 (GMT)', value: 'Europe/London', label: 'Europe/London (UTC+0)' },
{ name: '巴黎 (CET)', value: 'Europe/Paris', label: 'Europe/Paris (UTC+1)' },
{ name: '莫斯科 (MSK)', value: 'Europe/Moscow', label: 'Europe/Moscow (UTC+3)' },
{ name: '纽约 (EST)', value: 'America/New_York', label: 'America/New_York (UTC-5)' },
{ name: '洛杉矶 (PST)', value: 'America/Los_Angeles', label: 'America/Los_Angeles (UTC-8)' },
{ name: '芝加哥 (CST)', value: 'America/Chicago', label: 'America/Chicago (UTC-6)' },
{ name: '丹佛 (MST)', value: 'America/Denver', label: 'America/Denver (UTC-7)' },
{ name: 'UTC', value: 'UTC', label: 'UTC (UTC+0)' }
]
// 快速时间选择
const quickTimes = [
{ label: '现在', offset: 0 },
{ label: '1小时后', offset: 1 },
{ label: '明天此时', offset: 24 },
{ label: '下周此时', offset: 24 * 7 }
]
// 计算属性
const utcTime = computed(() => {
return formatTime(currentTime.value, 'UTC', 'yyyy-MM-dd HH:mm:ss')
})
const localTime = computed(() => {
return formatTime(currentTime.value, Intl.DateTimeFormat().resolvedOptions().timeZone, 'yyyy-MM-dd HH:mm:ss')
})
const localTimezone = computed(() => {
return Intl.DateTimeFormat().resolvedOptions().timeZone
})
const unixTimestamp = computed(() => {
const seconds = Math.floor(currentTime.value.getTime() / 1000)
const milliseconds = currentTime.value.getTime()
return `${seconds} / ${milliseconds}`
})
const isoTime = computed(() => {
return currentTime.value.toISOString()
})
// 定时器
let timeInterval: number | undefined
// 格式化时间
const formatTime = (date: Date, timezone: string, format: string): string => {
try {
const options: Intl.DateTimeFormatOptions = {
timeZone: timezone
}
if (format.includes('yyyy')) {
options.year = 'numeric'
}
if (format.includes('MM')) {
options.month = '2-digit'
}
if (format.includes('dd')) {
options.day = '2-digit'
}
if (format.includes('HH')) {
options.hour = '2-digit'
options.hour12 = false
}
if (format.includes('mm')) {
options.minute = '2-digit'
}
if (format.includes('ss')) {
options.second = '2-digit'
}
if (format.includes('EEEE')) {
options.weekday = 'long'
}
const formatter = new Intl.DateTimeFormat('zh-CN', options)
if (format === 'HH:mm:ss') {
return date.toLocaleTimeString('zh-CN', {
timeZone: timezone,
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} else if (format === 'yyyy-MM-dd EEEE') {
return date.toLocaleDateString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
weekday: 'long'
})
} else {
return formatter.format(date)
}
} catch (error) {
return date.toISOString()
}
}
// 获取时区时间
const getTimezoneTime = (timezone: string, format: string = 'yyyy-MM-dd HH:mm:ss'): string => {
return formatTime(currentTime.value, timezone, format)
}
// 获取时区偏移
const getTimezoneOffset = (timezone: string): string => {
try {
const date = new Date()
const utc = date.getTime() + (date.getTimezoneOffset() * 60000)
const targetTime = new Date(utc + getTimezoneOffsetMinutes(timezone) * 60000)
const offset = getTimezoneOffsetMinutes(timezone) / 60
return offset >= 0 ? `+${offset}` : `${offset}`
} catch {
return '+0'
}
}
// 获取时区偏移分钟数
const getTimezoneOffsetMinutes = (timezone: string): number => {
try {
const date = new Date()
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }))
const targetDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }))
return (targetDate.getTime() - utcDate.getTime()) / (1000 * 60)
} catch {
return 0
}
}
// 获取时差
const getTimeDifference = (): string => {
const offset1 = getTimezoneOffsetMinutes(timezone1.value)
const offset2 = getTimezoneOffsetMinutes(timezone2.value)
const diffMinutes = Math.abs(offset1 - offset2)
const hours = Math.floor(diffMinutes / 60)
const minutes = diffMinutes % 60
if (hours === 0) {
return `${minutes} 分钟`
} else if (minutes === 0) {
return `${hours} 小时`
} else {
return `${hours} 小时 ${minutes} 分钟`
}
}
// 获取会议建议
const getMeetingSuggestions = (): Array<{ time: string; zones: string }> => {
const suggestions = []
for (let hour = 9; hour <= 18; hour++) {
const time1 = `${hour.toString().padStart(2, '0')}:00`
const date = new Date()
date.setHours(hour, 0, 0, 0)
const time2 = formatTime(date, meetingTimezone2.value, 'HH:mm')
const hour2 = parseInt(time2.split(':')[0])
if (hour2 >= 9 && hour2 <= 18) {
suggestions.push({
time: `${time1} - ${time2}`,
zones: `${getTimezoneName(meetingTimezone1.value)} - ${getTimezoneName(meetingTimezone2.value)}`
})
}
}
return suggestions.slice(0, 3)
}
// 获取时区名称
const getTimezoneName = (timezone: string): string => {
const found = commonTimezones.find(tz => tz.value === timezone)
return found ? found.name : timezone
}
// 设置快速时间
const setQuickTime = (quickTime: any) => {
const date = new Date()
date.setHours(date.getHours() + quickTime.offset)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
inputDateTime.value = `${year}-${month}-${day}T${hours}:${minutes}`
convertTimezones()
}
// 转换时区
const convertTimezones = () => {
if (!inputDateTime.value) return
const inputDate = new Date(inputDateTime.value)
if (isNaN(inputDate.getTime())) return
conversionResults.value = displayTimezones.value.map(timezone => {
const time = formatTime(inputDate, timezone.value, 'HH:mm:ss')
const date = formatTime(inputDate, timezone.value, 'yyyy-MM-dd EEEE')
const fullTime = formatTime(inputDate, timezone.value, 'yyyy-MM-dd HH:mm:ss')
return {
name: timezone.name,
timezone: timezone.value,
time,
date,
fullTime
}
})
}
// 添加自定义时区
const addCustomTimezone = () => {
const timezone = prompt('请输入时区标识符 (如: Asia/Shanghai):')
if (!timezone) return
try {
// 验证时区是否有效
formatTime(new Date(), timezone, 'HH:mm:ss')
const name = prompt('请输入时区显示名称:', timezone) || timezone
displayTimezones.value.push({
name,
value: timezone
})
convertTimezones()
showStatus('时区添加成功', 'success')
} catch (error) {
showStatus('无效的时区标识符', 'error')
}
}
// 移除时区
const removeTimezone = (timezone: string) => {
displayTimezones.value = displayTimezones.value.filter(tz => tz.value !== timezone)
convertTimezones()
}
// 更新当前时间
const updateCurrentTime = () => {
currentTime.value = new Date()
}
// 重置到现在
const resetToNow = () => {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
inputDateTime.value = `${year}-${month}-${day}T${hours}:${minutes}`
convertTimezones()
}
// 复制结果
const copyResult = async () => {
if (!selectedTime.value) return
try {
await navigator.clipboard.writeText(selectedTime.value)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (error) {
showStatus('复制失败', 'error')
}
}
// 复制特定时间
const copySpecificTime = async (result: any) => {
try {
await navigator.clipboard.writeText(result.fullTime)
showStatus(`已复制 ${result.name} 时间`, 'success')
} catch (error) {
showStatus('复制失败', 'error')
}
}
// 显示状态消息
const showStatus = (message: string, type: 'success' | 'error') => {
statusMessage.value = message
statusType.value = type
setTimeout(() => {
statusMessage.value = ''
}, 3000)
}
// 组件挂载
onMounted(() => {
resetToNow()
updateCurrentTime()
// 每秒更新时间
timeInterval = setInterval(() => {
updateCurrentTime()
}, 1000)
})
// 组件卸载
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
})
</script>