关于 Web components
科普非我所长,大家可以参考这里:Web Components | MDN (mozilla.org)
我的小尝试
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <title>Digi Clock</title> <style> html, body { height: 100%; padding: 0; margin: 0; } body { display: flex; justify-content: center; align-items: center; } .bg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; background-image: url(R0000555.jpg); background-size: cover; opacity: .5; } </style> </head> <body> <div class="bg"></div> <digi-clock /> </body> </html> <script> class DigiClock extends HTMLElement { constructor() { super(); this.shadowDOM = this.attachShadow({ mode: 'open' }); this.init = false; this.initStyle(); this.setupState(); this.initVDOM(); requestAnimationFrame(() => this.render()); setInterval(() => { this.state.dt = new Date() }, 100); } getDateInfo(key, arr) { return arr.filter((ele) => ele.type === key)[0].value; } updateText(val) { const info = new Intl.DateTimeFormat("en", { hour: "2-digit", minute: "2-digit", hour12: true, weekday: "long", day: "2-digit", month: "long", }).formatToParts(val); this.state.hour = this.getDateInfo("hour", info); this.state.min = this.getDateInfo("minute", info); this.state.weekday = this.getDateInfo("weekday", info); this.state.dayPeriod = this.getDateInfo("dayPeriod", info); this.state.day = this.getDateInfo("day", info); this.state.month = this.getDateInfo("month", info); } updateHourMinutePanel(val) { const min = val.getMinutes(); let hour = val.getHours(); const sec = val.getSeconds(); if (hour >= 12) { hour -= 12; } const hourDeg = hour * 30 (min / 60) * 30; this.state.hourStyle = { transform: `rotate(${hourDeg}deg)`, }; const minDeg = min * 6 (sec / 60) * 6; this.state.minStyle = { transform: `rotate(${minDeg}deg)`, }; }; updateSecondPanel(val) { const min = val.getMinutes(); const sec = val.getSeconds(); const temp = Array(60).fill(false); for (const secKey in this.state.secondCtrl) { if (min % 2) { temp[secKey] = secKey <= sec; } else { temp[secKey] = secKey > sec; } } this.state.secondCtrl = temp; }; setupState() { this.dep = []; this.depResult = {}; this.waitForRender = []; this.watcher = { dt: () => { this.updateSecondPanel(this.state.dt); this.updateHourMinutePanel(this.state.dt); this.updateText(this.state.dt); } } this.state = new Proxy({ dt: null, secondCtrl: Array(60).fill(false), hour: '', min: '', dayPeriod: '', hourStyle: {}, minStyle: {}, weekday: '', day: '', month: '' }, { set: (state, prop, value) => { if (state[prop] === value) { return true; } state[prop] = value; this.watcher[prop] && this.watcher[prop](); this.dep.forEach(ele => { if (ele.depList.includes(prop)) { this.depResult[ele.id] = ele.func(state); this.waitForRender.push(ele.id); } }); return true; } }); } initStyle() { const style = document.createElement('style'); style.textContent = ` .digi-clock { width: 400px; height: 400px; position: relative; } .digi-clock > .second-panel { width: 380px; height: 380px; top: 10px; left: 10px; position: absolute; z-index: 0; } .digi-clock > .second-panel > .second-box { width: 10px; height: 30px; border: 1px solid rgba(85, 102, 119, 1); position: absolute; top: 175px; left: 185px; transition: background 0.3s linear; background: rgba(85, 102, 119, 0); } .digi-clock > .second-panel > .second-box.solid { background-color: rgba(85, 102, 119, 1); } .digi-clock > .second-panel > .second-box.sp { background-color: rgba(17, 34, 51, 0); border: 2px solid rgba(17, 34, 51, 1); } .digi-clock > .second-panel > .second-box.sp.solid { background-color: rgba(17, 34, 51, 1) !important; } .digi-clock > .hour-minute-panel { position: absolute; width: 100%; height: 100%; z-index: 1; } .digi-clock > .hour-minute-panel > .hour, .digi-clock > .hour-minute-panel > .minute { transform-origin: bottom; position: absolute; } .digi-clock > .hour-minute-panel > .hour { width: 4px; height: 120px; background-color: #781138; top: 80px; left: 198px; box-shadow: 2px -2px 3px 2px #555; } .digi-clock > .hour-minute-panel > .minute { width: 2px; height: 150px; background-color: #035373; top: 50px; left: 199px; box-shadow: 3px -2px 3px 2px #555; } .digi-clock > .hour-minute-panel > .dot { position: absolute; width: 20px; height: 20px; background: #000; border-radius: 10px; top: 0; left: 0; right: 0; bottom: 0; margin: auto; } .digi-clock > .date-text { position: absolute; width: 100%; height: 100%; z-index: 2; font-family: "Oxanium"; } .digi-clock > .date-text > .dt { font-size: 2.5em; height: 38px; width: 103px; line-height: 45px; text-align: center; position: absolute; left: 0px; bottom: 0px; } .digi-clock > .date-text > .dp { font-size: 1.4em; position: absolute; bottom: 0px; left: 105px; line-height: 0.9em; } .digi-clock > .date-text > .dd { position: absolute; bottom: 0px; right: 0px; font-size: 1.3em; } .digi-clock > .date-text > .dm { position: absolute; right: 0; bottom: 30px; font-size: 1.6em; } `; this.shadowDOM.appendChild(style); } createDep(func, depList) { const s = this.dep.length; const id = Symbol(`dep_${s}`); this.dep.push({ func, depList, id }); this.depResult[id] = func(this.state); return id; } initVDOM() { const radius = 175; const secondBoxStyle = Array(60) .fill() .map((...args) => { const s = args[1]; const ang = s * 6; const xArc = this.angleToArc(ang); const yArc = this.angleToArc(90 - ang); let x = radius * Math.sin(xArc); let y = radius * Math.sin(yArc); return { transform: `rotate(${ang}deg) translateZ(0)`, top: (y - radius) * -1 "px", left: x radius 10 "px", }; }); this.vDOM = { className: ['digi-clock'], children: [ { className: ['second-panel'], children: Array(60).fill().map((ele, index) => { const obj = { style: secondBoxStyle[index], className: [ 'second-box', this.createDep(state => state.secondCtrl[index] ? 'solid' : '', ['secondCtrl']) ] }; if ([0, 15, 30, 45].includes(index)) { obj.className.push('sp'); } return obj; }) }, { className: ['hour-minute-panel'], children: [ { className: ['hour'], style: this.createDep(state => state.hourStyle, ['hourStyle']) }, { className: ['minute'], style: this.createDep(state => state.minStyle, ['minStyle']) }, { className: ['dot'] } ] }, { className: ['date-text'], children: [ { className: ['dt'], content: this.createDep( ({ hour, min }) => `${hour}:${min}`, ['hour', 'min'] ) }, { className: ['dp'], content: this.createDep(state => state.dayPeriod, ['dayPeriod']) }, { className: ['dd'], content: this.createDep( ({ day, weekday }) => `${day} ${weekday}`, ['day', 'weekday'] ) }, { className: ['dm'], content: this.createDep(state => state.month, ['month']) }, ] } ] } this.domMaping = {} } angleToArc(angle) { return (angle * Math.PI) / 180; } renderElement(config, target) { const element = target || document.createElement('div'); if (config.className) { const classList = config.className.map(ele => { if (typeof ele === 'symbol') { if (!target) this.domMaping[ele] = { element, config }; return this.depResult[ele]; } return ele; }).join(' '); element.className = classList; } if (config.style) { let target; if (typeof config.style === 'symbol') { if (!target) this.domMaping[config.style] = { element, config }; target = this.depResult[config.style]; } else { target = config.style; } Object.keys(target).forEach(keyName => { element.style[keyName] = target[keyName]; }) } if (config.content) { if (typeof config.content === 'symbol') { if (!target) this.domMaping[config.content] = { element, config }; element.innerHTML = this.depResult[config.content]; } else { element.innerHTML = config.content; } } if (!target && config.children) { config.children.forEach(ele => { element.appendChild(this.renderElement(ele)); }); } return element } render() { if (!this.init) { const element = this.renderElement(this.vDOM); this.shadowDOM.appendChild(element); this.init = true; requestAnimationFrame(() => this.render()); return; } if (this.waitForRender.length === 0) { requestAnimationFrame(() => this.render()); return; } this.waitForRender.forEach(ele => { this.renderElement(this.domMaping[ele].config, this.domMaping[ele].element); }) this.waitForRender.length = 0; requestAnimationFrame(() => this.render()); } } customElements.define('digi-clock', DigiClock); </script>