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 ( / G M T ( [ + - ] ) ( \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 ( / G M T ( [ + - ] ) ( \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