diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..bba0fcf --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,54 @@ +name: Deploy playground demo # 工作流的名称 + +on: # 触发条件 + push: + branches: + - main # 当 main 分支收到推送时触发 + workflow_dispatch: # 允许手动触发 + +permissions: # GitHub token 的权限设置 + contents: read # 读取仓库内容 + pages: write # 写入 GitHub Pages + id-token: write # 写入身份令牌 + +concurrency: # 并发控制 + group: pages # 同一时间只允许一个部署任务运行 + cancel-in-progress: true # 如果有新的部署,取消正在进行的部署 + +jobs: + deploy: + environment: # 环境配置 + name: github-pages # 环境名称 + url: ${{ steps.deployment.outputs.page_url }} # 部署后的 URL + + runs-on: ubuntu-latest # 运行环境 + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + run_install: false + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: pnpm + + - run: pnpm i + - run: pnpm playground:build + + # 添加缓存验证步骤 + - name: Verify build + run: | + if [ ! -d "playground/dist" ]; then + echo "Playground build failed - dist directory not found" + exit 1 + fi + + - uses: actions/configure-pages@v5 + with: + enablement: true + - uses: actions/upload-pages-artifact@v3 + with: + path: playground/dist # 指定要部署的目录 + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/README.md b/README.md index 09d5557..5ed29b4 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ const timer = pausableTimers(() => { timer.pause() timer.resume() timer.clear() -timer.reset() +timer.restart() timer.isPaused() timers.getRemainingTime() timer.isCompleted() // only for timeout mode diff --git a/package.json b/package.json index 67fc530..305b2bc 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ "scripts": { "build": "unbuild", "dev": "unbuild --stub", + "playground:dev": "vite playground", + "playground:build": "vite build playground", + "playground:preview": "vite preview playground", "lint": "eslint .", "prepublishOnly": "automd && nr build", "release": "bumpp && pnpm publish", diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000..d472260 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + +
+
+

