本帖最后由 梦素 于 2025-5-27 15:59 编辑
保存成html文件,打开就行,也可以用Wallpaper Engine的添加自定义html功能把它设为桌面背景。
放到Wallpaper Engine试了下,发现始终居中有点笨,而且太大了,把组件缩小了,增加半透明模糊并且使得它可拖动,配合Wallpaper Engine用起来有MacOS桌面组件那味道了
效果:

代码:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Progress Tracker</title>
- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
- <style>
- :root {
- --top-color: #6A6A6F;
- --bottom-color: #101014;
- }
- body {
- background: linear-gradient(to bottom, var(--top-color), var(--bottom-color));
- color: white;
- font-family: 'PingFang SC', 'PingFang TC', 'San Francisco', 'MiSans', sans-serif;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100vh;
- margin: 0;
- }
- .section-box {
- position: absolute;
- z-index: 1;
- background: rgba(17, 17, 17, 0.8);
- border-radius: 1rem;
- padding: 14px;
- margin-bottom: 10px;
- text-align: center;
- user-select: none;
- backdrop-filter: blur(4px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
- }
- #dots-container {
- display: grid;
- grid-template-columns: repeat(12, 16px);
- grid-gap: 7px;
- margin-bottom: 8px;
- }
- .dot {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: rgba(48, 48, 48, 0.5);
- }
- .dot.active {
- background: rgb(78, 255, 55);
- }
- .dot.partial {
- background: rgb(255, 230, 0);
- }
- #dots-remaining {
- color: rgba(130, 130, 130, 0.9);
- ;
- font-size: 12px;
- margin-top: 5px;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
- .circle-group {
- display: flex;
- gap: 8px;
- justify-content: center;
- align-items: center;
- height: 80px;
- }
- .circle {
- position: relative;
- bottom: 10px;
- width: 60px;
- height: 60px;
- }
- .circle svg {
- transform: rotate(-90deg);
- }
- .circle-label {
- text-align: center;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- font-size: 14px;
- font-weight: normal;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
- .circle-percent {
- text-align: center;
- position: absolute;
- bottom: -20px;
- left: 50%;
- transform: translateX(-50%);
- font-size: 12px;
- font-weight: normal;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
- @keyframes fillRing {
- 0% {
- stroke-dashoffset: 251.2;
- }
- 100% {
- stroke-dashoffset: var(--final-offset);
- }
- }
- @keyframes dotWave {
- 0% {
- transform: scale(0.6);
- opacity: 0.3;
- }
- 50% {
- transform: scale(1.2);
- opacity: 1;
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
- .dot.animate {
- animation: dotWave 0.8s ease-out forwards;
- }
- </style>
- </head>
- <body>
- <div class="section-box" style="top: 50px; left: 80px;">
- <div id="dots-container"></div>
- <div id="dots-remaining"></div>
- </div>
- <div class="section-box" style="top: 50px; left: 180px;">
- <div class="circle-group">
- <div class="circle" data-unit="day" data-color="#DAAF00" data-bg="#262004"></div>
- <div class="circle" data-unit="week" data-color="#906F4C" data-bg="#1F1B10"></div>
- <div class="circle" data-unit="month" data-color="#E1790A" data-bg="#1A0C0B"></div>
- <div class="circle" data-unit="year" data-color="#E61F22" data-bg="#26130C"></div>
- </div>
- </div>
- <script>
- let lastDotState = '';
- let zCounter = 100;
- function createDots(firstLoad = false) {
- const now = new Date();
- const minsPassed = now.getHours() * 60 + now.getMinutes();
- const currentDot = Math.floor(minsPassed / 10);
- const totalRows = 12;
- const totalCols = 12;
- const newDotState = currentDot;
- const shouldAnimate = firstLoad || newDotState !== lastDotState;
- lastDotState = newDotState;
- $("#dots-container").empty();
- for (let row = totalRows - 1; row >= 0; row--) {
- for (let col = totalCols - 1; col >= 0; col--) {
- const index = row * totalCols + col;
- let cls = 'dot';
- if (index < currentDot) cls += ' active';
- else if (index === currentDot) cls += ' partial';
- const animateClass = shouldAnimate ? ' animate' : '';
- const $dot = $(`<div class="${cls}" data-index="${index}" style="animation-delay: ${(row + col) * 20}ms"></div>`);
- $("#dots-container").append($dot);
- }
- }
- const remaining = 1440 - minsPassed;
- $("#dots-remaining").text(`本日还剩 ${remaining} 分钟`);
- }
- function renderCircle($el, percent, label, color, bg, animate = false) {
- const radius = 25;
- const circumference = 2 * Math.PI * radius;
- const offset = circumference * (1 - percent);
- let circleMarkup = animate ?
- `<circle cx="30" cy="30" r="25" stroke="${color}" stroke-width="6" fill="none" stroke-linecap="round" stroke-dasharray="${circumference}" stroke-dashoffset="0">
- <animate attributeName="stroke-dashoffset" from="${circumference}" to="${offset}" dur="1s" fill="freeze" />
- </circle>` :
- `<circle cx="30" cy="30" r="25" stroke="${color}" stroke-width="6" fill="none" stroke-linecap="round" stroke-dasharray="${circumference}" stroke-dashoffset="${offset}" />`;
- const svg = `
- <svg width="60" height="60">
- <circle cx="30" cy="30" r="25" stroke="${bg}" stroke-width="6" fill="none" />
- ${circleMarkup}
- </svg>
- <div class="circle-label" style="color: ${color};">${label}</div>
- <div class="circle-percent" style="color: ${color};">${(percent * 100).toFixed(1)}%</div>
- `;
- $el.html(svg);
- }
- function updateProgressCircles(animate = false) {
- const now = new Date();
- const startOfYear = new Date(now.getFullYear(), 0, 1);
- const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
- const startOfWeek = new Date(now);
- startOfWeek.setDate(now.getDate() - now.getDay());
- const startOfDay = new Date(now);
- startOfDay.setHours(0, 0, 0, 0);
- const totalDay = 24 * 60 * 60 * 1000;
- const totalWeek = 7 * totalDay;
- const totalMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate() * totalDay;
- const totalYear = new Date(now.getFullYear() + 1, 0, 1) - new Date(now.getFullYear(), 0, 1);
- const elapsedDay = now - startOfDay;
- const elapsedWeek = now - startOfWeek;
- const elapsedMonth = now - startOfMonth;
- const elapsedYear = now - startOfYear;
- $(".circle").each(function () {
- const unit = $(this).data("unit");
- const color = $(this).data("color");
- const bg = $(this).data("bg");
- let percent = 0;
- let label = '';
- switch (unit) {
- case 'day': percent = elapsedDay / totalDay; label = '日'; break;
- case 'week': percent = elapsedWeek / totalWeek; label = '周'; break;
- case 'month': percent = elapsedMonth / totalMonth; label = '月'; break;
- case 'year': percent = elapsedYear / totalYear; label = '年'; break;
- }
- renderCircle($(this), percent, label, color, bg, animate);
- });
- }
- function triggerDotWave(centerIndex) {
- const totalCols = 12;
- const centerRow = Math.floor(centerIndex / totalCols);
- const centerCol = centerIndex % totalCols;
- $("#dots-container .dot").each(function () {
- const index = parseInt($(this).data("index"));
- const row = Math.floor(index / totalCols);
- const col = index % totalCols;
- const dx = row - centerRow;
- const dy = col - centerCol;
- const distance = Math.sqrt(dx * dx + dy * dy);
- $(this).removeClass("animate"); // 重置动画
- void this.offsetWidth; // 强制重绘
- $(this).addClass("animate").css("animation-delay", `${distance * 40}ms`);
- });
- }
- function makeDraggable($el) {
- let isDragging = false;
- let offsetX = 0;
- let offsetY = 0;
- $el.on("mousedown", function (e) {
- isDragging = true;
- const pos = $el.offset();
- offsetX = e.pageX - pos.left;
- offsetY = e.pageY - pos.top;
- $el.css("z-index", ++zCounter);
- e.preventDefault();
- });
- $(document).on("mousemove", function (e) {
- if (isDragging) {
- $el.css({
- left: e.pageX - offsetX,
- top: e.pageY - offsetY
- });
- }
- });
- $(document).on("mouseup", function () {
- isDragging = false;
- });
- }
- $(".section-box").each(function () {
- makeDraggable($(this));
- });
- $(document).on("click", ".dot", function () {
- const index = parseInt($(this).data("index"));
- triggerDotWave(index);
- });
- $(function () {
- function refreshPage(firstLoad = false) {
- createDots(firstLoad);
- updateProgressCircles(firstLoad);
- }
- refreshPage(true);
- setInterval(() => refreshPage(false), 15000);
- });
- </script>
- </body>
- </html>
复制代码 |