Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Summary
- [ ] 确认改动范围和目标
- [ ] 关键用户路径已自测,必要时补充截图/录屏
- [ ] 相关文档、配置、脚本已更新

## Testing
请勾选已执行的检查:
- [ ] `npm test`
- [ ] 其他:____

## 额外说明
如有风险、回滚方案或部署注意事项,请在此描述。
28 changes: 23 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@ on:
branches: [ main ]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test on Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm test
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --runInBand
- name: Validate deploy bundle can be prepared
run: npm run prepare:deploy
48 changes: 48 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Deploy static site

on:
push:
branches: [ main ]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --runInBand
- name: Prepare static bundle
run: npm run prepare:deploy
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
36 changes: 36 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Semantic PR title

on:
pull_request:
types: [opened, edited, reopened, synchronize, ready_for_review]

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
chore
ci
docs
feat
fix
perf
refactor
revert
style
test
scopes: |
ci
deps
docs
ui
gameplay
tooling
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ serve .
npm install
npm test
```
- 提交到仓库后,GitHub Actions 会自动执行 `npm test`,确保自适应机制与新手引导等关键逻辑保持稳定。
- GitHub Actions 会在 Node.js 18 与 20 上并行运行测试,并验证部署包可被正确产出(`npm run prepare:deploy`)。
- 推送到 `main` 将自动触发 Pages 部署流程,生成的静态资源位于 `dist/` 目录。

### 部署与打包

- 运行 `npm run prepare:deploy` 将核心静态资源复制到 `dist/`,供静态托管或 CDN 发布。
- GitHub Pages 工作流会复用该目录进行自动发布,默认包含 `index.html`、`app.js`、`sw.js` 与 `manifest.webmanifest`。

## 📄 许可证

Expand Down
35 changes: 34 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ const lettersPool = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65
const shapesPool = ['▲','■','●','◆','★','⬤','⬟','⬢','⬣','⬥','◼','◻','◾','◽','▣','▧','▨','✦','✧','✪','✸','✹','✤','✥','⬠','⬡'];
const colorsPool = ['#EF4444','#F97316','#F59E0B','#84CC16','#22C55E','#10B981','#06B6D4','#3B82F6','#6366F1','#8B5CF6','#A855F7','#EC4899','#F43F5E','#14B8A6','#EAB308','#0EA5E9','#4ADE80','#FB7185','#34D399','#60A5FA','#D946EF','#F59E0B','#22C55E'];

function logLifecycle(event, detail = {}) {
try {
console.info(`[Remember] ${event}`, detail);
} catch (_) {
// eslint-disable-line no-empty
}
}

function logError(event, detail = {}) {
try {
console.error(`[Remember] ${event}`, detail);
} catch (_) {
// eslint-disable-line no-empty
}
}

function getAccent() {
const a = settings.accent || 'indigo';
return ACCENTS[a] || ACCENTS.indigo;
Expand Down Expand Up @@ -699,6 +715,12 @@ function initGame(diffKey) {
movesEl.textContent = "0";
updateBestUI();
const assist = getAdaptiveAssist(currentDifficulty);
logLifecycle('init_game', {
difficulty: currentDifficulty,
adaptive: !!settings.adaptive,
previewSeconds: assist.previewSec,
hintLimit: assist.hintLimit,
});
hintsLeft = assist.hintLimit || 0;
hintsUsed = 0;
paused = false;
Expand Down Expand Up @@ -739,6 +761,14 @@ function onWin() {
updateBestUI();
winStatsEl.textContent = `用时 ${formatTime(elapsed)} · ${moves} 步`;
const stars = getRating(elapsed, moves, currentDifficulty, hintsUsed, maxComboThisGame);
logLifecycle('game_win', {
difficulty: currentDifficulty,
elapsed,
moves,
stars,
hintsUsed,
maxCombo: maxComboThisGame,
});
renderRating(stars);
winModal.classList.remove("hidden");
winModal.classList.add("flex");
Expand Down Expand Up @@ -771,6 +801,7 @@ function onTimeUp() {
timeUp = true;
lockBoard = true;
paused = true;
logLifecycle('time_up', { difficulty: currentDifficulty, elapsed, moves });
if (loseModal) { loseModal.classList.remove('hidden'); loseModal.classList.add('flex'); }
sfx('mismatch');
vibrateMs(100);
Expand Down Expand Up @@ -1036,7 +1067,9 @@ if (typeof document !== 'undefined') {
const mqlReduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
if (mqlReduce && mqlReduce.addEventListener) mqlReduce.addEventListener('change', () => { if ((settings.motion || 'auto') === 'auto') applyMotionPreference(); });
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(() => {});
navigator.serviceWorker.register('./sw.js')
.then((reg) => logLifecycle('service_worker_registered', { scope: reg.scope }))
.catch((err) => logError('service_worker_registration_failed', { message: err?.message }));
}

initGame(currentDifficulty);
Expand Down
Loading