双时区时钟, 可自定义时区
样式:

<!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>

浙公网安备 33010602011771号