Files
ToolBox/src/views/TimestampConverter.vue
2026-01-25 20:03:03 +08:00

914 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="timestamp-converter">
<div class="container">
<div class="conversion-card">
<!-- 精度选择器和时区选择器 -->
<div class="controls-row">
<div class="precision-selector">
<button
v-for="option in precisionOptions"
:key="option.value"
@click="timestampType = option.value; outputTimestampType = option.value"
:class="['precision-btn', { active: timestampType === option.value }]"
>
{{ option.label }}
</button>
</div>
<div class="timezone-selector">
<select v-model="timezone" class="timezone-select">
<option v-for="tz in timezoneOptions" :key="tz.value" :value="tz.value">
{{ tz.label }}
</option>
</select>
</div>
</div>
<!-- 日期转换为时间戳 -->
<div class="conversion-row">
<div class="conversion-label">日期 ({{ timezoneLabel }}) 时间戳:</div>
<div class="conversion-inputs">
<div class="input-with-calendar">
<input
v-model="dateStringInput"
@input="convertDateToTimestamp"
type="text"
:placeholder="getDatePlaceholder()"
class="input-field"
/>
<button @click="showDateTimePicker = true" class="calendar-btn" title="选择日期时间">
<i class="far fa-calendar"></i>
</button>
<!-- 日期时间选择器组件 -->
<DateTimePicker
v-model="dateStringInput"
v-model:show="showDateTimePicker"
:precision-type="outputTimestampType"
@confirm="handleDateTimeConfirm"
/>
</div>
<span class="arrow"></span>
<input
v-model="timestampOutput"
type="text"
readonly
class="input-field readonly"
/>
<button @click="copyToClipboard(timestampOutput)" class="copy-btn" title="复制">
<i class="far fa-copy"></i>
</button>
</div>
</div>
<!-- 时间戳转换为日期 -->
<div class="conversion-row">
<div class="conversion-label">时间戳 ({{ timezoneLabel }}) 日期</div>
<div class="conversion-inputs">
<input
v-model="timestampInput"
@input="convertTimestampToDate"
type="text"
placeholder="请输入时间戳"
class="input-field"
/>
<span class="arrow"></span>
<input
v-model="dateStringOutput"
type="text"
readonly
class="input-field readonly"
/>
<button @click="copyToClipboard(dateStringOutput)" class="copy-btn" title="复制">
<i class="far fa-copy"></i>
</button>
</div>
</div>
<!-- 当前时间戳显示与控制 -->
<div class="current-timestamp-row">
<div class="conversion-label">当前时间戳:</div>
<div class="current-timestamp-controls">
<span class="current-timestamp-value">{{ currentTimestampDisplay }}</span>
<button @click="togglePause" class="control-btn-icon" :title="isPaused ? '继续' : '暂停'">
<i :class="isPaused ? 'fas fa-play' : 'fas fa-pause'"></i>
</button>
<button @click="resetData" class="control-btn-icon" title="重置数据">
<i class="fas fa-rotate-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 遮罩层 -->
<Transition name="mask">
<div v-if="showDateTimePicker" class="picker-mask" @click="closeDateTimePicker"></div>
</Transition>
<!-- 提示消息 -->
<Transition name="toast">
<div v-if="toastMessage" class="toast-notification" :class="toastType">
<div class="toast-content">
<i v-if="toastType === 'error'" class="fas fa-circle-exclamation"></i>
<i v-else class="fas fa-circle-check"></i>
<span>{{ toastMessage }}</span>
</div>
<button @click="closeToast" class="toast-close-btn" title="关闭">
<i class="fas fa-xmark"></i>
</button>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import DateTimePicker from '@/components/DateTimePicker.vue'
// 精度选项
const precisionOptions = [
{ value: 'seconds', label: '秒' },
{ value: 'milliseconds', label: '毫秒' },
{ value: 'nanoseconds', label: '纳秒' }
]
// 所有时区选项
const timezoneOptions = [
{ value: 'UTC-12:00', label: 'UTC-12:00 | 贝克岛' },
{ value: 'UTC-11:00', label: 'UTC-11:00 | 萨摩亚' },
{ value: 'UTC-10:00', label: 'UTC-10:00 | 夏威夷' },
{ value: 'UTC-09:30', label: 'UTC-09:30 | 马克萨斯群岛' },
{ value: 'UTC-09:00', label: 'UTC-09:00 | 阿拉斯加' },
{ value: 'UTC-08:00', label: 'UTC-08:00 | 洛杉矶' },
{ value: 'UTC-07:00', label: 'UTC-07:00 | 丹佛' },
{ value: 'UTC-06:00', label: 'UTC-06:00 | 芝加哥' },
{ value: 'UTC-05:00', label: 'UTC-05:00 | 纽约' },
{ value: 'UTC-04:00', label: 'UTC-04:00 | 加拉加斯' },
{ value: 'UTC-03:30', label: 'UTC-03:30 | 纽芬兰' },
{ value: 'UTC-03:00', label: 'UTC-03:00 | 布宜诺斯艾利斯' },
{ value: 'UTC-02:00', label: 'UTC-02:00 | 大西洋中部' },
{ value: 'UTC-01:00', label: 'UTC-01:00 | 亚速尔群岛' },
{ value: 'UTC+00:00', label: 'UTC+00:00 | 伦敦' },
{ value: 'UTC+01:00', label: 'UTC+01:00 | 巴黎' },
{ value: 'UTC+02:00', label: 'UTC+02:00 | 开罗' },
{ value: 'UTC+03:00', label: 'UTC+03:00 | 莫斯科' },
{ value: 'UTC+03:30', label: 'UTC+03:30 | 德黑兰' },
{ value: 'UTC+04:00', label: 'UTC+04:00 | 迪拜' },
{ value: 'UTC+04:30', label: 'UTC+04:30 | 喀布尔' },
{ value: 'UTC+05:00', label: 'UTC+05:00 | 伊斯兰堡' },
{ value: 'UTC+05:30', label: 'UTC+05:30 | 新德里' },
{ value: 'UTC+05:45', label: 'UTC+05:45 | 加德满都' },
{ value: 'UTC+06:00', label: 'UTC+06:00 | 达卡' },
{ value: 'UTC+06:30', label: 'UTC+06:30 | 仰光' },
{ value: 'UTC+07:00', label: 'UTC+07:00 | 曼谷' },
{ value: 'UTC+08:00', label: 'UTC+08:00 | 北京' },
{ value: 'UTC+08:45', label: 'UTC+08:45 | 尤克拉' },
{ value: 'UTC+09:00', label: 'UTC+09:00 | 东京' },
{ value: 'UTC+09:30', label: 'UTC+09:30 | 阿德莱德' },
{ value: 'UTC+10:00', label: 'UTC+10:00 | 悉尼' },
{ value: 'UTC+10:30', label: 'UTC+10:30 | 豪勋爵岛' },
{ value: 'UTC+11:00', label: 'UTC+11:00 | 新喀里多尼亚' },
{ value: 'UTC+12:00', label: 'UTC+12:00 | 奥克兰' },
{ value: 'UTC+12:45', label: 'UTC+12:45 | 查塔姆群岛' },
{ value: 'UTC+13:00', label: 'UTC+13:00 | 萨摩亚' },
{ value: 'UTC+14:00', label: 'UTC+14:00 | 基里巴斯' }
]
// 当前时间相关
const currentTime = ref(new Date())
let timeInterval = null
const isPaused = ref(false)
// 时区相关
const timezone = ref('UTC+08:00')
const timezoneLabel = computed(() => {
const tz = timezoneOptions.find(opt => opt.value === timezone.value)
return tz ? tz.label.split('|')[1].trim() : '北京'
})
// 时间戳转时间
const timestampType = ref('milliseconds')
const timestampInput = ref('')
const dateStringOutput = ref('')
// 时间转时间戳
const dateStringInput = ref('')
const outputTimestampType = ref('milliseconds')
const timestampOutput = ref('')
// 日期时间选择器状态
const showDateTimePicker = ref(false)
// 提示消息
const toastMessage = ref('')
const toastType = ref('success')
// 计算当前秒级时间戳
const currentTimestampSeconds = computed(() => {
return Math.floor(currentTime.value.getTime() / 1000)
})
// 计算当前毫秒级时间戳
const currentTimestampMilliseconds = computed(() => {
return currentTime.value.getTime()
})
// 计算当前纳秒级时间戳
const currentTimestampNanoseconds = computed(() => {
// JavaScript Date 只能精确到毫秒所以纳秒部分设为0
// 纳秒 = 毫秒 * 1000000
const ms = currentTime.value.getTime()
return BigInt(ms) * BigInt(1000000)
})
// 当前时间戳显示
const currentTimestampDisplay = computed(() => {
if (timestampType.value === 'seconds') {
return currentTimestampSeconds.value.toString()
} else if (timestampType.value === 'milliseconds') {
return currentTimestampMilliseconds.value.toString()
} else {
return currentTimestampNanoseconds.value.toString()
}
})
// 获取日期输入框的placeholder
const getDatePlaceholder = () => {
if (timestampType.value === 'seconds') {
return '格式yyyy-MM-dd HH:mm:ss'
} else if (timestampType.value === 'milliseconds') {
return '格式yyyy-MM-dd HH:mm:ss.SSS'
} else {
return '格式yyyy-MM-dd HH:mm:ss.SSSSSSSSS'
}
}
// 更新时间
const updateCurrentTime = () => {
if (!isPaused.value) {
currentTime.value = new Date()
}
}
// 切换暂停/继续
const togglePause = () => {
isPaused.value = !isPaused.value
if (!isPaused.value) {
updateCurrentTime()
}
}
// 重置数据
const resetData = () => {
timestampInput.value = ''
dateStringOutput.value = ''
dateStringInput.value = ''
timestampOutput.value = ''
currentTime.value = new Date()
showToast('数据已重置', 'success')
}
// 时间戳转时间字符串
const convertTimestampToDate = () => {
if (!timestampInput.value.trim()) {
dateStringOutput.value = ''
return
}
try {
let timestampStr = timestampInput.value.trim()
if (!timestampStr) {
dateStringOutput.value = ''
return
}
let timestampMs = 0
let nanoseconds = 0
if (timestampType.value === 'nanoseconds') {
// 纳秒级时间戳,使用 BigInt 处理
try {
const timestampNs = BigInt(timestampStr)
// 转换为毫秒(纳秒 / 1000000
timestampMs = Number(timestampNs / BigInt(1000000))
// 获取纳秒部分(纳秒 % 1000000
nanoseconds = Number(timestampNs % BigInt(1000000))
} catch (error) {
dateStringOutput.value = ''
showToast('请输入有效的纳秒级时间戳', 'error')
return
}
} else {
const timestamp = parseInt(timestampStr)
if (isNaN(timestamp)) {
dateStringOutput.value = ''
showToast('请输入有效的数字', 'error')
return
}
// 如果是秒级时间戳,转换为毫秒
if (timestampType.value === 'seconds') {
// 判断是否是秒级时间戳通常小于13位数字
if (timestamp.toString().length <= 10) {
timestampMs = timestamp * 1000
} else {
timestampMs = timestamp
}
} else {
timestampMs = timestamp
}
}
const date = new Date(timestampMs)
if (isNaN(date.getTime())) {
dateStringOutput.value = ''
showToast('无效的时间戳', 'error')
return
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
const milliseconds = String(date.getMilliseconds()).padStart(3, '0')
if (timestampType.value === 'seconds') {
dateStringOutput.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} else if (timestampType.value === 'milliseconds') {
dateStringOutput.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`
} else {
// 纳秒级:显示毫秒 + 纳秒部分
const nanosecondsStr = String(nanoseconds).padStart(6, '0')
dateStringOutput.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}${nanosecondsStr}`
}
} catch (error) {
dateStringOutput.value = ''
showToast('转换失败:' + error.message, 'error')
}
}
// 时间字符串转时间戳
const convertDateToTimestamp = () => {
if (!dateStringInput.value.trim()) {
timestampOutput.value = ''
return
}
try {
let dateStr = dateStringInput.value.trim()
// 尝试解析不同的时间格式
let date = null
// 处理纳秒级格式yyyy-MM-dd HH:mm:ss.SSSSSSSSS9位纳秒
if (dateStr.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{9}$/)) {
// 提取毫秒部分前3位和纳秒部分后6位
const parts = dateStr.split('.')
const baseTime = parts[0].replace(' ', 'T')
const nanoseconds = parts[1]
const milliseconds = nanoseconds.substring(0, 3)
date = new Date(baseTime + '.' + milliseconds)
}
// 格式1: yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd HH:mm:ss.SSS
else if (dateStr.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,3})?$/)) {
date = new Date(dateStr.replace(' ', 'T'))
}
// 格式2: yyyy-MM-ddTHH:mm:ss 或 yyyy-MM-ddTHH:mm:ss.SSS
else if (dateStr.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?/)) {
date = new Date(dateStr)
}
// 格式3: yyyy/MM/dd HH:mm:ss
else if (dateStr.match(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}/)) {
date = new Date(dateStr.replace(' ', 'T').replace(/\//g, '-'))
}
// 其他格式,直接尝试解析
else {
date = new Date(dateStr)
}
if (isNaN(date.getTime())) {
timestampOutput.value = ''
showToast('无效的时间格式', 'error')
return
}
if (outputTimestampType.value === 'seconds') {
timestampOutput.value = Math.floor(date.getTime() / 1000).toString()
} else if (outputTimestampType.value === 'milliseconds') {
timestampOutput.value = date.getTime().toString()
} else {
// 纳秒级:毫秒 * 1000000
const ms = date.getTime()
timestampOutput.value = (BigInt(ms) * BigInt(1000000)).toString()
}
} catch (error) {
timestampOutput.value = ''
showToast('转换失败:' + error.message, 'error')
}
}
// 处理日期时间选择器确认
const handleDateTimeConfirm = (value) => {
dateStringInput.value = value
convertDateToTimestamp()
}
// 复制到剪贴板
const copyToClipboard = async (text) => {
if (!text) {
showToast('没有可复制的内容', 'error')
return
}
try {
await navigator.clipboard.writeText(text)
showToast('已复制到剪贴板', 'success')
} catch (error) {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.opacity = '0'
document.body.appendChild(textArea)
textArea.select()
try {
document.execCommand('copy')
showToast('已复制到剪贴板', 'success')
} catch (err) {
showToast('复制失败', 'error')
}
document.body.removeChild(textArea)
}
}
// 显示提示消息
const showToast = (message, type = 'success') => {
toastMessage.value = message
toastType.value = type
setTimeout(() => {
closeToast()
}, 3000)
}
// 关闭提示消息
const closeToast = () => {
toastMessage.value = ''
}
// 监听时间戳类型变化
const watchTimestampType = () => {
if (timestampInput.value) {
convertTimestampToDate()
}
}
// 监听输出时间戳类型变化
const watchOutputTimestampType = () => {
if (dateStringInput.value) {
convertDateToTimestamp()
}
}
// 监听时间戳类型变化
watch(timestampType, watchTimestampType)
watch(outputTimestampType, watchOutputTimestampType)
onMounted(() => {
updateCurrentTime()
timeInterval = setInterval(updateCurrentTime, 1000)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
})
</script>
<style scoped>
.timestamp-converter {
width: 100%;
min-height: 100vh;
background: #f5f5f5;
padding: 2rem 1rem;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.conversion-card {
background: #ffffff;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid #e5e5e5;
}
.controls-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
}
.precision-selector {
display: flex;
gap: 0.5rem;
}
.precision-btn {
padding: 0.5rem 1rem;
background: #f5f5f5;
color: #333333;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.precision-btn:hover {
background: #e5e5e5;
}
.precision-btn.active {
background: #1a1a1a;
color: #ffffff;
border-color: #1a1a1a;
}
.timezone-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.timezone-select {
padding: 0.5rem 0.75rem;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 0.875rem;
background: #ffffff;
cursor: pointer;
transition: all 0.2s;
}
.timezone-select:focus {
outline: none;
border-color: #1a1a1a;
}
.conversion-row {
margin-bottom: 1.5rem;
}
.conversion-label {
font-size: 0.875rem;
font-weight: 500;
color: #333333;
margin-bottom: 0.5rem;
}
.conversion-inputs {
display: flex;
gap: 0.75rem;
align-items: center;
}
.arrow {
font-size: 1.25rem;
color: #666666;
font-weight: 500;
flex-shrink: 0;
}
.current-timestamp-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid #e5e5e5;
}
.current-timestamp-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.current-timestamp-value {
font-family: 'Courier New', monospace;
font-size: 1rem;
color: #1a1a1a;
font-weight: 500;
min-width: 150px;
text-align: right;
}
.control-btn {
padding: 0.5rem 1rem;
background: #f5f5f5;
color: #333333;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.control-btn:hover {
background: #e5e5e5;
border-color: #1a1a1a;
}
.control-btn:active {
transform: scale(0.98);
}
.control-btn-icon {
padding: 0.375rem;
background: transparent;
border: 1px solid #d0d0d0;
border-radius: 6px;
color: #666666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
width: 2rem;
height: 2rem;
font-size: 0.875rem;
}
.control-btn-icon:hover {
background: #f5f5f5;
border-color: #1a1a1a;
color: #1a1a1a;
}
.control-btn-icon:active {
transform: scale(0.98);
}
.input-field {
flex: 1;
padding: 0.75rem;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 0.9375rem;
font-family: 'Courier New', monospace;
transition: all 0.2s;
}
.input-field:focus {
outline: none;
border-color: #1a1a1a;
box-shadow: 0 0 0 3px rgba(26, 26, 26, 0.1);
}
.input-field.readonly {
background: #f9f9f9;
cursor: not-allowed;
}
.action-btn {
padding: 0.75rem 1rem;
background: #1a1a1a;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.action-btn:hover {
background: #333333;
}
.action-btn:active {
transform: scale(0.98);
}
.copy-btn {
padding: 0.5rem;
background: transparent;
border: 1px solid #d0d0d0;
border-radius: 6px;
color: #666666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.copy-btn:hover {
background: #f5f5f5;
border-color: #1a1a1a;
color: #1a1a1a;
}
.input-with-calendar {
flex: 1;
position: relative;
display: flex;
align-items: center;
min-width: 0;
}
.input-with-calendar .input-field {
flex: 1;
padding-right: 2.5rem;
min-width: 0;
}
.calendar-btn {
position: absolute;
right: 0.5rem;
padding: 0.5rem;
background: transparent;
border: none;
border-radius: 6px;
color: #666666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
z-index: 1;
}
.calendar-btn:hover {
background: #f5f5f5;
color: #1a1a1a;
}
.input-with-calendar {
position: relative;
}
/* 提示消息样式 */
.toast-notification {
position: fixed;
top: 80px;
right: 20px;
background: #ffffff;
border: 1px solid #e5e5e5;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 1rem 1.25rem;
display: flex;
align-items: center;
gap: 0.75rem;
z-index: 1000;
min-width: 280px;
max-width: 400px;
}
.toast-notification.success {
border-left: 4px solid #10b981;
}
.toast-notification.error {
border-left: 4px solid #ef4444;
}
.toast-content {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
font-size: 0.875rem;
color: #333333;
}
.toast-content svg,
.toast-content i {
flex-shrink: 0;
}
.toast-notification.success .toast-content svg {
color: #10b981;
}
.toast-notification.error .toast-content svg {
color: #ef4444;
}
.toast-close-btn {
background: transparent;
border: none;
color: #666666;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
flex-shrink: 0;
}
.toast-close-btn:hover {
color: #1a1a1a;
}
.toast-enter-active,
.toast-leave-active {
transition: all 0.3s ease;
}
.toast-enter-from {
opacity: 0;
transform: translateX(100%);
}
.toast-leave-to {
opacity: 0;
transform: translateX(100%);
}
@media (max-width: 768px) {
.timestamp-converter {
padding: 1rem 0.5rem;
}
.conversion-card {
padding: 1rem;
}
.controls-row {
flex-direction: column;
align-items: stretch;
}
.precision-selector {
width: 100%;
justify-content: space-between;
}
.precision-btn {
flex: 1;
}
.timezone-selector {
width: 100%;
}
.conversion-inputs {
flex-direction: column;
align-items: stretch;
}
.arrow {
transform: rotate(90deg);
}
.current-timestamp-row {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.current-timestamp-controls {
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.current-timestamp-value {
text-align: left;
}
.control-btn-icon {
width: 2rem;
height: 2rem;
}
.toast-notification {
right: 10px;
left: 10px;
max-width: none;
}
}
</style>