init
This commit is contained in:
897
src/views/TimestampConverter.vue
Normal file
897
src/views/TimestampConverter.vue
Normal file
@@ -0,0 +1,897 @@
|
||||
<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="选择日期时间">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<rect x="2" y="3" width="12" height="11" rx="1" stroke-width="1.5"/>
|
||||
<path d="M5 1v4M11 1v4M2 7h12" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</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="复制">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<rect x="5" y="5" width="8" height="8" rx="1" stroke-width="1.5"/>
|
||||
<path d="M3 11V5a2 2 0 0 1 2-2h6" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</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="复制">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<rect x="5" y="5" width="8" height="8" rx="1" stroke-width="1.5"/>
|
||||
<path d="M3 11V5a2 2 0 0 1 2-2h6" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</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" :title="isPaused ? '继续' : '暂停'">
|
||||
{{ isPaused ? '▶' : 'II' }} {{ isPaused ? '继续' : '暂停' }}
|
||||
</button>
|
||||
<button @click="resetData" class="control-btn" title="重置数据">C 重置数据</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">
|
||||
<svg v-if="toastType === 'error'" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<circle cx="8" cy="8" r="6" stroke-width="1.5"/>
|
||||
<path d="M8 5v3M8 11h.01" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<path d="M13 4L6 11L3 8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>{{ toastMessage }}</span>
|
||||
</div>
|
||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor">
|
||||
<path d="M3 3l8 8M11 3l-8 8" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</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.SSSSSSSSS(9位纳秒)
|
||||
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: 0.75rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
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: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.current-timestamp-value {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-notification {
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user