+ +
+
+
+
+
+
+
+ +
+
+ + + +
+ +
+ + + + +
+
+
+ + + diff --git a/playground/index.ts b/playground/index.ts deleted file mode 100644 index ffd5f97..0000000 --- a/playground/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable antfu/no-top-level-await */ -/* eslint-disable no-console */ -import { pausableTimers } from '../src/index' - -const timer = pausableTimers(() => { - console.timeEnd('timeout timer') - - console.time('timeout timer') -}, 5000, { - mode: 'interval', -}) - -// remain 3s -await delay(2000) -timer.pause() -timer.resume() - -// remain 1s -await delay(2000) -timer.pause() -timer.resume() - -console.time('timeout timer') - -function delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) -} diff --git a/playground/main.js b/playground/main.js new file mode 100644 index 0000000..104b0ef --- /dev/null +++ b/playground/main.js @@ -0,0 +1,180 @@ +import pkg from '../package.json' +import { pausableTimers } from '../src/index' + +// 设置页面标题和项目信息 +document.title = `${pkg.name} | Playground` +document.querySelector('.project-info h1').textContent = pkg.name +document.querySelector('.version').textContent = `v${pkg.version}` + +const elements = { + progressBar: document.querySelector('.progress-bar'), + statusText: document.querySelector('.status-text'), + timeInput: document.querySelector('#timeInput'), + modeSelect: document.querySelector('#modeSelect'), + startBtn: document.querySelector('#startBtn'), + pauseBtn: document.querySelector('#pauseBtn'), + clearBtn: document.querySelector('#clearBtn'), + restartBtn: document.querySelector('#restartBtn'), +} + +let timer = null +let animationFrameId = null +let cycleCount = 0 + +function updateProgress(remaining, total) { + const progress = (remaining / total) * 100 + const progressBar = elements.progressBar + + // 完全移除过渡效果 + progressBar.style.transition = 'none' + progressBar.style.width = `${progress}%` + + if (remaining === total) { + // 当需要重置到 100% 时,保持 none + return + } + + // 确保 DOM 更新后再添加过渡效果 + requestAnimationFrame(() => { + progressBar.style.transition = 'width 0.1s linear' + }) +} + +function updateButtons(isRunning) { + elements.startBtn.disabled = isRunning + elements.pauseBtn.disabled = !isRunning + elements.clearBtn.disabled = !isRunning + elements.restartBtn.disabled = !isRunning + elements.timeInput.disabled = isRunning + elements.modeSelect.disabled = isRunning +} + +function animate(total) { + if (!timer) + return + + const remaining = timer.getRemainingTime() + updateProgress(remaining, total) + + // 根据模式显示不同的状态信息 + if (elements.modeSelect.value === 'interval') { + elements.statusText.textContent = `Cycle ${cycleCount} - Remaining: ${remaining}ms` + } + else { + elements.statusText.textContent = `Remaining: ${remaining}ms` + } + + if (timer.isCompleted()) { + elements.statusText.textContent = 'Completed!' + updateButtons(false) + return + } + + animationFrameId = requestAnimationFrame(() => animate(total)) +} + +function startTimer() { + const delay = Number.parseInt(elements.timeInput.value) + // 添加输入值检查 + if (delay < 100) { + elements.statusText.textContent = 'Please enter a value greater than 100ms' + return + } + + const mode = elements.modeSelect.value + + // 先清理现有的计时器和动画 + if (timer) { + timer.clear() + } + if (animationFrameId) { + cancelAnimationFrame(animationFrameId) + animationFrameId = null + } + + elements.statusText.textContent = '' + timer = pausableTimers(() => { + if (mode === 'timeout') { + cancelAnimationFrame(animationFrameId) + updateProgress(0, delay) + elements.statusText.textContent = 'Completed!' + updateButtons(false) + } + else { + // 在 interval 模式下,先取消动画帧 + cancelAnimationFrame(animationFrameId) + // 立即重置进度条到 100%(无动画) + updateProgress(delay, delay) + // 在 interval 模式下添加循环次数显示 + cycleCount++ + elements.statusText.textContent = `Cycle ${cycleCount}` + // 立即开始新的倒计时(有动画) + requestAnimationFrame(() => animate(delay)) + } + }, delay, { mode }) + + updateButtons(true) + // 启动时也应用相同的逻辑 + updateProgress(delay, delay) + requestAnimationFrame(() => animate(delay)) +} + +// Event Listeners +elements.startBtn.addEventListener('click', startTimer) + +elements.pauseBtn.addEventListener('click', () => { + if (!timer) + return + if (timer.isPaused()) { + timer.resume() + elements.pauseBtn.textContent = 'Pause' + animate(Number.parseInt(elements.timeInput.value)) + } + else { + timer.pause() + elements.pauseBtn.textContent = 'Resume' + cancelAnimationFrame(animationFrameId) + } +}) + +elements.clearBtn.addEventListener('click', () => { + if (!timer) + return + timer.clear() + // 确保动画帧被取消 + if (animationFrameId) { + cancelAnimationFrame(animationFrameId) + animationFrameId = null + } + updateProgress(100, 100) + elements.statusText.textContent = '' + updateButtons(false) + elements.pauseBtn.textContent = 'Pause' + cycleCount = 0 +}) + +elements.restartBtn.addEventListener('click', () => { + if (!timer) + return + // 先清理现有的动画帧 + if (animationFrameId) { + cancelAnimationFrame(animationFrameId) + animationFrameId = null + } + timer.restart() + elements.pauseBtn.textContent = 'Pause' + updateProgress(Number.parseInt(elements.timeInput.value), Number.parseInt(elements.timeInput.value)) + animate(Number.parseInt(elements.timeInput.value)) + cycleCount = 0 +}) + +// 添加输入框验证 +elements.timeInput.addEventListener('input', (e) => { + const value = Number.parseInt(e.target.value) + if (value < 100) { + e.target.setCustomValidity('Please enter a value greater than 100ms') + } + else { + e.target.setCustomValidity('') + } +}) diff --git a/playground/style.css b/playground/style.css new file mode 100644 index 0000000..1261a0d --- /dev/null +++ b/playground/style.css @@ -0,0 +1,155 @@ +:root { + --primary: #F9B208; + --primary-light: #FFD93D; + --primary-dark: #F48B29; + --background: #FCF5ED; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: var(--background); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; +} + +.container { + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 500px; +} + +.timer-display { + margin-bottom: 2rem; +} + +.progress-container { + background: #eee; + height: 20px; + border-radius: 10px; + overflow: hidden; + margin-bottom: 1rem; +} + +.progress-bar { + height: 100%; + background: var(--primary); + width: 100%; + transition: width 0.1s linear; +} + +.status-text { + text-align: center; + color: var(--primary-dark); + font-weight: bold; + min-height: 24px; +} + +.control-panel { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.input-group { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.input-group input { + padding: 0.5rem; + border: 2px solid var(--primary); + border-radius: 4px; + width: 120px; + /* 修复 Edge 浏览器下的数字输入框样式 */ + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 1; + height: 24px; + } + &::-ms-clear { + display: none; + } +} + +.input-group select { + padding: 0.5rem; + border: 2px solid var(--primary); + border-radius: 4px; +} + +.button-group { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + background: var(--primary); + color: white; + cursor: pointer; + flex: 1; + min-width: 100px; +} + +button:hover { + background: var(--primary-dark); +} + +button:disabled { + background: #ccc; + cursor: not-allowed; +} + +/* GitHub Corner 动画 */ +.github-corner:hover .octo-arm { + animation: octocat-wave 560ms ease-in-out; +} + +@keyframes octocat-wave { + 0%, 100% { transform: rotate(0) } + 20%, 60% { transform: rotate(-25deg) } + 40%, 80% { transform: rotate(10deg) } +} + +@media (max-width: 500px) { + .github-corner:hover .octo-arm { + animation: none; + } + .github-corner .octo-arm { + animation: octocat-wave 560ms ease-in-out; + } +} + +.project-info { + text-align: center; + margin-bottom: 2rem; + color: var(--primary-dark); +} + +.project-info h1 { + margin: 0; + font-size: 2rem; +} + +.version { + font-size: 0.9rem; + opacity: 0.8; + line-height: 1.4; +} + diff --git a/playground/vite.config.js b/playground/vite.config.js new file mode 100644 index 0000000..3849afd --- /dev/null +++ b/playground/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import pkg from '../package.json' + +function getRepoName(url) { + const match = url.match(/github\.com\/[\w-]+\/([\w-]+)/) + return match ? `/${match[1]}/` : '/' +} + +export default defineConfig({ + base: getRepoName(pkg.repository.url), +})