双时区时钟, 可自定义时区

  样式:

f31a20bde4758edbaf05e73802fd8d0

 


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>双时区时钟 | 北京&伦敦&纽约</title>
    <meta name="theme-color" content="#061326">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        :root {
            --bg-start: #061326; --bg-mid: #0a3150; --bg-end: #0e746f;
            --panel: rgba(7,22,38,.48); --panel-strong: rgba(8,25,43,.76);
            --primary: #5eead4; --primary-strong: #2dd4bf; --secondary: #7dd3fc; --accent: #fb7185;
            --clock-face: rgba(235,248,255,.92); --clock-text: #17324a;
            --hour-hand: #16324a; --minute-hand: #0ea5a5; --second-hand: #f43f5e;
        }
        .clock-face::after{
            content: attr(data-label);
            position: absolute;
            top: 65%;
            left: 50%;
            transform: translateX(-50%);
            color: #17324a57;
            font-size: 12px;
        }
        body {
            min-height: 100vh;
            background: linear-gradient(135deg, var(--bg-start), var(--bg-mid), var(--bg-end));
            color: #fff;
            font-family: system-ui, sans-serif;
            padding: 20px;
            transition: all 0.3s ease;
        }
        .global-controls {
			position: fixed;
			top: 20px;
			right: 20px;
			display: flex;
			gap: 10px;
			z-index: 999;
			transition: opacity 0.3s ease, transform 0.3s ease;
		}
		.global-controls.hidden {
			opacity: 0;
			transform: translateY(-20px);
			pointer-events: none;
			display: none;
		}
		body.fullscreen-mode:hover .global-controls.hidden {
			opacity: 1;
			transform: translateY(0);
			pointer-events: all;
		}
        .control-btn {
            padding: 10px 14px;
            border: none;
            border-radius: 8px;
            background: var(--panel);
            color: var(--primary);
            backdrop-filter: blur(8px);
            cursor: pointer;
            font-size: 14px;
            border: 1px solid rgba(255,255,255,.1);
            transition: 0.2s;
        }
        .control-btn:hover { background: var(--panel-strong); }
        .theme-panel {
            position: fixed;
            top: 80px;
            right: 20px;
            background: var(--panel-strong);
            backdrop-filter: blur(12px);
            border-radius: 12px;
            padding: 20px;
            z-index: 998;
            opacity: 0;
            transform: translateY(-10px);
            pointer-events: none;
            transition: all 0.25s ease;
            border: 1px solid rgba(255,255,255,.1);
        }
        .theme-panel.open { opacity: 1; transform: translateY(0); pointer-events: all; }
        .theme-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
        }
        .theme-swatch {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
            border: none;
            background: transparent;
            color: #fff;
            cursor: pointer;
        }
        .swatch-preview {
            width: 50px;
            height: 50px;
            border-radius: 50%;
            background: var(--swatch);
            border: 3px solid transparent;
            transition: 0.2s;
        }
        .theme-swatch.active .swatch-preview { border-color: #fff; }
        .dual-clock-wrap {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 60px;
            min-height: 85vh;
            flex-wrap: wrap;
        }
		.add-clock-box{
			display:flex;
			gap:8px;
			align-items:center;
		}
		#addZone, #addClockBtn{
			padding: 10px 14px;
			border: none;
			border-radius: 8px;
			background: var(--panel);
			color: var(--primary);
			backdrop-filter: blur(8px);
			cursor: pointer;
			font-size: 14px;
			border: 1px solid rgba(255, 255, 255, .1);
			transition: 0.2s;
		}
        .clock-card {
            background: var(--panel);
            padding: 30px;
            border-radius: 20px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255,255,255,.08);
            text-align: center;
            min-width: 320px;
        }
        .clock-title {
            color: var(--primary);
            margin-bottom: 20px;
            font-size: 22px;
            font-weight: 500;
        }
        .analog-clock {
            width: 220px;
            height: 220px;
            margin: 0 auto;
            position: relative;
            background: var(--clock-face);
            border-radius: 50%;
            box-shadow: 0 0 20px rgba(0,0,0,0.2);
        }
        .hand {
            position: absolute;
            bottom: 50%;
            left: 50%;
            transform-origin: bottom center;
            border-radius: 4px;
        }
        .hour-hand {
            width: 7px;
            height: 60px;
            background: var(--hour-hand);
            margin-left: -3.5px;
            z-index: 3;
        }
        .minute-hand {
            width: 4px;
            height: 80px;
            background: var(--minute-hand);
            margin-left: -2px;
            z-index: 2;
        }
        .second-hand {
            width: 2px;
            height: 90px;
            background: var(--second-hand);
            margin-left: -1px;
            z-index: 4;
        }
        .center-dot {
            width: 14px;
            height: 14px;
            background: var(--accent);
            border-radius: 50%;
            position: absolute;
            top: 50%;
            left: 49%;
            transform: translate(-50%, -50%);
            z-index: 5;
        }
        .clock-mark {
            position: absolute;
            width: 2px;
            height: 8px;
            background: rgba(23,50,74,0.5);
            top: 0;
            left: 50%;
            margin-left: -1px;
            transform-origin: center 110px;
            transform: rotate(var(--angle, 0deg));
        }
        .hour-mark {
            width: 4px;
            height: 15px;
            background: var(--clock-text);
            margin-left: -2px;
        }
        .clock-number {
            position: absolute;
            color: var(--clock-text);
            font-size: 20px;
            font-weight: 600;
            transform: translate(-50%, -50%);
        }
        .digital-display {
            margin-top: 20px;
            font-size: 42px;
            font-weight: 300;
        }
        .sec-time { color: var(--accent); font-size: 20px; }
        .date-info { color: var(--secondary); font-size: 16px; }
        .weekday-info { color: rgba(255,255,255,0.7); font-size: 14px; }
        .hidden-digital .digital-display { display: none; }
        .toast {
            position: fixed;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%) translateY(20px);
            background: rgba(0,0,0,.75);
            color: #fff;
            padding: 12px 24px;
            border-radius: 30px;
            opacity: 0;
            z-index: 1000;
            transition: all 0.3s ease;
        }
        .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
        .copyright {
            text-align: center;
            color: rgba(255,255,255,.4);
            font-size: 13px;
            padding: 20px 0;
        }
        .fullscreen-mode body { overflow: hidden; }
    </style>
