Skip to content

Commit d389316

Browse files
committed
feat(tools): 添加 Unix 时间戳转换工具页面及脚本
新增中英文双语工具页面,包含实时时间显示与时间戳互转功能。提供多时区支持、国际化文本及复制功能。
1 parent bdbae29 commit d389316

3 files changed

Lines changed: 414 additions & 0 deletions

File tree

assets/js/tools/time.js

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
(function() {
2+
const container = document.getElementById('tool-time');
3+
if (!container) return;
4+
5+
const lang = container.getAttribute('data-lang') || 'en';
6+
7+
const i18n = {
8+
'zh-cn': {
9+
titleCurrent: '实时时间',
10+
labelTimeDisplay: '当前时间',
11+
labelTimezone: '选择时区',
12+
labelLocalTimePrefix: '⭐ 本地时间',
13+
labelTimestampSec: '秒级时间戳 (s)',
14+
labelTimestampMs: '毫秒级时间戳 (ms)',
15+
titleConverter: '时间戳转换器',
16+
labelConvertTimestamp: '时间戳 (Timestamp)',
17+
labelConvertDateTime: '日期时间 (Date Time)',
18+
labelConvertTimezone: '时区 (Timezone)',
19+
btnToDateTime: '转换 ➔ 日期时间',
20+
btnToTimestamp: '转换 ➔ 时间戳',
21+
placeholderTimestamp: '输入秒或毫秒时间戳...',
22+
placeholderDateTime: 'YYYY-MM-DD HH:mm:ss',
23+
errorInvalid: '无效的输入格式',
24+
copyBtn: '复制',
25+
copied: '已复制'
26+
},
27+
'en': {
28+
titleCurrent: 'Real-time Time',
29+
labelTimeDisplay: 'Current Time',
30+
labelTimezone: 'Select Timezone',
31+
labelLocalTimePrefix: '⭐ Local Time',
32+
labelTimestampSec: 'Timestamp (s)',
33+
labelTimestampMs: 'Timestamp (ms)',
34+
titleConverter: 'Timestamp Converter',
35+
labelConvertTimestamp: 'Timestamp',
36+
labelConvertDateTime: 'Date Time',
37+
labelConvertTimezone: 'Timezone',
38+
btnToDateTime: 'Convert ➔ Date Time',
39+
btnToTimestamp: 'Convert ➔ Timestamp',
40+
placeholderTimestamp: 'Enter seconds or milliseconds...',
41+
placeholderDateTime: 'YYYY-MM-DD HH:mm:ss',
42+
errorInvalid: 'Invalid input format',
43+
copyBtn: 'Copy',
44+
copied: 'Copied'
45+
}
46+
};
47+
48+
const t = i18n[lang] || i18n['en'];
49+
50+
container.innerHTML = `
51+
<style>
52+
#tool-time .tool-container { max-width: 100%; }
53+
#tool-time .tool-section { margin-bottom: 2rem; padding: 1.5rem; border-radius: 12px; background: var(--card-background); border: 1px solid var(--border-color); }
54+
#tool-time h3 { margin-top: 0; margin-bottom: 1.2rem; font-size: 1.6rem; color: var(--accent-color); border-bottom: 2px solid var(--accent-color); display: inline-block; padding-bottom: 0.3rem; }
55+
#tool-time .time-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
56+
#tool-time .time-card { padding: 1rem; border-radius: 8px; background: var(--body-background); border: 1px solid var(--border-color); }
57+
#tool-time .time-label { font-size: 1.1rem; color: var(--card-text-color-secondary); margin-bottom: 0.4rem; font-weight: bold; }
58+
#tool-time .time-value-wrapper { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
59+
#tool-time .time-value { font-family: 'Fira Code', monospace; font-size: 1.4rem; color: var(--card-text-color-main); word-break: break-all; }
60+
#tool-time .converter-group { display: flex; flex-direction: column; gap: 1.2rem; }
61+
#tool-time .input-row { display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap; }
62+
#tool-time .input-field { flex: 1; min-width: 250px; }
63+
#tool-time input {
64+
width: 100%;
65+
padding: 0.8rem 1rem;
66+
border: 1px solid var(--border-color);
67+
border-radius: 6px;
68+
background: var(--body-background);
69+
color: var(--card-text-color-main);
70+
font-family: 'Fira Code', monospace;
71+
font-size: 1.3rem;
72+
outline: none;
73+
transition: border-color 0.2s;
74+
}
75+
#tool-time input:focus { border-color: var(--accent-color); }
76+
#tool-time .btn {
77+
padding: 0.8rem 1.5rem;
78+
border: none;
79+
border-radius: 6px;
80+
cursor: pointer;
81+
font-weight: bold;
82+
background: var(--accent-color);
83+
color: #fff;
84+
transition: opacity 0.2s;
85+
white-space: nowrap;
86+
height: 3.8rem;
87+
}
88+
#tool-time .btn:hover { opacity: 0.9; }
89+
#tool-time .copy-btn {
90+
padding: 0.2rem 0.5rem;
91+
font-size: 1rem;
92+
background: var(--border-color);
93+
color: var(--card-text-color-main);
94+
border: none;
95+
border-radius: 4px;
96+
cursor: pointer;
97+
transition: all 0.2s;
98+
}
99+
#tool-time select {
100+
width: 100%;
101+
padding: 0.8rem 1rem;
102+
border: 1px solid var(--border-color);
103+
border-radius: 6px;
104+
background: var(--body-background);
105+
color: var(--card-text-color-main);
106+
font-family: inherit;
107+
font-size: 1.3rem;
108+
outline: none;
109+
cursor: pointer;
110+
}
111+
#tool-time select:focus { border-color: var(--accent-color); }
112+
#tool-time .time-display-main { font-size: 2.4rem; font-weight: bold; color: var(--accent-color); margin-bottom: 1rem; text-align: center; }
113+
#tool-time .timezone-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
114+
#tool-time .timezone-row label { font-weight: bold; font-size: 1.2rem; white-space: nowrap; }
115+
#tool-time .converter-timezone { margin-bottom: 1rem; }
116+
</style>
117+
<div class="tool-container">
118+
<div class="tool-section">
119+
<h3>${t.titleCurrent}</h3>
120+
<div class="timezone-row">
121+
<label>${t.labelTimezone}:</label>
122+
<select id="current-timezone"></select>
123+
</div>
124+
<div class="time-display-main" id="current-display">-</div>
125+
<div class="time-grid">
126+
<div class="time-card">
127+
<div class="time-label">${t.labelTimestampSec}</div>
128+
<div class="time-value-wrapper">
129+
<div class="time-value" id="current-sec">-</div>
130+
<button class="copy-btn" data-target="current-sec">${t.copyBtn}</button>
131+
</div>
132+
</div>
133+
<div class="time-card">
134+
<div class="time-label">${t.labelTimestampMs}</div>
135+
<div class="time-value-wrapper">
136+
<div class="time-value" id="current-ms">-</div>
137+
<button class="copy-btn" data-target="current-ms">${t.copyBtn}</button>
138+
</div>
139+
</div>
140+
</div>
141+
</div>
142+
<div class="tool-section">
143+
<h3>${t.titleConverter}</h3>
144+
<div class="converter-group">
145+
<div class="input-field converter-timezone">
146+
<div class="time-label">${t.labelConvertTimezone}</div>
147+
<select id="converter-timezone"></select>
148+
</div>
149+
<div class="input-row">
150+
<div class="input-field">
151+
<div class="time-label">${t.labelConvertTimestamp}</div>
152+
<input type="text" id="input-ts" placeholder="${t.placeholderTimestamp}">
153+
</div>
154+
<button class="btn" id="btn-to-dt">${t.btnToDateTime}</button>
155+
</div>
156+
<div class="input-row">
157+
<div class="input-field">
158+
<div class="time-label">${t.labelConvertDateTime}</div>
159+
<input type="text" id="input-dt" placeholder="${t.placeholderDateTime}">
160+
</div>
161+
<button class="btn" id="btn-to-ts">${t.btnToTimestamp}</button>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
166+
`;
167+
168+
const normalizedLang = lang.toLowerCase() === 'zh-cn' ? 'zh-CN' : lang;
169+
170+
const getTimezones = () => {
171+
const now = new Date();
172+
const allTz = Intl.supportedValuesOf('timeZone');
173+
174+
let dn;
175+
try {
176+
dn = new Intl.DisplayNames([normalizedLang], { type: 'timeZone' });
177+
} catch (e) {
178+
dn = null;
179+
}
180+
181+
const list = allTz.map(tz => {
182+
const dtf = new Intl.DateTimeFormat(normalizedLang, {
183+
timeZone: tz,
184+
timeZoneName: 'longOffset'
185+
});
186+
const parts = dtf.formatToParts(now);
187+
const offset = parts.find(p => p.type === 'timeZoneName').value;
188+
189+
let localizedName = '';
190+
if (dn) {
191+
localizedName = dn.of(tz);
192+
}
193+
194+
if (!localizedName || localizedName === tz) {
195+
try {
196+
const nameParts = new Intl.DateTimeFormat(normalizedLang, {
197+
timeZone: tz,
198+
timeZoneName: 'longGeneric'
199+
}).formatToParts(now);
200+
localizedName = nameParts.find(p => p.type === 'timeZoneName').value;
201+
} catch (e) {
202+
localizedName = tz.replace(/_/g, ' ');
203+
}
204+
}
205+
206+
return {
207+
label: `(${offset}) ${localizedName}`,
208+
value: tz,
209+
offset: offset,
210+
localizedName: localizedName
211+
};
212+
});
213+
214+
// Deduplicate: Group by (Offset + LocalizedName) to avoid repeating "Central European Time" many times
215+
const seen = new Set();
216+
const uniqueList = list.filter(item => {
217+
const key = `${item.offset}-${item.localizedName}`;
218+
if (seen.has(key)) return false;
219+
seen.add(key);
220+
return true;
221+
});
222+
223+
return uniqueList.sort((a, b) => {
224+
const extractOffset = (s) => {
225+
const m = s.match(/GMT([+-])(\d+):?(\d+)?/);
226+
if (!m) return 0;
227+
return (m[1] === '+' ? 1 : -1) * (parseInt(m[2]) * 60 + (m[3] ? parseInt(m[3]) : 0));
228+
};
229+
const offA = extractOffset(a.offset);
230+
const offB = extractOffset(b.offset);
231+
if (offA !== offB) return offA - offB;
232+
return a.value.localeCompare(b.value);
233+
});
234+
};
235+
236+
const timezones = getTimezones();
237+
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
238+
239+
const currentTzSelect = document.getElementById('current-timezone');
240+
const converterTzSelect = document.getElementById('converter-timezone');
241+
242+
const localOpt = (select) => {
243+
const opt = document.createElement('option');
244+
opt.value = localTz;
245+
let localTzName = '';
246+
try {
247+
localTzName = new Intl.DisplayNames([normalizedLang], { type: 'timeZone' }).of(localTz);
248+
} catch(e) {}
249+
250+
if (!localTzName || localTzName === localTz) {
251+
try {
252+
localTzName = new Intl.DateTimeFormat(normalizedLang, {
253+
timeZone: localTz,
254+
timeZoneName: 'longGeneric'
255+
}).formatToParts(new Date()).find(p => p.type === 'timeZoneName').value;
256+
} catch(e) {
257+
localTzName = localTz.replace(/_/g, ' ');
258+
}
259+
}
260+
opt.textContent = `${t.labelLocalTimePrefix} (${localTzName})`;
261+
opt.selected = true;
262+
select.appendChild(opt);
263+
};
264+
265+
localOpt(currentTzSelect);
266+
localOpt(converterTzSelect);
267+
268+
timezones.forEach(tz => {
269+
const opt1 = document.createElement('option');
270+
opt1.value = tz.value;
271+
opt1.textContent = tz.label;
272+
currentTzSelect.appendChild(opt1);
273+
274+
const opt2 = document.createElement('option');
275+
opt2.value = tz.value;
276+
opt2.textContent = tz.label;
277+
converterTzSelect.appendChild(opt2);
278+
});
279+
280+
const currentDisplay = document.getElementById('current-display');
281+
const currentSec = document.getElementById('current-sec');
282+
const currentMs = document.getElementById('current-ms');
283+
284+
const inputTs = document.getElementById('input-ts');
285+
const inputDt = document.getElementById('input-dt');
286+
const btnToDt = document.getElementById('btn-to-dt');
287+
const btnToTs = document.getElementById('btn-to-ts');
288+
289+
function formatDate(date, tz) {
290+
return new Intl.DateTimeFormat('en-GB', {
291+
year: 'numeric',
292+
month: '2-digit',
293+
day: '2-digit',
294+
hour: '2-digit',
295+
minute: '2-digit',
296+
second: '2-digit',
297+
hour12: false,
298+
timeZone: tz
299+
}).format(date).replace(/(\d+)\/(\d+)\/(\d+),?/, '$3-$2-$1');
300+
}
301+
302+
function updateCurrent() {
303+
const now = new Date();
304+
const tz = currentTzSelect.value;
305+
currentDisplay.textContent = formatDate(now, tz);
306+
currentSec.textContent = Math.floor(now.getTime() / 1000);
307+
currentMs.textContent = now.getTime();
308+
}
309+
310+
// Initialize inputs with current time
311+
const initDate = new Date();
312+
inputTs.value = Math.floor(initDate.getTime() / 1000);
313+
inputDt.value = formatDate(initDate, converterTzSelect.value);
314+
315+
setInterval(updateCurrent, 1000);
316+
updateCurrent();
317+
318+
btnToDt.onclick = () => {
319+
let ts = inputTs.value.trim();
320+
if (!ts) return;
321+
let val = parseInt(ts);
322+
if (isNaN(val)) { alert(t.errorInvalid); return; }
323+
if (ts.length <= 11) val *= 1000;
324+
inputDt.value = formatDate(new Date(val), converterTzSelect.value);
325+
};
326+
327+
btnToTs.onclick = () => {
328+
let dtStr = inputDt.value.trim();
329+
if (!dtStr) return;
330+
331+
const tz = converterTzSelect.value;
332+
// Parsing with timezone is tricky with native Date.
333+
// We use Intl to get components and then construct UTC date.
334+
try {
335+
// For simple implementation, we assume YYYY-MM-DD HH:mm:ss
336+
const parts = dtStr.match(/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/);
337+
if (!parts) throw new Error();
338+
339+
const [_, y, m, d, h, min, s] = parts.map(Number);
340+
341+
// Construct a date string that includes the offset for the target timezone
342+
// or use a more robust approach:
343+
const isoStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}T${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
344+
345+
// To handle timezone correctly without libraries:
346+
const date = new Date(isoStr);
347+
// This date is currently interpreted as local time.
348+
// We need to adjust it so that its representation in the TARGET timezone matches the input.
349+
350+
const getOffset = (date, tz) => {
351+
const parts = new Intl.DateTimeFormat('en-US', {
352+
timeZone: tz,
353+
timeZoneName: 'longOffset'
354+
}).formatToParts(date);
355+
const offsetName = parts.find(p => p.type === 'timeZoneName').value; // e.g. "GMT+8" or "GMT-04:00"
356+
const match = offsetName.match(/GMT([+-])(\d{1,2}):?(\d{2})?/);
357+
if (!match) return 0;
358+
const [__, sign, hours, minutes] = match;
359+
const totalMinutes = parseInt(hours) * 60 + (minutes ? parseInt(minutes) : 0);
360+
return (sign === '+' ? 1 : -1) * totalMinutes;
361+
};
362+
363+
const targetOffset = getOffset(date, tz);
364+
const localOffset = -date.getTimezoneOffset();
365+
const diff = targetOffset - localOffset;
366+
367+
const adjustedDate = new Date(date.getTime() - diff * 60000);
368+
369+
if (isNaN(adjustedDate.getTime())) throw new Error();
370+
inputTs.value = Math.floor(adjustedDate.getTime() / 1000);
371+
} catch (e) {
372+
alert(t.errorInvalid);
373+
}
374+
};
375+
376+
// Copy functionality
377+
container.addEventListener('click', (e) => {
378+
if (e.target.classList.contains('copy-btn')) {
379+
const targetId = e.target.getAttribute('data-target');
380+
const text = document.getElementById(targetId).textContent;
381+
navigator.clipboard.writeText(text).then(() => {
382+
const originalText = e.target.textContent;
383+
e.target.textContent = t.copied;
384+
setTimeout(() => e.target.textContent = originalText, 1500);
385+
});
386+
}
387+
});
388+
})();

0 commit comments

Comments
 (0)