</head>
<body>
    <div class="global-controls">
        <div class="add-clock-box">
		  <select id="addZone"></select>
		  <button id="addClockBtn">🕒 添加时钟</button>
		</div>
		<button id="theme-button" class="control-btn">🎨 切换主题</button>
        <button id="toggle-digital" class="control-btn">🕒 隐藏数字时间</button>
        <button id="fullscreen-button" class="control-btn">⛶ 全屏显示</button>
    </div>
    <div id="theme-panel" class="theme-panel" aria-hidden="true">
        <div id="theme-grid" class="theme-grid"></div>
    </div>
    <div class="dual-clock-wrap" id="clockContainer"></div>
	<div id="toast"></div>
    <footer class="copyright"><span id="copyright-year"></span></footer>
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            const timezoneList = [
				{"id":"NewYork","label":"美国·纽约","zone":"America/New_York"},
				{"id":"Hong_Kong","label":"中国·香港","zone":"Asia/Hong_Kong"},
				{"id":"Tokyo","label":"日本·东京","zone":"Asia/Tokyo"},
				{"id":"Seoul","label":"韩国·首尔","zone":"Asia/Seoul"},
				{"id":"Singapore","label":"新加坡","zone":"Asia/Singapore"},
				{"id":"Asia/Bangkok","label":"泰国·曼谷","zone":"Asia/Bangkok"},
				{"id":"Dubai","label":"迪拜","zone":"Asia/Dubai"},
				{"id":"Paris","label":"法国·巴黎","zone":"Europe/Paris"},
				{"id":"Berlin","label":"德国·柏林","zone":"Europe/Berlin"},
				{"id":"Moscow","label":"俄罗斯·莫斯科","zone":"Europe/Moscow"},
				{"id":"Los_Angeles","label":"美国·洛杉矶","zone":"America/Los_Angeles"},
				{"id":"Chicago","label":"美国·芝加哥","zone":"America/Chicago"},
				{"id":"Toronto","label":"多伦多","zone":"America/Toronto"},
				{"id":"Sydney","label":"悉尼","zone":"Australia/Sydney"},
				{"id":"Auckland","label":"奥克兰","zone":"Pacific/Auckland"}
			];
			const clockTimezoneConfig = [
				{"id":"BeiJing","label":"中国·北京","zone":"Asia/Shanghai"},
				{"id":"LonDon","label":"英国·伦敦","zone":"Europe/London"}
			];
            const root = document.documentElement;
            const body = document.body;
            const globalControls = document.querySelector('.global-controls');
            const themeButton = document.getElementById('theme-button');
            const addZone = document.getElementById('addZone');
			const addClockBtn = document.getElementById('addClockBtn');
			
			const themePanel = document.getElementById('theme-panel');
            const themeGrid = document.getElementById('theme-grid');
            const toggleDigitalButton = document.getElementById('toggle-digital');
            const fullscreenButton = document.getElementById('fullscreen-button');
            const toast = document.getElementById('toast');
            const copyrightYear = document.getElementById('copyright-year');
            const clockContainer = document.getElementById('clockContainer');

            let resizeTimer = null;
            let toastTimer = null;
            let showDigital = localStorage.getItem('clock-show-digital') !== 'false';
            const themes = [
                {
                    id: 'aurora',
                    name: '极光青',
                    swatch: 'linear-gradient(135deg,#14b8a6,#38bdf8)',
                    vars: {
                        '--bg-start': '#061326', '--bg-mid': '#0a3150', '--bg-end': '#0e746f',
                        '--panel': 'rgba(7,22,38,.48)', '--panel-strong': 'rgba(8,25,43,.76)',
                        '--primary': '#5eead4', '--primary-strong': '#2dd4bf', '--secondary': '#7dd3fc', '--accent': '#fb7185',
                        '--clock-face': 'rgba(235,248,255,.92)', '--clock-text': '#17324a',
                        '--hour-hand': '#16324a', '--minute-hand': '#0ea5a5', '--second-hand': '#f43f5e'
                    }
                },
                {
                    id: 'ocean',
                    name: '深海蓝',
                    swatch: 'linear-gradient(135deg,#1d4ed8,#06b6d4)',
                    vars: {
                        '--bg-start': '#030b1d', '--bg-mid': '#08245d', '--bg-end': '#075d78',
                        '--panel': 'rgba(3,17,44,.50)', '--panel-strong': 'rgba(4,20,50,.78)',
                        '--primary': '#67e8f9', '--primary-strong': '#22d3ee', '--secondary': '#93c5fd', '--accent': '#f472b6',
                        '--clock-face': 'rgba(239,247,255,.94)', '--clock-text': '#15335c',
                        '--hour-hand': '#142e55', '--minute-hand': '#0284c7', '--second-hand': '#db2777'
                    }
                },
                {
                    id: 'sunset',
                    name: '日落橙',
                    swatch: 'linear-gradient(135deg,#ea580c,#fb7185)',
                    vars: {
                        '--bg-start': '#241006', '--bg-mid': '#78320f', '--bg-end': '#8a294c',
                        '--panel': 'rgba(42,17,11,.48)', '--panel-strong': 'rgba(53,20,15,.78)',
                        '--primary': '#fdba74', '--primary-strong': '#fb923c', '--secondary': '#fda4af', '--accent': '#f43f5e',
                        '--clock-face': 'rgba(255,247,237,.94)', '--clock-text': '#5c2b1a',
                        '--hour-hand': '#592b1c', '--minute-hand': '#ea580c', '--second-hand': '#e11d48'
                    }
                }
            ];
            function renderClockCards() {
				const frag = document.createDocumentFragment();
				clockContainer.innerHTML = '';
				clockTimezoneConfig.forEach(item => {
					const cardHtml = `
						<div class="clock-card" id="${item.id}">
							<h3 class="clock-title">${item.label}时间</h3>
							<div class="analog-clock" data-tz="${item.zone}">
								<div id="${item.id}-hour-hand" class="hand hour-hand"></div>
								<div id="${item.id}-minute-hand" class="hand minute-hand"></div>
								<div id="${item.id}-second-hand" class="hand second-hand"></div>
								<div class="center-dot"></div>
								<div id="${item.id}-clock-face" class="clock-face"></div>
							</div>
							<div class="digital-display">
								<div style="display: flex;justify-content: center;gap: 5px;margin-top:10px;">
									<span id="${item.id}-date-display" class="date-info">加载中...</span>
									<span id="${item.id}-weekday-display" class="weekday-info"></span>
								</div>
								<div style="display: flex;justify-content: center; gap: 5px;">
									<span id="${item.id}-digital-time" class="main-time">00:00</span>
									<span id="${item.id}-digital-seconds" class="sec-time">00</span>
								</div>
							</div>
						</div>
					`;
					const temp = document.createElement('div');
					temp.innerHTML = cardHtml;
					frag.appendChild(temp.firstElementChild);
				})
				clockContainer.appendChild(frag);
			}
            class SingleTimezoneClock {
                constructor(timeZone, prefixId, displayName) {
                    this.timeZone = timeZone;
                    this.prefix = prefixId;
                    this.displayName = displayName;
                    this.hourHand = document.getElementById(`${prefixId}-hour-hand`);
                    this.minuteHand = document.getElementById(`${prefixId}-minute-hand`);
                    this.secondHand = document.getElementById(`${prefixId}-second-hand`);
                    this.digitalTime = document.getElementById(`${prefixId}-digital-time`);
                    this.digitalSeconds = document.getElementById(`${prefixId}-digital-seconds`);
                    this.dateDisplay = document.getElementById(`${prefixId}-date-display`);
                    this.weekdayDisplay = document.getElementById(`${prefixId}-weekday-display`);
                    this.clockFace = document.getElementById(`${prefixId}-clock-face`);
                    this.clockFace.setAttribute('data-label', this.displayName);
                    this.lastDigitalSecond = -1;
                    this.lastDateKey = '';
                    this.initFormatters();
                    this.createClockMarks();
                    this.updateLoop();
                }
                initFormatters() {
                    this.timeFormatter = new Intl.DateTimeFormat('zh-CN', {
                        timeZone: this.timeZone,
                        hour12: false,
                        hour: '2-digit',
                        minute: '2-digit',
                        second: '2-digit'
                    });
                    this.dateFormatter = new Intl.DateTimeFormat('zh-CN', {
                        timeZone: this.timeZone,
                        year: 'numeric',
                        month: 'long',
                        day: 'numeric'
                    });
                    this.weekdayFormatter = new Intl.DateTimeFormat('zh-CN', {
                        timeZone: this.timeZone,
                        weekday: 'long'
                    });
                }
                createClockMarks() {
                    const faceSize = this.clockFace.clientWidth;
                    if (!faceSize) return;
                    const radius = faceSize / 2;
                    const numberRadius = radius * 0.735;
                    const fragment = document.createDocumentFragment();
                    this.clockFace.innerHTML = '';
                    for (let i = 0; i < 60; i++) {
                        const mark = document.createElement('span');
                        mark.className = i % 5 === 0 ? 'clock-mark hour-mark' : 'clock-mark';
                        mark.style.setProperty('--angle', `${i * 6}deg`);
                        fragment.appendChild(mark);
                    }
                    for (let n = 1; n <= 12; n++) {
                        const number = document.createElement('span');
                        number.className = 'clock-number';
                        number.textContent = n;
                        const angle = (n * 30 - 90) * Math.PI / 180;
                        number.style.left = `${radius + numberRadius * Math.cos(angle)}px`;
                        number.style.top = `${radius + numberRadius * Math.sin(angle)}px`;
                        fragment.appendChild(number);
                    }
                    this.clockFace.appendChild(fragment);
                }
                getClockParts() {
                    const now = new Date();
                    const timeParts = this.timeFormatter.formatToParts(now);
                    const getPart = type => {
                        const matched = timeParts.find(part => part.type === type);
                        return matched ? Number(matched.value) : 0;
                    };
                    return {
                        hours: getPart('hour') % 24,
                        minutes: getPart('minute'),
                        seconds: getPart('second'),
                        dateText: this.dateFormatter.format(now),
                        weekdayText: this.weekdayFormatter.format(now)
                    };
                }
                updateLoop() {
                    const parts = this.getClockParts();
                    const hourDeg = (parts.hours % 12) * 30 + parts.minutes * 0.5 + parts.seconds / 120;
                    const minuteDeg = parts.minutes * 6 + parts.seconds * 0.1;
                    const secondDeg = parts.seconds * 6;
                    this.hourHand.style.transform = `translateX(-50%) rotate(${hourDeg}deg)`;
                    this.minuteHand.style.transform = `translateX(-50%) rotate(${minuteDeg}deg)`;
                    this.secondHand.style.transform = `translateX(-50%) rotate(${secondDeg}deg)`;
                    if (parts.seconds !== this.lastDigitalSecond) {
                        this.digitalTime.textContent = `${String(parts.hours).padStart(2, '0')}:${String(parts.minutes).padStart(2, '0')}`;
                        this.digitalSeconds.textContent = `${String(parts.seconds).padStart(2, '0')}`;
                        this.lastDigitalSecond = parts.seconds;
                    }
                    const dateKey = parts.dateText + parts.weekdayText;
                    if (dateKey !== this.lastDateKey) {
                        this.dateDisplay.textContent = parts.dateText;
                        this.weekdayDisplay.textContent = parts.weekdayText;
                        this.lastDateKey = dateKey;
                    }
                    requestAnimationFrame(() => this.updateLoop());
                }
				
            }

            function applyTheme(themeId, notify) {
                const targetId = themeId || themes[0].id;
                const selected = themes.find(t => t.id === targetId) || themes[0];
                Object.keys(selected.vars).forEach(key => root.style.setProperty(key, selected.vars[key]));
                document.querySelectorAll('.theme-swatch').forEach(item => item.classList.toggle('active', item.dataset.theme === selected.id));
                localStorage.setItem('clock-theme', selected.id);
                document.querySelector('meta[name="theme-color"]').setAttribute('content', selected.vars['--bg-start']);
            }

            function openThemePanel() {
                themePanel.classList.add('open');
                themePanel.setAttribute('aria-hidden', 'false');
                themeButton.setAttribute('aria-expanded', 'true');
            }
            function closeThemePanel() {
                themePanel.classList.remove('open');
                themePanel.setAttribute('aria-hidden', 'true');
                themeButton.setAttribute('aria-expanded', 'false');
            }
            function updateDigitalVisibility() {
                body.classList.toggle('hidden-digital', !showDigital);
                toggleDigitalButton.innerText = showDigital ? '🕒 隐藏数字时间' : '🕒 显示数字时间';
                localStorage.setItem('clock-show-digital', String(showDigital));
            }
            async function toggleFullscreen() {
                try {
                    if (!document.fullscreenElement) await document.documentElement.requestFullscreen();
                    else await document.exitFullscreen();
                } catch (e) {
                    showToast('当前浏览器暂不支持全屏操作');
                }
            }
            function updateFullscreenState() {
				const isFullscreen = !!document.fullscreenElement;
				body.classList.toggle('fullscreen-mode', isFullscreen);
				fullscreenButton.setAttribute('aria-label', isFullscreen ? '退出全屏' : '全屏显示');
				fullscreenButton.title = isFullscreen ? '退出全屏' : '全屏显示';
				if (isFullscreen) {
					globalControls.classList.add('hidden');
				} else {
					globalControls.classList.remove('hidden');
				}
			}
			document.addEventListener('keydown', e => {
				if (e.key === 'F11') {
					e.preventDefault();
					toggleFullscreen();
				}
			});
            function showToast(message) {
                clearTimeout(toastTimer);
                toast.textContent = message;
                toast.classList.add('show');
                toastTimer = setTimeout(() => toast.classList.remove('show'), 1800);
            }
            function renderThemeOptions() {
                const fragment = document.createDocumentFragment();
                themes.forEach(theme => {
                    const button = document.createElement('button');
                    button.type = 'button';
                    button.className = 'theme-swatch';
                    button.dataset.theme = theme.id;
                    button.setAttribute('aria-label', `切换到${theme.name}主题`);
                    button.innerHTML = `<span class="swatch-preview" style="--swatch:${theme.swatch}"></span><span class="swatch-name">${theme.name}</span>`;
                    button.addEventListener('click', () => {
                        applyTheme(theme.id, true);
                        closeThemePanel();
                    });
                    fragment.appendChild(button);
                });
                themeGrid.appendChild(fragment);
            }
            themeButton.addEventListener('click', () => themePanel.classList.contains('open') ? closeThemePanel() : openThemePanel());
            fullscreenButton.addEventListener('click', toggleFullscreen);
			toggleDigitalButton.addEventListener('click', () => {
                showDigital = !showDigital;
                updateDigitalVisibility();
            });
            document.addEventListener('fullscreenchange', updateFullscreenState);
            document.addEventListener('click', e => {
                if (!themePanel.contains(e.target) && !themeButton.contains(e.target)) closeThemePanel();
            });
            function refreshAllClockMarks(clockInstanceList) {
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(() => {
                    clockInstanceList.forEach(ins => ins.createClockMarks());
                }, 120);
            }
            renderClockCards();
            renderThemeOptions();
            const savedTheme = localStorage.getItem('clock-theme');
            applyTheme(savedTheme, false);
            updateDigitalVisibility();
            updateFullscreenState();
            copyrightYear.textContent = '';
            const clockInstances = [];
            clockTimezoneConfig.forEach(cfg => {
                const ins = new SingleTimezoneClock(cfg.zone, cfg.id, cfg.label);
                clockInstances.push(ins);
            })
            window.addEventListener('resize', () => refreshAllClockMarks(clockInstances));
			function fillZoneSelect() {
				addZone.innerHTML = '';
				timezoneList.forEach(item => {
					const opt = document.createElement('option');
					opt.value = item.zone;
					opt.textContent = item.label;
					addZone.appendChild(opt);
				});
			}
			fillZoneSelect();
			addClockBtn.addEventListener('click', () => {
				const selectedVal = addZone.value;
				const targetZone = timezoneList.find(item => item.zone === selectedVal);
				if (!targetZone) return;
				const id = targetZone.id;
				const selectedLabel = targetZone.label;
				const exist = clockTimezoneConfig.find(cfg => cfg.id === id);
				if (exist) {
					showToast('该城市时钟已存在');
					return;
				}
				clockTimezoneConfig.push({
					id: id,
					label: selectedLabel,
					zone: selectedVal
				});
				const delIndex = timezoneList.findIndex(i => i.zone === selectedVal);
				if (delIndex > -1) {
					timezoneList.splice(delIndex, 1);
				}
				fillZoneSelect();
				clockInstances.length = 0;
				renderClockCards();
				clockTimezoneConfig.forEach(cfg => {
					const ins = new SingleTimezoneClock(cfg.zone, cfg.id, cfg.label);
					clockInstances.push(ins);
				});
			});
        });
    </script>
</body>
</html>
posted @ 2026-08-09 17:23  钢锅  阅读(3)  评论(0)    收藏  举报