├── favicon.ico
├── .gitignore
├── samples
├── piano-A3.mp3
├── piano-A4.mp3
├── piano-A5.mp3
├── piano-A6.mp3
├── piano-B3.mp3
├── piano-B4.mp3
├── piano-B5.mp3
├── piano-B6.mp3
├── piano-C3.mp3
├── piano-C4.mp3
├── piano-C5.mp3
├── piano-C6.mp3
├── piano-D3.mp3
├── piano-D4.mp3
├── piano-D5.mp3
├── piano-D6.mp3
├── piano-E3.mp3
├── piano-E4.mp3
├── piano-E5.mp3
├── piano-E6.mp3
├── piano-F3.mp3
├── piano-F4.mp3
├── piano-F5.mp3
├── piano-F6.mp3
├── piano-G3.mp3
├── piano-G4.mp3
├── piano-G5.mp3
└── piano-G6.mp3
├── .claude
└── settings.local.json
├── ads.txt
├── sounds
├── metronome
│ ├── click.mp3
│ ├── accent.mp3
│ ├── 34272_89860-lq.mp3
│ └── 34273_89860-lq.mp3
└── piano
│ ├── piano-a3.mp3
│ ├── piano-a4.mp3
│ ├── piano-a5.mp3
│ ├── piano-c3.mp3
│ ├── piano-c4.mp3
│ ├── piano-c5.mp3
│ ├── piano-ds3.mp3
│ ├── piano-ds4.mp3
│ ├── piano-ds5.mp3
│ ├── piano-fs3.mp3
│ ├── piano-fs4.mp3
│ └── piano-fs5.mp3
├── robots.txt
├── images
├── icons
│ ├── practice.svg
│ ├── basics.svg
│ └── notes.svg
├── favicon.svg
├── logo.svg
└── piano-placeholder.svg
├── package.json
├── download_samples.sh
├── scripts
├── download_samples.py
├── download_tonejs_samples.py
├── download_salamander.py
└── generate_samples.py
├── verify.js
├── llms.txt
├── test-audio.html
├── setup.js
├── README.md
├── js
├── main.js
├── tutorial.js
├── staff.js
├── audio-generator.js
├── practice.js
├── piano-audio.js
├── practice-songs.js
├── translations.js
└── recorder.js
├── styles.css
├── generate_samples.py
├── generate_samples.html
├── css
├── tutorials.css
├── article.css
├── practice.css
└── about.css
├── piano.css
├── TESTING_CHECKLIST.md
├── tutorial.html
├── sitemap.xml
├── IMPLEMENTATION_PLAN.md
├── SEO-OPTIMIZATION-GUIDE.md
├── script.js
├── QUICK-ACTION-CHECKLIST.md
├── tutorial-content.html
├── articles
└── tutorials
│ ├── finger-numbers.html
│ ├── reading-notes.html
│ ├── piano-basics.html
│ └── rhythm-basics.html
├── tutorials.html
├── practice.html
└── test-improvements.html
/favicon.ico:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 |
--------------------------------------------------------------------------------
/samples/piano-A3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A3.mp3
--------------------------------------------------------------------------------
/samples/piano-A4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A4.mp3
--------------------------------------------------------------------------------
/samples/piano-A5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A5.mp3
--------------------------------------------------------------------------------
/samples/piano-A6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A6.mp3
--------------------------------------------------------------------------------
/samples/piano-B3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B3.mp3
--------------------------------------------------------------------------------
/samples/piano-B4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B4.mp3
--------------------------------------------------------------------------------
/samples/piano-B5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B5.mp3
--------------------------------------------------------------------------------
/samples/piano-B6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B6.mp3
--------------------------------------------------------------------------------
/samples/piano-C3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C3.mp3
--------------------------------------------------------------------------------
/samples/piano-C4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C4.mp3
--------------------------------------------------------------------------------
/samples/piano-C5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C5.mp3
--------------------------------------------------------------------------------
/samples/piano-C6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C6.mp3
--------------------------------------------------------------------------------
/samples/piano-D3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D3.mp3
--------------------------------------------------------------------------------
/samples/piano-D4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D4.mp3
--------------------------------------------------------------------------------
/samples/piano-D5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D5.mp3
--------------------------------------------------------------------------------
/samples/piano-D6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D6.mp3
--------------------------------------------------------------------------------
/samples/piano-E3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E3.mp3
--------------------------------------------------------------------------------
/samples/piano-E4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E4.mp3
--------------------------------------------------------------------------------
/samples/piano-E5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E5.mp3
--------------------------------------------------------------------------------
/samples/piano-E6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E6.mp3
--------------------------------------------------------------------------------
/samples/piano-F3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F3.mp3
--------------------------------------------------------------------------------
/samples/piano-F4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F4.mp3
--------------------------------------------------------------------------------
/samples/piano-F5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F5.mp3
--------------------------------------------------------------------------------
/samples/piano-F6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F6.mp3
--------------------------------------------------------------------------------
/samples/piano-G3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G3.mp3
--------------------------------------------------------------------------------
/samples/piano-G4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G4.mp3
--------------------------------------------------------------------------------
/samples/piano-G5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G5.mp3
--------------------------------------------------------------------------------
/samples/piano-G6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G6.mp3
--------------------------------------------------------------------------------
/.claude/settings.local.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": {
3 | "allow": [
4 | "Bash(open:*)",
5 | "Bash(node:*)"
6 | ],
7 | "deny": [],
8 | "ask": []
9 | }
10 | }
--------------------------------------------------------------------------------
/ads.txt:
--------------------------------------------------------------------------------
1 | # ads.txt file for piano-online
2 | # This file is used to authorize digital sellers for this domain
3 |
4 | # Google AdSense
5 | google.com, pub-4423187689700927, DIRECT, f08c47fec0942fa0
--------------------------------------------------------------------------------
/sounds/metronome/click.mp3:
--------------------------------------------------------------------------------
1 |
2 |
302 Found
3 |
4 | 302 Found
5 |
nginx/1.14.2
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sounds/metronome/accent.mp3:
--------------------------------------------------------------------------------
1 |
2 | 302 Found
3 |
4 | 302 Found
5 |
nginx/1.14.2
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sounds/metronome/34272_89860-lq.mp3:
--------------------------------------------------------------------------------
1 |
2 | 302 Found
3 |
4 | 302 Found
5 |
nginx/1.14.2
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sounds/metronome/34273_89860-lq.mp3:
--------------------------------------------------------------------------------
1 |
2 | 302 Found
3 |
4 | 302 Found
5 |
nginx/1.14.2
6 |
7 |
8 |
--------------------------------------------------------------------------------
/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
4 | # 允许搜索引擎索引所有内容
5 | Sitemap: https://piano-online.com/sitemap.xml
6 |
7 | # 禁止访问敏感目录
8 | Disallow: /admin/
9 | Disallow: /private/
10 | Disallow: /temp/
11 | Disallow: /*.log$
12 |
13 | # 允许访问静态资源
14 | Allow: /css/
15 | Allow: /js/
16 | Allow: /images/
17 | Allow: /samples/
--------------------------------------------------------------------------------
/images/icons/practice.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/images/icons/basics.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/images/icons/notes.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/images/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "piano_online",
3 | "version": "1.0.0",
4 | "description": "1. 技术栈:用纯粹的HTML + TailwindCSS来做 2. 核心功能:我要做一个在线钢琴网站,支持普通键盘和数字键盘,支持在线演奏。同时也支持练习,支持在线学习:选择一个曲子,会给出对应的按键,有点像俄罗斯方块,你只需要按对应的键,一首曲子即可以完整的演奏出来。 3. 功能区域:你要有完整的header,hero, testinoials, faq, features, pricing, how it works ,footer 区域。其他内容需要编写得专业、真实。 4. 网站文案:所有网站上的文案内容,我要求你使用中文。 5. 网站风格:网站配色和风格需要现代化、高级,类似于苹果官网的风格。网站需要自适应各种屏幕尺寸。",
5 | "main": "piano.js",
6 | "scripts": {
7 | "start": "http-server -p 8000 -c-1",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "http-server": "^14.1.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/download_samples.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Create samples directory if it doesn't exist
4 | mkdir -p samples
5 |
6 | # Clean existing files
7 | rm -f samples/*.mp3
8 |
9 | # Download piano samples from an alternative source
10 | BASE_URL="https://gleitz.github.io/midi-js-soundfonts/FatBoy/acoustic_grand_piano-mp3"
11 | NOTES=("A" "B" "C" "D" "E" "F" "G")
12 | OCTAVES=(3 4 5 6)
13 |
14 | for note in "${NOTES[@]}"; do
15 | for octave in "${OCTAVES[@]}"; do
16 | target_file="samples/piano-${note}${octave}.mp3"
17 | url="${BASE_URL}/${note}${octave}.mp3"
18 | echo "Downloading ${note}${octave} to $target_file..."
19 |
20 | # Download and convert the file
21 | if curl -L -f "$url" -o "$target_file"; then
22 | echo "Successfully downloaded ${note}${octave}"
23 | else
24 | echo "Failed to download ${note}${octave}"
25 | fi
26 | done
27 | done
28 |
29 | echo "Download complete!"
30 |
--------------------------------------------------------------------------------
/images/piano-placeholder.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/scripts/download_samples.py:
--------------------------------------------------------------------------------
1 | import os
2 | import urllib.request
3 |
4 | def download_piano_samples():
5 | # Piano sample URLs (replace these with actual URLs to piano samples)
6 | sample_urls = {
7 | 'F1': 'https://piano-samples.s3.amazonaws.com/piano-F1.mp3',
8 | 'F2': 'https://piano-samples.s3.amazonaws.com/piano-F2.mp3',
9 | 'F3': 'https://piano-samples.s3.amazonaws.com/piano-F3.mp3',
10 | 'F4': 'https://piano-samples.s3.amazonaws.com/piano-F4.mp3',
11 | 'F5': 'https://piano-samples.s3.amazonaws.com/piano-F5.mp3',
12 | 'F6': 'https://piano-samples.s3.amazonaws.com/piano-F6.mp3',
13 | }
14 |
15 | # Create samples directory if it doesn't exist
16 | if not os.path.exists('../samples'):
17 | os.makedirs('../samples')
18 |
19 | # Download each sample
20 | for note, url in sample_urls.items():
21 | output_path = f'../samples/piano-{note}.mp3'
22 | try:
23 | print(f'Downloading {note}...')
24 | urllib.request.urlretrieve(url, output_path)
25 | print(f'Successfully downloaded {note}')
26 | except Exception as e:
27 | print(f'Error downloading {note}: {str(e)}')
28 |
29 | if __name__ == '__main__':
30 | download_piano_samples()
31 |
--------------------------------------------------------------------------------
/scripts/download_tonejs_samples.py:
--------------------------------------------------------------------------------
1 | import os
2 | import urllib.request
3 | import ssl
4 |
5 | def download_tonejs_samples():
6 | # Disable SSL certificate verification (only if needed)
7 | ssl._create_default_https_context = ssl._create_unverified_context
8 |
9 | # Base URL for tonejs-instruments piano samples
10 | base_url = "https://nbrosowsky.github.io/tonejs-instruments/samples/piano"
11 |
12 | # Notes we need (focusing on F notes which we're missing)
13 | notes = ['F1', 'F2', 'F3', 'F4', 'F5', 'F6']
14 |
15 | # Create samples directory if it doesn't exist
16 | samples_dir = '../samples'
17 | if not os.path.exists(samples_dir):
18 | os.makedirs(samples_dir)
19 |
20 | # Download each sample
21 | for note in notes:
22 | filename = f'piano-{note}.mp3'
23 | url = f"{base_url}/{note}.mp3"
24 | output_path = os.path.join(samples_dir, filename)
25 |
26 | try:
27 | print(f'Downloading {note}...')
28 | urllib.request.urlretrieve(url, output_path)
29 | print(f'Successfully downloaded {note}')
30 | except Exception as e:
31 | print(f'Error downloading {note}: {str(e)}')
32 |
33 | if __name__ == '__main__':
34 | download_tonejs_samples()
35 |
--------------------------------------------------------------------------------
/sounds/piano/piano-a3.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-a4.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-a5.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-c3.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-c4.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-c5.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-ds3.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-ds4.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-ds5.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-fs3.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-fs4.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sounds/piano/piano-fs5.mp3:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 - File or directory not found.
6 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/scripts/download_salamander.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | from tqdm import tqdm
4 |
5 | def download_piano_samples():
6 | # 创建samples目录(如果不存在)
7 | samples_dir = 'samples'
8 | if not os.path.exists(samples_dir):
9 | os.makedirs(samples_dir)
10 |
11 | # 需要下载的音符列表
12 | notes = ['B4', 'D4', 'E4', 'G4', 'D5', 'E5', 'G5']
13 |
14 | # Tone.js piano samples的基础URL
15 | base_url = "https://raw.githubusercontent.com/nbrosowsky/tonejs-instruments/master/samples/piano"
16 |
17 | for note in tqdm(notes, desc="Downloading piano samples"):
18 | filename = f"piano-{note}.mp3"
19 | filepath = os.path.join(samples_dir, filename)
20 |
21 | # 如果文件已存在,跳过
22 | if os.path.exists(filepath):
23 | print(f"Skipping {filename} - already exists")
24 | continue
25 |
26 | # 构建完整的URL
27 | url = f"{base_url}/{filename}"
28 |
29 | try:
30 | # 下载文件
31 | response = requests.get(url)
32 | response.raise_for_status()
33 |
34 | # 保存文件
35 | with open(filepath, 'wb') as f:
36 | f.write(response.content)
37 |
38 | print(f"Downloaded {filename}")
39 |
40 | except Exception as e:
41 | print(f"Error downloading {filename}: {str(e)}")
42 |
43 | if __name__ == "__main__":
44 | download_piano_samples()
45 |
--------------------------------------------------------------------------------
/verify.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | // 需要验证的音符列表
5 | const notes = [
6 | 'A0', 'C1', 'D#1', 'F#1', 'A1', 'C2', 'D#2', 'F#2', 'A2', 'C3', 'D#3', 'F#3',
7 | 'A3', 'C4', 'D#4', 'F#4', 'A4', 'C5', 'D#5', 'F#5', 'A5', 'C6', 'D#6', 'F#6', 'A6'
8 | ];
9 |
10 | // 验证文件
11 | console.log('Verifying downloaded files...');
12 | const missingFiles = [];
13 | const invalidFiles = [];
14 |
15 | notes.forEach(note => {
16 | const filePath = path.join('samples', `piano-${note}.mp3`);
17 |
18 | // 检查文件是否存在
19 | if (!fs.existsSync(filePath)) {
20 | missingFiles.push(note);
21 | return;
22 | }
23 |
24 | // 检查文件大小
25 | const stats = fs.statSync(filePath);
26 | if (stats.size < 1000) { // 文件太小可能是损坏的
27 | invalidFiles.push(note);
28 | }
29 | });
30 |
31 | if (missingFiles.length > 0) {
32 | console.error('\nMissing files:');
33 | missingFiles.forEach(note => console.error(`- piano-${note}.mp3`));
34 | }
35 |
36 | if (invalidFiles.length > 0) {
37 | console.error('\nPotentially corrupted files:');
38 | invalidFiles.forEach(note => console.error(`- piano-${note}.mp3`));
39 | }
40 |
41 | if (missingFiles.length === 0 && invalidFiles.length === 0) {
42 | console.log('\nAll files verified successfully!');
43 | console.log(`Total files: ${notes.length}`);
44 | console.log('Piano samples are ready to use.');
45 | } else {
46 | console.error('\nSome files need to be redownloaded.');
47 | console.log('Please run setup.js again for the missing or corrupted files.');
48 | }
--------------------------------------------------------------------------------
/llms.txt:
--------------------------------------------------------------------------------
1 | # 钢琴在线学习平台 - 大语言模型爬虫指南
2 | # 更新日期: 2025-04-25
3 |
4 | # 所有大语言模型爬虫的默认规则
5 | User-Agent: *
6 | Allow: /
7 | Allow: /index.html
8 | Allow: /about.html
9 | Allow: /piano.html
10 | Allow: /practice.html
11 | Allow: /tutorial.html
12 | Allow: /tutorials.html
13 | Allow: /articles/
14 | Disallow: /api/
15 | Disallow: /_next/
16 | Disallow: /static/
17 | Disallow: /404
18 | Disallow: /500
19 | Disallow: /*.json$
20 | Disallow: /samples/
21 | Disallow: /sounds/
22 | Disallow: /temp_download/
23 | Disallow: /node_modules/
24 |
25 | # OpenAI GPT爬虫规则
26 | User-Agent: GPTBot
27 | Allow: /index.html
28 | Allow: /about.html
29 | Allow: /piano.html
30 | Allow: /practice.html
31 | Allow: /tutorial.html
32 | Allow: /tutorials.html
33 | Allow: /articles/
34 | Disallow: /
35 |
36 | # Anthropic Claude爬虫规则
37 | User-Agent: anthropic-ai
38 | Allow: /index.html
39 | Allow: /about.html
40 | Allow: /piano.html
41 | Allow: /practice.html
42 | Allow: /tutorial.html
43 | Allow: /tutorials.html
44 | Allow: /articles/
45 | Disallow: /
46 |
47 | # Google Bard/Gemini爬虫规则
48 | User-Agent: Google-Extended
49 | Allow: /
50 | Disallow: /api/
51 | Disallow: /_next/
52 | Disallow: /static/
53 | Disallow: /404
54 | Disallow: /500
55 | Disallow: /*.json$
56 | Disallow: /samples/
57 | Disallow: /sounds/
58 | Disallow: /temp_download/
59 | Disallow: /node_modules/
60 |
61 | # 百度文心一言爬虫规则
62 | User-Agent: Baiduspider-ERNIE-Bot
63 | Allow: /
64 | Disallow: /api/
65 | Disallow: /_next/
66 | Disallow: /static/
67 | Disallow: /404
68 | Disallow: /500
69 | Disallow: /*.json$
70 | Disallow: /samples/
71 | Disallow: /sounds/
72 | Disallow: /temp_download/
73 | Disallow: /node_modules/
74 |
--------------------------------------------------------------------------------
/test-audio.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Piano Audio Test
5 |
6 |
7 |
8 |
9 |
16 |
17 | Piano Audio Test
18 |
19 |
20 |
21 |
22 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/setup.js:
--------------------------------------------------------------------------------
1 | const https = require('https');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | // 创建项目结构
6 | const dirs = ['samples'];
7 | dirs.forEach(dir => {
8 | if (!fs.existsSync(dir)) {
9 | fs.mkdirSync(dir);
10 | console.log(`Created directory: ${dir}`);
11 | }
12 | });
13 |
14 | // 定义要下载的音符
15 | const notes = [
16 | 'A0', 'C1', 'D#1', 'F#1', 'A1', 'C2', 'D#2', 'F#2', 'A2', 'C3', 'D#3', 'F#3',
17 | 'A3', 'C4', 'D#4', 'F#4', 'A4', 'C5', 'D#5', 'F#5', 'A5', 'C6', 'D#6', 'F#6', 'A6'
18 | ];
19 |
20 | // 下载进度追踪
21 | let downloadedCount = 0;
22 | const totalFiles = notes.length;
23 |
24 | // 下载单个文件的函数
25 | function downloadFile(note) {
26 | const url = `https://tonejs.github.io/audio/salamander/${note}.mp3`;
27 | const filePath = path.join('samples', `piano-${note}.mp3`);
28 |
29 | https.get(url, (response) => {
30 | if (response.statusCode === 200) {
31 | const fileStream = fs.createWriteStream(filePath);
32 | response.pipe(fileStream);
33 |
34 | fileStream.on('finish', () => {
35 | downloadedCount++;
36 | const progress = Math.round((downloadedCount / totalFiles) * 100);
37 | console.log(`Downloaded: piano-${note}.mp3 (${progress}% complete)`);
38 |
39 | if (downloadedCount === totalFiles) {
40 | console.log('\nAll piano samples downloaded successfully!');
41 | console.log('Setup complete! You can now run the piano application.');
42 | }
43 | });
44 | } else {
45 | console.error(`Failed to download ${note}: HTTP ${response.statusCode}`);
46 | }
47 | }).on('error', (err) => {
48 | console.error(`Error downloading ${note}:`, err);
49 | });
50 | }
51 |
52 | console.log('Starting piano samples download...');
53 | notes.forEach(note => downloadFile(note));
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 钢琴在线 (Piano Online)
2 |
3 | ## 项目介绍
4 | 一个现代化的在线钢琴学习平台,让每个人都能轻松学习钢琴。基于纯HTML和TailwindCSS开发,提供专业的钢琴学习体验。
5 |
6 | ## 核心功能
7 | ### 🎹 在线演奏
8 | - 支持多种输入方式
9 | - 电脑键盘输入
10 | - 数字键盘输入
11 | - 鼠标点击演奏
12 |
13 | ### 🎼 智能练习模式
14 | - 交互式学习体验
15 | - 类似节奏游戏的学习模式
16 | - 实时按键提示
17 | - 自动跟踪演奏进度
18 | - 曲目练习功能
19 | - 可选择不同难度的曲目
20 | - 按键提示系统(类似俄罗斯方块)
21 | - 实时反馈演奏效果
22 |
23 | ## 技术实现
24 | - 前端技术
25 | - HTML5
26 | - TailwindCSS
27 | - 原生JavaScript
28 |
29 | ## 网站功能区域
30 | 1. Header - 导航栏
31 | 2. Hero - 主视觉区域
32 | 3. Features - 功能特性展示
33 | 4. How it works - 使用教程
34 | 5. Testimonials - 用户评价
35 | 6. FAQ - 常见问题
36 | 7. Footer - 页脚信息
37 |
38 | ## 设计理念
39 | - 🎨 视觉风格
40 | - 现代简约设计
41 | - 参考苹果官网的高端风格
42 | - 专业且真实的内容呈现
43 | - 📱 响应式设计
44 | - 完美适配各种屏幕尺寸
45 | - 移动端优化体验
46 | - 🌐 本地化
47 | - 全中文界面
48 | - 符合中国用户使用习惯
49 |
50 | ## 开发状态
51 | - ✅ 基础钢琴功能
52 | - ✅ 键盘映射系统
53 | - ✅ 练习模式框架
54 | - ✅ Google AdSense自动广告集成
55 | - ✅ Google Analytics数据追踪
56 | - 🚧 曲目库建设中
57 | - 🚧 用户系统开发中
58 |
59 | ## 如何开始
60 | 1. 克隆项目到本地
61 | 2. 确保安装了必要的开发工具
62 | 3. 使用Live Server运行项目
63 | 4. 访问 http://localhost:8000 开始体验
64 |
65 | ## 浏览器支持
66 | - Chrome (推荐)
67 | - Firefox
68 | - Safari
69 | - Edge
70 |
71 | ## 贡献指南
72 | 欢迎提交Issue和Pull Request来帮助改进项目。
73 |
74 | ## 许可证
75 | MIT License
76 |
77 | ---
78 |
79 | ## 更新日志
80 |
81 | ### 2025-10-26
82 |
83 | #### ✅ 谷歌广告集成完成
84 | - 为所有18个HTML页面添加了Google AdSense自动广告代码
85 | - 包含主要页面: index.html, about.html, piano.html, practice.html, tutorials.html
86 | - 包含13个教程文章页面
87 | - 集成Google Analytics追踪代码 (ID: G-EYGD99YB4Y)
88 | - 集成Google AdSense自动广告 (Publisher ID: ca-pub-4423187689700927)
89 | - 添加Google站点验证meta标签
90 |
91 | #### ✅ 全面SEO优化完成
92 | **目标**: 2周内UV从100提升到200+
93 |
94 | **技术优化**:
95 | - ✅ 更新sitemap.xml,包含所有18个页面
96 | - ✅ 为所有页面添加Open Graph标签(Facebook分享优化)
97 | - ✅ 为所有页面添加Twitter Card标签
98 | - ✅ 首页添加Schema.org结构化数据(WebApplication + Course)
99 | - ✅ 添加资源预加载和DNS预解析
100 | - ✅ 优化所有页面的canonical标签
101 |
102 | **创建的文档**:
103 | - 📄 `SEO-OPTIMIZATION-GUIDE.md` - 完整的SEO优化和流量提升指南
104 | - 📄 `QUICK-ACTION-CHECKLIST.md` - 快速执行清单和每日任务
105 |
106 | **预期效果**:
107 | - 1周后: UV增长20-30%
108 | - 2周后: UV增长50-100%
109 | - 1个月后: UV达到200-300
110 | - 3个月后: UV达到500+
--------------------------------------------------------------------------------
/scripts/generate_samples.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from scipy.io import wavfile
3 | from scipy.signal import sawtooth
4 | import os
5 |
6 | def generate_piano_note(freq, duration=1.0, sample_rate=44100, amplitude=0.5):
7 | t = np.linspace(0, duration, int(sample_rate * duration))
8 |
9 | # Generate a complex waveform that approximates a piano note
10 | fundamental = amplitude * np.sin(2 * np.pi * freq * t)
11 | second_harmonic = 0.5 * amplitude * np.sin(2 * np.pi * (2 * freq) * t)
12 | third_harmonic = 0.25 * amplitude * np.sin(2 * np.pi * (3 * freq) * t)
13 |
14 | # Combine harmonics
15 | note = fundamental + second_harmonic + third_harmonic
16 |
17 | # Apply envelope
18 | envelope = np.exp(-3 * t)
19 | note = note * envelope
20 |
21 | # Normalize
22 | note = note / np.max(np.abs(note))
23 |
24 | return note
25 |
26 | def note_to_freq(note):
27 | # A4 = 440Hz
28 | # Each semitone is a factor of 2^(1/12)
29 | A4 = 440
30 | notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
31 | octave = int(note[-1])
32 | note_name = note[:-1]
33 |
34 | semitones_from_a4 = (octave - 4) * 12 + notes.index(note_name) - notes.index('A')
35 | return A4 * (2 ** (semitones_from_a4 / 12))
36 |
37 | def generate_piano_samples():
38 | if not os.path.exists('../samples'):
39 | os.makedirs('../samples')
40 |
41 | # Generate F notes from F1 to F6
42 | for octave in range(1, 7):
43 | note = f'F{octave}'
44 | freq = note_to_freq(note)
45 | audio_data = generate_piano_note(freq)
46 |
47 | # Convert to 16-bit PCM
48 | audio_data_16bit = (audio_data * 32767).astype(np.int16)
49 |
50 | # Save as WAV first
51 | wav_path = f'../samples/piano-{note}.wav'
52 | wavfile.write(wav_path, 44100, audio_data_16bit)
53 |
54 | # Convert to MP3 using ffmpeg
55 | mp3_path = f'../samples/piano-{note}.mp3'
56 | os.system(f'ffmpeg -i {wav_path} -codec:a libmp3lame -qscale:a 2 {mp3_path}')
57 |
58 | # Remove WAV file
59 | os.remove(wav_path)
60 | print(f'Generated {mp3_path}')
61 |
62 | if __name__ == '__main__':
63 | generate_piano_samples()
64 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | // 导入所需的模块
2 | import { Piano } from './piano.js';
3 | import { PracticeMode } from './practice-mode.js';
4 | import { PianoRecorder } from './recorder.js';
5 | import { RhythmGame } from './rhythm-game.js';
6 | import { Tutorial } from './tutorial.js';
7 |
8 | // 等待 DOM 完全加载
9 | document.addEventListener('DOMContentLoaded', async () => {
10 | console.log('DOM Content Loaded');
11 |
12 | try {
13 | // 1. 首先初始化钢琴实例
14 | console.log('Initializing Piano...');
15 | const piano = new Piano();
16 | await piano.audio.init();
17 | console.log('Piano initialized');
18 |
19 | // 2. 初始化练习模式
20 | console.log('Initializing Practice Mode...');
21 | const practiceMode = new PracticeMode(piano);
22 | console.log('Practice Mode initialized');
23 |
24 | // 3. 初始化录音功能
25 | console.log('Initializing Recorder...');
26 | const recorder = new PianoRecorder(piano);
27 | console.log('Recorder initialized');
28 |
29 | // 4. 初始化节奏大师游戏
30 | console.log('Initializing Rhythm Game...');
31 | const rhythmGame = new RhythmGame(piano);
32 | console.log('Rhythm Game initialized');
33 |
34 | // 5. 初始化首次使用教程
35 | console.log('Initializing Tutorial...');
36 | const tutorial = new Tutorial(piano);
37 | console.log('Tutorial initialized');
38 |
39 | // 等待首次交互恢复音频上下文
40 | const resumeAudio = async () => {
41 | if (piano?.audio?.context?.state === 'suspended') {
42 | try {
43 | await piano.audio.context.resume();
44 | console.log('AudioContext resumed successfully');
45 | } catch (error) {
46 | console.error('Failed to resume AudioContext:', error);
47 | }
48 | }
49 | };
50 | ['click', 'keydown', 'touchstart'].forEach(event => {
51 | document.addEventListener(event, resumeAudio, { once: true });
52 | });
53 |
54 | // 页面卸载时释放资源
55 | window.addEventListener('beforeunload', () => {
56 | recorder?.dispose();
57 | if (piano?.audio?.context) {
58 | piano.audio.context.close();
59 | }
60 | });
61 |
62 | // 将 tutorial 暴露到全局,方便调试
63 | window.pianoTutorial = tutorial;
64 |
65 | console.log('All components initialized successfully');
66 | } catch (error) {
67 | console.error('Error during initialization:', error);
68 | }
69 | });
70 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | background: #f0f0f0;
5 | font-family: Arial, sans-serif;
6 | }
7 |
8 | .container {
9 | width: 100%;
10 | min-height: 100vh;
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | padding: 20px 0;
15 | }
16 |
17 | .controls {
18 | margin-bottom: 30px;
19 | display: flex;
20 | gap: 15px;
21 | }
22 |
23 | .controls button,
24 | .controls select {
25 | padding: 8px 15px;
26 | border: none;
27 | border-radius: 4px;
28 | background: white;
29 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
30 | cursor: pointer;
31 | font-size: 14px;
32 | }
33 |
34 | #piano {
35 | position: relative;
36 | width: 100%;
37 | max-width: 1200px;
38 | height: 220px;
39 | background: linear-gradient(to bottom, #424242 0%, #232323 100%);
40 | padding: 30px 40px 20px;
41 | border-radius: 8px;
42 | box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
43 | }
44 |
45 | .keys {
46 | display: flex;
47 | position: relative;
48 | width: 100%;
49 | height: 100%;
50 | }
51 |
52 | /* 白键样式 */
53 | .key.white {
54 | width: calc(100% / 52); /* 52个白键 */
55 | height: 100%;
56 | background: linear-gradient(to bottom, #fff 0%, #eee 100%);
57 | border: 1px solid #ccc;
58 | border-radius: 0 0 4px 4px;
59 | box-shadow: 0 1px 1px rgba(0,0,0,0.1);
60 | z-index: 1;
61 | position: relative;
62 | }
63 |
64 | .key.white:hover {
65 | background: linear-gradient(to bottom, #fff 0%, #f2f2f2 100%);
66 | }
67 |
68 | .key.white:active,
69 | .key.white.active {
70 | background: linear-gradient(to bottom, #f2f2f2 0%, #e6e6e6 100%);
71 | box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
72 | transform: translateY(1px);
73 | }
74 |
75 | /* 黑键样式 */
76 | .key.black {
77 | position: absolute;
78 | width: calc((100% / 52) * 0.65);
79 | height: 65%;
80 | background: linear-gradient(to bottom, #000 0%, #222 100%);
81 | border-radius: 0 0 3px 3px;
82 | z-index: 2;
83 | box-shadow: inset 0px -1px 2px rgba(255,255,255,0.2),
84 | 0 2px 3px rgba(0,0,0,0.6);
85 | }
86 |
87 | .key.black:hover {
88 | background: linear-gradient(to bottom, #222 0%, #333 100%);
89 | }
90 |
91 | .key.black:active,
92 | .key.black.active {
93 | background: linear-gradient(to bottom, #333 0%, #444 100%);
94 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.8);
95 | transform: translateY(1px);
96 | }
97 |
98 | /* 黑键位置计算 */
99 | .key.black[data-note*
100 |
101 | #hint {
102 | margin-top: 20px;
103 | font-size: 18px;
104 | color: #666;
105 | }
--------------------------------------------------------------------------------
/js/tutorial.js:
--------------------------------------------------------------------------------
1 | // 首次使用教程逻辑
2 | export class Tutorial {
3 | constructor(piano) {
4 | this.piano = piano;
5 | this.hasSeenTutorial = localStorage.getItem('piano-tutorial-completed') === 'true';
6 | this.init();
7 | }
8 |
9 | init() {
10 | // 如果用户已经看过教程,不显示
11 | if (this.hasSeenTutorial) {
12 | return;
13 | }
14 |
15 | // 延迟显示教程,给页面加载时间
16 | setTimeout(() => {
17 | this.show();
18 | }, 1000);
19 |
20 | this.setupEventListeners();
21 | }
22 |
23 | show() {
24 | const overlay = document.getElementById('tutorial-overlay');
25 | if (overlay) {
26 | overlay.style.display = 'flex';
27 | }
28 | }
29 |
30 | hide() {
31 | const overlay = document.getElementById('tutorial-overlay');
32 | if (overlay) {
33 | overlay.style.display = 'none';
34 | }
35 | }
36 |
37 | complete() {
38 | localStorage.setItem('piano-tutorial-completed', 'true');
39 | this.hasSeenTutorial = true;
40 | this.hide();
41 | }
42 |
43 | setupEventListeners() {
44 | // 跳过按钮
45 | const skipButton = document.getElementById('tutorial-skip');
46 | if (skipButton) {
47 | skipButton.addEventListener('click', () => {
48 | this.complete();
49 | });
50 | }
51 |
52 | // 开始体验按钮
53 | const startButton = document.getElementById('tutorial-start');
54 | if (startButton) {
55 | startButton.addEventListener('click', () => {
56 | this.complete();
57 | });
58 | }
59 |
60 | // 监听键盘按下 A 键
61 | const handleKeyPress = (e) => {
62 | if (e.key.toLowerCase() === 'a' && !this.hasSeenTutorial) {
63 | // 用户按下了 A 键,显示成功提示
64 | const keyDemo = document.querySelector('.tutorial-key-demo');
65 | if (keyDemo) {
66 | keyDemo.style.background = 'linear-gradient(135deg, #4CAF50, #45a049)';
67 | keyDemo.textContent = '✓ 太棒了!';
68 |
69 | // 2秒后自动关闭教程
70 | setTimeout(() => {
71 | this.complete();
72 | }, 2000);
73 | }
74 |
75 | // 移除监听器,避免重复触发
76 | document.removeEventListener('keydown', handleKeyPress);
77 | }
78 | };
79 |
80 | document.addEventListener('keydown', handleKeyPress);
81 | }
82 |
83 | // 提供一个方法让用户可以重新显示教程
84 | static resetTutorial() {
85 | localStorage.removeItem('piano-tutorial-completed');
86 | window.location.reload();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/generate_samples.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from scipy.io import wavfile
3 | import os
4 | from scipy.io.wavfile import write
5 | import subprocess
6 |
7 | def note_to_freq(note):
8 | # 音符到频率的映射
9 | notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
10 | octave = int(note[-1])
11 | note_name = note[:-1]
12 |
13 | if note_name not in notes:
14 | raise ValueError(f"Invalid note name: {note_name}")
15 |
16 | # A4 = 440Hz
17 | A4 = 440
18 | notes_from_A4 = notes.index(note_name) - notes.index('A') + (octave - 4) * 12
19 |
20 | return A4 * (2 ** (notes_from_A4 / 12))
21 |
22 | def generate_piano_note(freq, duration=2.0, sample_rate=44100, decay_factor=5.0):
23 | """生成钢琴音色的音符"""
24 | t = np.linspace(0, duration, int(sample_rate * duration))
25 |
26 | # 基频
27 | fundamental = np.sin(2 * np.pi * freq * t)
28 |
29 | # 添加泛音
30 | harmonics = [
31 | 0.5 * np.sin(2 * np.pi * (2 * freq) * t), # 八度
32 | 0.3 * np.sin(2 * np.pi * (3 * freq) * t), # 十二度
33 | 0.2 * np.sin(2 * np.pi * (4 * freq) * t), # 双八度
34 | ]
35 |
36 | # 合并所有波形
37 | wave = fundamental + sum(harmonics)
38 |
39 | # 添加衰减包络
40 | envelope = np.exp(-decay_factor * t)
41 | wave = wave * envelope
42 |
43 | # 归一化
44 | wave = wave / np.max(np.abs(wave))
45 |
46 | return wave
47 |
48 | def save_as_mp3(wav_path, mp3_path):
49 | """将WAV文件转换为MP3"""
50 | subprocess.run([
51 | 'ffmpeg', '-i', wav_path,
52 | '-codec:a', 'libmp3lame',
53 | '-qscale:a', '2',
54 | mp3_path,
55 | '-y' # 覆盖已存在的文件
56 | ], capture_output=True)
57 |
58 | # 删除临时的WAV文件
59 | os.remove(wav_path)
60 |
61 | def generate_note_file(note, output_dir='samples'):
62 | """生成指定音符的音频文件"""
63 | if not os.path.exists(output_dir):
64 | os.makedirs(output_dir)
65 |
66 | freq = note_to_freq(note)
67 | wave = generate_piano_note(freq)
68 |
69 | # 首先保存为WAV
70 | wav_path = os.path.join(output_dir, f'temp_{note}.wav')
71 | mp3_path = os.path.join(output_dir, f'piano-{note}.mp3')
72 |
73 | # 保存为16位WAV
74 | wave = np.int16(wave * 32767)
75 | write(wav_path, 44100, wave)
76 |
77 | # 转换为MP3
78 | save_as_mp3(wav_path, mp3_path)
79 | print(f"Generated {mp3_path}")
80 |
81 | def main():
82 | # 需要生成的音符列表
83 | notes = ['B4', 'D4', 'E4', 'G4', 'D5', 'E5', 'G5']
84 |
85 | for note in notes:
86 | if not os.path.exists(f'samples/piano-{note}.mp3'):
87 | generate_note_file(note)
88 | else:
89 | print(f"Skipping piano-{note}.mp3 - already exists")
90 |
91 | if __name__ == '__main__':
92 | main()
93 |
--------------------------------------------------------------------------------
/generate_samples.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Generate Piano Samples
5 |
6 |
7 |
8 | Piano Sample Generator
9 |
10 |
11 |
12 |
13 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/css/tutorials.css:
--------------------------------------------------------------------------------
1 | /* 教程页面特定样式 */
2 | .tutorials-hero {
3 | background-color: #f8f9fa;
4 | padding: 60px 0;
5 | text-align: center;
6 | }
7 |
8 | .tutorials-hero h1 {
9 | font-size: 2.5rem;
10 | color: #333;
11 | margin-bottom: 20px;
12 | }
13 |
14 | .tutorials-hero .subtitle {
15 | font-size: 1.2rem;
16 | color: #666;
17 | }
18 |
19 | .tutorial-categories {
20 | padding: 60px 0;
21 | }
22 |
23 | .category-grid {
24 | display: grid;
25 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
26 | gap: 30px;
27 | margin-top: 30px;
28 | }
29 |
30 | .category-card {
31 | background: #fff;
32 | border-radius: 10px;
33 | padding: 30px;
34 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
35 | }
36 |
37 | .category-card h2 {
38 | color: #333;
39 | font-size: 1.5rem;
40 | margin-bottom: 20px;
41 | border-bottom: 2px solid #007bff;
42 | padding-bottom: 10px;
43 | }
44 |
45 | .tutorial-list {
46 | list-style: none;
47 | padding: 0;
48 | margin: 0;
49 | }
50 |
51 | .tutorial-list li {
52 | margin-bottom: 15px;
53 | }
54 |
55 | .tutorial-list a {
56 | color: #444;
57 | text-decoration: none;
58 | display: block;
59 | padding: 8px 0;
60 | transition: color 0.3s;
61 | }
62 |
63 | .tutorial-list a:hover {
64 | color: #007bff;
65 | }
66 |
67 | .latest-tutorials {
68 | background-color: #f8f9fa;
69 | padding: 60px 0;
70 | }
71 |
72 | .latest-tutorials h2 {
73 | text-align: center;
74 | margin-bottom: 40px;
75 | color: #333;
76 | font-size: 2rem;
77 | }
78 |
79 | .tutorials-grid {
80 | display: grid;
81 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
82 | gap: 30px;
83 | }
84 |
85 | .tutorial-card {
86 | background: #fff;
87 | border-radius: 10px;
88 | overflow: hidden;
89 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
90 | transition: transform 0.3s;
91 | }
92 |
93 | .tutorial-card:hover {
94 | transform: translateY(-5px);
95 | }
96 |
97 | .tutorial-content {
98 | padding: 20px;
99 | }
100 |
101 | .tutorial-content h3 {
102 | color: #333;
103 | font-size: 1.3rem;
104 | margin-bottom: 15px;
105 | }
106 |
107 | .tutorial-content p {
108 | color: #666;
109 | margin-bottom: 20px;
110 | line-height: 1.6;
111 | }
112 |
113 | .read-more {
114 | display: inline-block;
115 | color: #007bff;
116 | text-decoration: none;
117 | font-weight: 500;
118 | transition: color 0.3s;
119 | }
120 |
121 | .read-more:hover {
122 | color: #0056b3;
123 | }
124 |
125 | .site-footer {
126 | background-color: #333;
127 | color: #fff;
128 | padding: 20px 0;
129 | text-align: center;
130 | }
131 |
132 | /* 响应式设计 */
133 | @media (max-width: 768px) {
134 | .category-grid,
135 | .tutorials-grid {
136 | grid-template-columns: 1fr;
137 | }
138 |
139 | .tutorials-hero h1 {
140 | font-size: 2rem;
141 | }
142 |
143 | .category-card,
144 | .tutorial-card {
145 | margin-bottom: 20px;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/piano.css:
--------------------------------------------------------------------------------
1 | /* 钢琴容器 */
2 | .piano-container {
3 | background: #1a1a1a;
4 | padding: 20px;
5 | border-radius: 8px;
6 | position: relative;
7 | height: 300px;
8 | overflow: hidden;
9 | }
10 |
11 | /* 钢琴键盘 */
12 | .piano {
13 | position: relative;
14 | width: 100%;
15 | height: 220px;
16 | background: linear-gradient(to bottom, #1a1a1a, #000);
17 | border-radius: 8px 8px 5px 5px;
18 | box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
19 | transform-style: preserve-3d;
20 | transform: rotateX(2deg);
21 | }
22 |
23 | /* 钢琴白键 */
24 | .octave {
25 | position: relative;
26 | height: 100%;
27 | display: inline-block;
28 | width: 33.33%; /* 3个八度 */
29 | }
30 |
31 | .piano-key {
32 | position: relative;
33 | transition: all 0.1s;
34 | cursor: pointer;
35 | box-sizing: border-box;
36 | }
37 |
38 | .piano-key.white {
39 | background: white;
40 | height: 100%;
41 | width: 14.28%; /* 7个白键 */
42 | display: inline-block;
43 | border-radius: 0 0 4px 4px;
44 | border: 1px solid #ccc;
45 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
46 | vertical-align: top;
47 | position: relative;
48 | z-index: 1;
49 | }
50 |
51 | .piano-key.black {
52 | background: #333;
53 | height: 60%;
54 | width: 8%;
55 | position: absolute;
56 | top: 0;
57 | z-index: 2;
58 | border-radius: 0 0 3px 3px;
59 | border: 1px solid #000;
60 | }
61 |
62 | .key-label {
63 | position: absolute;
64 | bottom: 10px;
65 | left: 50%;
66 | transform: translateX(-50%);
67 | font-size: 12px;
68 | color: #666;
69 | pointer-events: none;
70 | font-weight: 500;
71 | text-align: center;
72 | width: 100%;
73 | white-space: pre-line;
74 | }
75 |
76 | .piano-key.black .key-label {
77 | color: #fff;
78 | bottom: 20px;
79 | }
80 |
81 | .piano-key.white:active,
82 | .piano-key.white.pressed {
83 | background: #f0f0f0;
84 | transform: translateY(2px);
85 | }
86 |
87 | .piano-key.black:active,
88 | .piano-key.black.pressed {
89 | background: #222;
90 | transform: translateY(2px);
91 | }
92 |
93 | /* 钢琴外壳 */
94 | .piano-case {
95 | position: relative;
96 | width: 100%;
97 | height: 50px;
98 | background: linear-gradient(to bottom, #4a4a4a, #000);
99 | border-radius: 8px 8px 0 0;
100 | box-shadow:
101 | inset 0 1px 0px rgba(255,255,255,0.1),
102 | 0 -1px 0px rgba(0,0,0,0.3);
103 | }
104 |
105 | /* 钢琴品牌标志 */
106 | .piano-brand {
107 | position: absolute;
108 | top: 50%;
109 | left: 50%;
110 | transform: translate(-50%, -50%);
111 | color: rgba(255,255,255,0.8);
112 | font-family: "Times New Roman", serif;
113 | font-size: 24px;
114 | letter-spacing: 2px;
115 | text-shadow: 0 1px 2px rgba(0,0,0,0.5);
116 | }
117 |
118 | /* 键盘提示 */
119 | .key-hint {
120 | position: absolute;
121 | bottom: 10px;
122 | left: 50%;
123 | transform: translateX(-50%);
124 | color: #666;
125 | font-size: 12px;
126 | font-weight: bold;
127 | }
128 |
129 | /* 钢琴底座 */
130 | .piano-stand {
131 | width: 100%;
132 | height: 20px;
133 | background: linear-gradient(to bottom, #333, #000);
134 | border-radius: 0 0 8px 8px;
135 | box-shadow: 0 2px 6px rgba(0,0,0,0.3);
136 | }
137 |
--------------------------------------------------------------------------------
/TESTING_CHECKLIST.md:
--------------------------------------------------------------------------------
1 | # Testing Checklist for Piano Online UX Improvements
2 |
3 | ## Date: 2025-11-04
4 |
5 | ## Changes Summary
6 | 1. ✅ Moved piano keyboard to first screen (above the fold)
7 | 2. ✅ Compressed hero section to minimal text
8 | 3. ✅ Added keyboard key labels (A, S, D, etc.) on piano keys
9 | 4. ✅ Created first-time user tutorial overlay
10 | 5. ✅ Simplified terminology with tooltips
11 | 6. ✅ Added visual feedback (success/error animations) for key presses
12 |
13 | ## Manual Testing Required
14 |
15 | ### Desktop Testing (1920x1080)
16 | - [ ] Navigate to homepage
17 | - [ ] Verify piano keyboard is visible without scrolling
18 | - [ ] Verify hero text is concise: "用电脑键盘弹钢琴 🎹"
19 | - [ ] Check tutorial overlay appears on first visit
20 | - [ ] Press 'A' key during tutorial - should trigger success feedback
21 | - [ ] Close tutorial and verify it doesn't show again
22 | - [ ] Verify keyboard labels visible on all white keys (A, S, D, F, G, H, J, K, L)
23 | - [ ] Verify keyboard labels visible on black keys (1, 2, 4, 5, 6, 8, 9)
24 | - [ ] Hover over "声音延长 🎵" - tooltip should appear
25 | - [ ] Hover over "🎮 游戏模式" - tooltip should appear
26 | - [ ] Hover over "✨ 跟着提示弹" - tooltip should appear
27 |
28 | ### Practice Mode Testing
29 | - [ ] Select a song from dropdown (e.g., "⭐ 小星星 (简单)")
30 | - [ ] Click "✨ 跟着提示弹" button
31 | - [ ] Press the correct key - should see green glow animation
32 | - [ ] Press an incorrect key - should see shake/red border animation
33 | - [ ] Complete a song - should see completion message with stats
34 |
35 | ### Tablet Testing (768px)
36 | - [ ] Piano keyboard visible without scrolling
37 | - [ ] Tutorial overlay responsive and readable
38 | - [ ] All tooltips work on tap/hover
39 | - [ ] Keyboard labels readable
40 |
41 | ### Mobile Testing (375px)
42 | - [ ] Piano keyboard visible (horizontal scroll acceptable)
43 | - [ ] Tutorial steps stack vertically
44 | - [ ] All buttons accessible
45 | - [ ] Key labels still visible
46 |
47 | ### First-Time User Experience
48 | - [ ] Clear localStorage: `localStorage.removeItem('piano-tutorial-completed')`
49 | - [ ] Refresh page
50 | - [ ] Tutorial should appear after ~1 second
51 | - [ ] Tutorial content is clear and actionable
52 | - [ ] "A" key demo pulses/animates
53 | - [ ] "跳过" button works
54 | - [ ] "开始体验" button works
55 | - [ ] After closing, tutorial doesn't reappear
56 |
57 | ### Accessibility
58 | - [ ] All buttons have clear labels
59 | - [ ] Tooltips provide helpful explanations
60 | - [ ] Visual feedback is obvious and distinct
61 | - [ ] No jargon without explanation
62 |
63 | ## Browser Compatibility
64 | - [ ] Chrome (latest)
65 | - [ ] Firefox (latest)
66 | - [ ] Safari (latest)
67 | - [ ] Edge (latest)
68 |
69 | ## Performance
70 | - [ ] Page loads quickly (< 3 seconds)
71 | - [ ] No console errors
72 | - [ ] Smooth animations
73 | - [ ] Audio plays without delay
74 |
75 | ## Issues Found
76 | (List any issues discovered during testing)
77 |
78 | ---
79 |
80 | ## Test Server
81 | Local server running at: http://localhost:8080
82 |
83 | To test:
84 | 1. Open browser
85 | 2. Navigate to http://localhost:8080
86 | 3. Clear localStorage if needed: `localStorage.clear()`
87 | 4. Follow checklist above
88 |
89 | ## Rollback Instructions
90 | If critical issues found:
91 | ```bash
92 | cp index.backup.html index.html
93 | git checkout -- css/piano.css js/piano.js js/main.js js/practice-mode.js
94 | rm js/tutorial.js
95 | ```
96 |
--------------------------------------------------------------------------------
/js/staff.js:
--------------------------------------------------------------------------------
1 | // 五线谱渲染工具
2 | class StaffRenderer {
3 | constructor(container) {
4 | this.container = container;
5 | this.vf = null;
6 | this.context = null;
7 | this.stave = null;
8 | this.init();
9 | }
10 |
11 | init() {
12 | // 清空容器
13 | this.container.innerHTML = '';
14 |
15 | // 创建渲染器
16 | const renderer = new Vex.Flow.Renderer(this.container, Vex.Flow.Renderer.Backends.SVG);
17 | renderer.resize(500, 150);
18 | this.context = renderer.getContext();
19 |
20 | // 创建五线谱
21 | this.stave = new Vex.Flow.Stave(10, 40, 480);
22 | this.stave.addClef('treble').addTimeSignature('4/4');
23 | }
24 |
25 | // 将音符名称转换为VexFlow音符
26 | convertToVexFlowNotes(noteNames) {
27 | return noteNames.map(noteName => {
28 | // 解析音符名称(例如:'C4'变成'c/4')
29 | const pitch = noteName.slice(0, -1).toLowerCase();
30 | const octave = noteName.slice(-1);
31 | return new Vex.Flow.StaveNote({
32 | clef: 'treble',
33 | keys: [`${pitch}/${octave}`],
34 | duration: 'q'
35 | });
36 | });
37 | }
38 |
39 | // 渲染音符
40 | renderNotes(noteNames) {
41 | // 清除之前的内容
42 | this.init();
43 |
44 | // 创建音符
45 | const notes = this.convertToVexFlowNotes(noteNames);
46 |
47 | // 创建声部
48 | const voice = new Vex.Flow.Voice({
49 | num_beats: notes.length,
50 | beat_value: 4
51 | });
52 | voice.addTickables(notes);
53 |
54 | // 创建格式化器
55 | const formatter = new Vex.Flow.Formatter();
56 | formatter.joinVoices([voice]).format([voice], 400);
57 |
58 | // 绘制五线谱和音符
59 | this.stave.setContext(this.context).draw();
60 | voice.draw(this.context, this.stave);
61 | }
62 |
63 | // 清除五线谱
64 | clear() {
65 | this.container.innerHTML = '';
66 | }
67 | }
68 |
69 | // 音符工具类
70 | class NoteUtils {
71 | static noteNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
72 | static octaves = ['3', '4', '5'];
73 |
74 | // 生成随机音符
75 | static generateRandomNotes(difficulty, count) {
76 | const notes = [];
77 | let availableNotes;
78 |
79 | switch(difficulty) {
80 | case 'beginner':
81 | // 初级:只使用中央C周围的音符
82 | availableNotes = this.noteNames.map(note => `${note}4`);
83 | break;
84 | case 'intermediate':
85 | // 中级:使用两个八度的音符
86 | availableNotes = this.noteNames.flatMap(note =>
87 | ['4', '5'].map(octave => `${note}${octave}`)
88 | );
89 | break;
90 | case 'advanced':
91 | // 高级:使用三个八度的音符
92 | availableNotes = this.noteNames.flatMap(note =>
93 | this.octaves.map(octave => `${note}${octave}`)
94 | );
95 | break;
96 | default:
97 | availableNotes = this.noteNames.map(note => `${note}4`);
98 | }
99 |
100 | // 生成指定数量的随机音符
101 | for(let i = 0; i < count; i++) {
102 | const randomIndex = Math.floor(Math.random() * availableNotes.length);
103 | notes.push(availableNotes[randomIndex]);
104 | }
105 |
106 | return notes;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/js/audio-generator.js:
--------------------------------------------------------------------------------
1 | // 音频生成器类
2 | class AudioGenerator {
3 | constructor() {
4 | this.audioContext = null;
5 | this.masterGain = null;
6 | this.init();
7 | }
8 |
9 | init() {
10 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
11 | this.masterGain = this.audioContext.createGain();
12 | this.masterGain.connect(this.audioContext.destination);
13 | }
14 |
15 | // 生成节拍器声音
16 | createMetronomeSound(isAccent = false) {
17 | const osc = this.audioContext.createOscillator();
18 | const gainNode = this.audioContext.createGain();
19 |
20 | osc.connect(gainNode);
21 | gainNode.connect(this.masterGain);
22 |
23 | // 重拍使用较低的频率和较大的音量
24 | osc.frequency.value = isAccent ? 880 : 440;
25 | gainNode.gain.value = isAccent ? 0.5 : 0.3;
26 |
27 | osc.start();
28 |
29 | // 快速衰减
30 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1);
31 | osc.stop(this.audioContext.currentTime + 0.1);
32 | }
33 |
34 | // 生成钢琴音色
35 | createPianoSound(frequency, duration = 1) {
36 | const osc = this.audioContext.createOscillator();
37 | const gainNode = this.audioContext.createGain();
38 |
39 | osc.connect(gainNode);
40 | gainNode.connect(this.masterGain);
41 |
42 | // 使用正弦波模拟钢琴音色
43 | osc.type = 'sine';
44 | osc.frequency.value = frequency;
45 |
46 | // 添加包络
47 | gainNode.gain.setValueAtTime(0.8, this.audioContext.currentTime);
48 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
49 |
50 | osc.start();
51 | osc.stop(this.audioContext.currentTime + duration);
52 | }
53 |
54 | // 音符名称转换为频率
55 | noteToFrequency(note) {
56 | const notes = {
57 | 'C': 0, 'C#': 1, 'Db': 1,
58 | 'D': 2, 'D#': 3, 'Eb': 3,
59 | 'E': 4,
60 | 'F': 5, 'F#': 6, 'Gb': 6,
61 | 'G': 7, 'G#': 8, 'Ab': 8,
62 | 'A': 9, 'A#': 10, 'Bb': 10,
63 | 'B': 11
64 | };
65 |
66 | // 处理音符名称,支持#和b
67 | let noteName = note.slice(0, -1);
68 | if (noteName.length > 1 && (noteName[1] === '#' || noteName[1] === 'b')) {
69 | noteName = noteName.slice(0, 2);
70 | } else {
71 | noteName = noteName[0];
72 | }
73 |
74 | const octave = parseInt(note.slice(-1));
75 |
76 | if (!notes.hasOwnProperty(noteName) || isNaN(octave)) {
77 | console.error('Invalid note:', note);
78 | return 440; // 返回默认频率 A4
79 | }
80 |
81 | // A4 = 440Hz
82 | const A4 = 440;
83 | const A4_INDEX = 9 + 4 * 12; // A4的MIDI音符编号
84 | const noteIndex = notes[noteName] + octave * 12;
85 |
86 | return A4 * Math.pow(2, (noteIndex - A4_INDEX) / 12);
87 | }
88 |
89 | // 播放音符
90 | playNote(note, duration = 1) {
91 | try {
92 | const frequency = this.noteToFrequency(note);
93 | if (isFinite(frequency) && frequency > 0) {
94 | this.createPianoSound(frequency, duration);
95 | } else {
96 | console.error('Invalid frequency for note:', note);
97 | }
98 | } catch (error) {
99 | console.error('Error playing note:', note, error);
100 | }
101 | }
102 |
103 | // 播放和弦
104 | playChord(notes, duration = 1) {
105 | try {
106 | notes.forEach(note => {
107 | if (note && typeof note === 'string') {
108 | this.playNote(note, duration);
109 | }
110 | });
111 | } catch (error) {
112 | console.error('Error playing chord:', notes, error);
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/css/article.css:
--------------------------------------------------------------------------------
1 | /* 文章页面样式 */
2 | .tutorial-article {
3 | padding-top: 80px;
4 | padding-bottom: var(--spacing-xl);
5 | }
6 |
7 | .article-header {
8 | text-align: center;
9 | margin-bottom: var(--spacing-xl);
10 | padding: var(--spacing-xl) 0;
11 | background: linear-gradient(135deg, #f8f9fa 0%, #e8f0fe 100%);
12 | border-radius: var(--border-radius);
13 | }
14 |
15 | .article-header h1 {
16 | font-size: 2.5rem;
17 | color: var(--text-primary);
18 | margin-bottom: var(--spacing-md);
19 | }
20 |
21 | .article-meta {
22 | color: var(--text-secondary);
23 | font-size: 0.9rem;
24 | display: flex;
25 | justify-content: center;
26 | gap: var(--spacing-md);
27 | }
28 |
29 | .article-content {
30 | max-width: 800px;
31 | margin: 0 auto;
32 | line-height: 1.8;
33 | }
34 |
35 | .content-section {
36 | margin-bottom: var(--spacing-xl);
37 | }
38 |
39 | .content-section h2 {
40 | font-size: 1.8rem;
41 | color: var(--text-primary);
42 | margin-bottom: var(--spacing-lg);
43 | padding-bottom: var(--spacing-sm);
44 | border-bottom: 2px solid var(--primary-color);
45 | }
46 |
47 | .content-section h3 {
48 | font-size: 1.4rem;
49 | color: var(--text-primary);
50 | margin: var(--spacing-lg) 0 var(--spacing-md);
51 | }
52 |
53 | .content-section p {
54 | margin-bottom: var(--spacing-md);
55 | color: var(--text-secondary);
56 | }
57 |
58 | .content-section ul,
59 | .content-section ol {
60 | margin-bottom: var(--spacing-md);
61 | padding-left: var(--spacing-lg);
62 | }
63 |
64 | .content-section li {
65 | margin-bottom: var(--spacing-sm);
66 | color: var(--text-secondary);
67 | }
68 |
69 | .tip-box {
70 | background: var(--background-dark);
71 | padding: var(--spacing-lg);
72 | border-radius: var(--border-radius);
73 | margin: var(--spacing-lg) 0;
74 | border-left: 4px solid var(--primary-color);
75 | }
76 |
77 | .tip-box h4 {
78 | color: var(--primary-color);
79 | margin-bottom: var(--spacing-sm);
80 | }
81 |
82 | .practice-suggestion {
83 | background: var(--background-dark);
84 | padding: var(--spacing-lg);
85 | border-radius: var(--border-radius);
86 | margin: var(--spacing-lg) 0;
87 | border-left: 4px solid var(--accent-color);
88 | }
89 |
90 | .practice-suggestion h4 {
91 | color: var(--accent-color);
92 | margin-bottom: var(--spacing-sm);
93 | }
94 |
95 | .practice-plan {
96 | background: var(--background-dark);
97 | padding: var(--spacing-lg);
98 | border-radius: var(--border-radius);
99 | margin: var(--spacing-lg) 0;
100 | }
101 |
102 | .article-footer {
103 | max-width: 800px;
104 | margin: var(--spacing-xl) auto 0;
105 | padding-top: var(--spacing-lg);
106 | border-top: 1px solid var(--background-dark);
107 | }
108 |
109 | .article-tags {
110 | margin-bottom: var(--spacing-lg);
111 | }
112 |
113 | .tag {
114 | display: inline-block;
115 | padding: var(--spacing-xs) var(--spacing-sm);
116 | background: var(--background-dark);
117 | color: var(--text-secondary);
118 | border-radius: var(--border-radius);
119 | margin-right: var(--spacing-xs);
120 | font-size: 0.9rem;
121 | }
122 |
123 | .article-navigation {
124 | display: flex;
125 | justify-content: space-between;
126 | gap: var(--spacing-md);
127 | }
128 |
129 | .nav-link {
130 | color: var(--primary-color);
131 | text-decoration: none;
132 | transition: var(--transition);
133 | }
134 |
135 | .nav-link:hover {
136 | color: var(--secondary-color);
137 | }
138 |
139 | /* 响应式设计 */
140 | @media (max-width: 768px) {
141 | .article-header h1 {
142 | font-size: 2rem;
143 | }
144 |
145 | .article-meta {
146 | flex-direction: column;
147 | gap: var(--spacing-xs);
148 | }
149 |
150 | .article-content {
151 | padding: 0 var(--spacing-md);
152 | }
153 |
154 | .article-navigation {
155 | flex-direction: column;
156 | align-items: center;
157 | text-align: center;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/tutorial.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
钢琴基础知识
20 |
21 |
22 |
23 |
24 |
25 | 音乐理论基础
26 |
27 | - 音程与和弦
28 | - 调式与调性
29 | - 节奏与拍子
30 | - 速度与力度记号
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 | 实用弹奏技巧
44 |
45 | - 踏板的使用方法
46 | - 装饰音的弹奏
47 | - 连音与断音技巧
48 | - 音色的控制方法
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
互动练习区
60 |
61 |
62 |
节奏训练
63 |
64 |
65 |
66 |
67 |
68 |
69 |
音符识别
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
进阶练习技巧
80 |
81 |
82 | 表现力提升
83 |
84 | - 情感表达方法
85 | - 音乐句子处理
86 | - 声部平衡控制
87 | - 音色变化技巧
88 |
89 |
90 |
91 |
92 | 练习效率提升
93 |
94 | - 科学的练习方法
95 | - 记忆力训练技巧
96 | - 难点突破方法
97 | - 演奏心理调节
98 |
99 |
100 |
101 |
102 |
103 |
104 |
118 |
119 |
--------------------------------------------------------------------------------
/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | https://piano-online.com/
6 | 2025-10-26
7 | daily
8 | 1.0
9 |
10 |
11 | https://piano-online.com/piano.html
12 | 2025-10-26
13 | weekly
14 | 0.9
15 |
16 |
17 | https://piano-online.com/practice.html
18 | 2025-10-26
19 | weekly
20 | 0.9
21 |
22 |
23 | https://piano-online.com/tutorials.html
24 | 2025-10-26
25 | weekly
26 | 0.8
27 |
28 |
29 | https://piano-online.com/about.html
30 | 2025-10-26
31 | monthly
32 | 0.6
33 |
34 |
35 |
36 |
37 | https://piano-online.com/articles/tutorials/piano-basics.html
38 | 2025-10-26
39 | monthly
40 | 0.8
41 |
42 |
43 | https://piano-online.com/articles/tutorials/reading-notes.html
44 | 2025-10-26
45 | monthly
46 | 0.8
47 |
48 |
49 | https://piano-online.com/articles/tutorials/finger-numbers.html
50 | 2025-10-26
51 | monthly
52 | 0.7
53 |
54 |
55 | https://piano-online.com/articles/tutorials/rhythm-basics.html
56 | 2025-10-26
57 | monthly
58 | 0.7
59 |
60 |
61 |
62 |
63 | https://piano-online.com/articles/tutorials/practice-methods.html
64 | 2025-10-26
65 | monthly
66 | 0.8
67 |
68 |
69 | https://piano-online.com/articles/tutorials/scales-practice.html
70 | 2025-10-26
71 | monthly
72 | 0.7
73 |
74 |
75 | https://piano-online.com/articles/tutorials/hand-coordination.html
76 | 2025-10-26
77 | monthly
78 | 0.7
79 |
80 |
81 |
82 |
83 | https://piano-online.com/articles/tutorials/music-theory.html
84 | 2025-10-26
85 | monthly
86 | 0.8
87 |
88 |
89 | https://piano-online.com/articles/tutorials/advanced-music-theory.html
90 | 2025-10-26
91 | monthly
92 | 0.7
93 |
94 |
95 |
96 |
97 | https://piano-online.com/articles/tutorials/sight-reading.html
98 | 2025-10-26
99 | monthly
100 | 0.7
101 |
102 |
103 | https://piano-online.com/articles/tutorials/performance-skills.html
104 | 2025-10-26
105 | monthly
106 | 0.7
107 |
108 |
109 | https://piano-online.com/articles/tutorials/music-analysis.html
110 | 2025-10-26
111 | monthly
112 | 0.7
113 |
114 |
115 | https://piano-online.com/articles/tutorials/piano-maintenance.html
116 | 2025-10-26
117 | monthly
118 | 0.6
119 |
120 |
--------------------------------------------------------------------------------
/IMPLEMENTATION_PLAN.md:
--------------------------------------------------------------------------------
1 | # Piano Online UX Optimization Implementation Plan
2 |
3 | ## Goal
4 | Improve user onboarding and comprehension to ensure users understand:
5 | 1. This is a computer keyboard piano simulator (not mouse-based)
6 | 2. How keyboard keys map to piano keys
7 | 3. Core functionality is visible on first screen
8 | 4. Terminology is clear and accessible
9 |
10 | ## Stage 1: Homepage Layout Restructure ✅
11 | **Goal**: Move piano keyboard to first screen (above the fold)
12 | **Success Criteria**:
13 | - Piano visible without scrolling on desktop (1920x1080) and tablets (768px+)
14 | - Hero text reduced to 1-2 lines
15 | - CTAs simplified to 3 clear buttons
16 |
17 | **Changes**:
18 | - Compress hero section to ~200px height
19 | - Remove verbose description from hero
20 | - Move piano-container from line 196 to immediately after hero (line ~110)
21 | - Add min-height constraints to ensure visibility
22 |
23 | **Status**: ✅ Completed
24 |
25 | ## Stage 2: Keyboard Key Labels on Piano Keys
26 | **Goal**: Show computer keyboard letters (A, S, D, F...) directly on piano keys
27 | **Success Criteria**:
28 | - Each white key shows corresponding keyboard letter
29 | - Labels are large, high-contrast, and always visible
30 | - Black keys show labels when applicable
31 |
32 | **Implementation**:
33 | - ✅ Modified `js/piano.js` generateKeyboard() to add key labels
34 | - ✅ Added CSS `.key-label` styling in piano.css
35 | - ✅ Labels display uppercase letters for white keys (A, S, D...)
36 | - ✅ Labels display numbers for black keys (1, 2, 4, 5...)
37 |
38 | **Status**: ✅ Completed
39 |
40 | ## Stage 3: First-Time User Guide Overlay
41 | **Goal**: Interactive tutorial that teaches users to press keyboard keys
42 | **Success Criteria**:
43 | - Shows on first visit (localStorage flag)
44 | - Animated highlight showing "Press 'A' key on your keyboard"
45 | - Dismissible but can be reopened via help button
46 | - Step-by-step guide (3-4 steps maximum)
47 |
48 | **Implementation**:
49 | - ✅ Created `js/tutorial.js` with Tutorial class
50 | - ✅ Added tutorial overlay HTML in index.html
51 | - ✅ Tutorial checks localStorage for completion flag
52 | - ✅ Shows after 1 second delay on first visit
53 | - ✅ "A" key demo with pulsing animation
54 | - ✅ 3-step guide explaining: labels → song selection → practice
55 | - ✅ Skip and Start buttons both dismiss tutorial
56 | - ✅ Auto-completes when user presses 'A' key
57 |
58 | **Status**: ✅ Completed
59 |
60 | ## Stage 4: Simplify Terminology & Add Tooltips
61 | **Goal**: Replace technical terms with beginner-friendly language
62 | **Success Criteria**:
63 | - All buttons and labels are self-explanatory
64 | - Tooltips explain advanced features
65 | - No musical jargon without explanation
66 |
67 | **Changes**:
68 | - ✅ "延音踏板" → "声音延长 🎵" with tooltip
69 | - ✅ "节奏大师模式" → "🎮 游戏模式" with tooltip
70 | - ✅ "开始练习" → "✨ 跟着提示弹" with tooltip
71 | - ✅ Added emoji to song options with difficulty labels
72 | - ✅ Implemented CSS tooltip system with [data-tooltip]
73 |
74 | **Status**: ✅ Completed
75 |
76 | ## Stage 5: Enhanced Visual Feedback
77 | **Goal**: Clear visual response when users press correct/incorrect keys
78 | **Success Criteria**:
79 | - Correct key press: green glow + animation
80 | - Incorrect key press: gentle shake + red border
81 | - Immediate visual feedback
82 |
83 | **Implementation**:
84 | - ✅ Added `.key.success` animation with green glow in piano.css
85 | - ✅ Added `.key.error` animation with shake effect in piano.css
86 | - ✅ Modified `practice-mode.js` handleNotePlayed() to add classes
87 | - ✅ Animations trigger on correct/incorrect key presses
88 | - ✅ Added completion message with accuracy stats
89 |
90 | **Status**: ✅ Completed
91 |
92 | ## Stage 6: Testing & Verification
93 | **Goal**: Ensure all changes work correctly
94 | **Success Criteria**:
95 | - First-time user can understand within 10 seconds
96 | - Piano keyboard visible on first screen (no scroll)
97 | - All tooltips work
98 | - Tutorial overlay functions properly
99 | - Responsive on mobile, tablet, desktop
100 |
101 | **Tests**:
102 | - [ ] Desktop 1920x1080 - piano visible without scroll
103 | - [ ] Tablet 768px - piano visible without scroll
104 | - [ ] Mobile 375px - piano visible (may need horizontal scroll)
105 | - [ ] Tutorial shows on first visit
106 | - [ ] Tutorial doesn't show on return visits
107 | - [ ] Keyboard labels visible on all keys
108 | - [ ] Tooltips appear on hover
109 |
110 | **Status**: Pending
111 |
112 | ---
113 |
114 | ## Implementation Order
115 | 1. Stage 1: Layout (highest impact, foundation for other changes)
116 | 2. Stage 2: Key labels (critical for understanding)
117 | 3. Stage 4: Terminology (quick wins, improves clarity)
118 | 4. Stage 3: Tutorial overlay (requires other elements in place)
119 | 5. Stage 5: Visual feedback (polish)
120 | 6. Stage 6: Testing
121 |
122 | ## Rollback Plan
123 | - Keep backup of original index.html as `index.backup.html`
124 | - Test changes locally before deployment
125 | - Each stage can be independently rolled back via git
126 |
--------------------------------------------------------------------------------
/SEO-OPTIMIZATION-GUIDE.md:
--------------------------------------------------------------------------------
1 | # Piano Online - SEO优化与流量提升指南
2 |
3 | ## 📊 当前状态
4 | - **日UV**: <100
5 | - **优化日期**: 2025-10-26
6 | - **目标**: 2周内UV翻倍至200+
7 |
8 | ---
9 |
10 | ## ✅ 已完成的技术优化
11 |
12 | ### 1. Sitemap优化
13 | - ✅ 更新sitemap.xml,包含所有18个页面
14 | - ✅ 更新日期为2025-10-26
15 | - ✅ 设置合理的优先级和更新频率
16 | - **下一步**: 提交到Google Search Console和Bing Webmaster Tools
17 |
18 | ### 2. Open Graph和Twitter卡片
19 | - ✅ 所有页面添加完整的OG标签
20 | - ✅ 添加Twitter Card标签
21 | - ✅ 优化社交媒体分享效果
22 | - **效果**: 提升社交媒体分享点击率30-50%
23 |
24 | ### 3. 结构化数据(Schema.org)
25 | - ✅ 首页添加WebApplication结构化数据
26 | - ✅ 首页添加Course结构化数据
27 | - ✅ 文章页面添加Article类型标记
28 | - **效果**: 提升搜索结果展示效果,增加点击率
29 |
30 | ### 4. 性能优化
31 | - ✅ 添加DNS预解析(preconnect)
32 | - ✅ 优化资源加载顺序
33 | - **建议**: 添加图片懒加载,压缩CSS/JS
34 |
35 | ---
36 |
37 | ## 🚀 立即执行的营销策略
38 |
39 | ### 第1周: 搜索引擎优化
40 |
41 | #### Day 1-2: 提交到搜索引擎
42 | ```bash
43 | 1. Google Search Console
44 | - 提交sitemap: https://piano-online.com/sitemap.xml
45 | - 请求索引所有18个页面
46 | - 检查移动端适配性
47 |
48 | 2. Bing Webmaster Tools
49 | - 提交sitemap
50 | - 验证站点所有权
51 |
52 | 3. 百度站长平台
53 | - 提交sitemap
54 | - 提交URL
55 | ```
56 |
57 | #### Day 3-5: 内容优化
58 | - [ ] 为每篇教程文章添加"相关推荐"模块
59 | - [ ] 在首页添加"热门教程"板块
60 | - [ ] 添加面包屑导航
61 | - [ ] 优化内链结构
62 |
63 | #### Day 6-7: 外链建设
64 | - [ ] 提交到音乐教育网站目录
65 | - [ ] 在知乎回答钢琴相关问题,带链接
66 | - [ ] 在小红书发布钢琴学习笔记
67 |
68 | ### 第2周: 内容营销
69 |
70 | #### 知乎营销策略
71 | **目标问题**:
72 | 1. "零基础如何自学钢琴?"
73 | 2. "有哪些好用的在线钢琴学习网站?"
74 | 3. "钢琴初学者应该如何练习?"
75 | 4. "在线钢琴和真实钢琴有什么区别?"
76 |
77 | **回答模板**:
78 | ```
79 | 作为一个钢琴爱好者,我推荐几个方法:
80 |
81 | 1. [分享实用建议]
82 | 2. [分享个人经验]
83 | 3. [推荐工具] 我平时用Piano Online (piano-online.com) 练习...
84 |
85 | [详细说明优势]
86 | ```
87 |
88 | #### 小红书营销策略
89 | **内容方向**:
90 | - "每天10分钟,在线学钢琴"
91 | - "上班族的钢琴学习日记"
92 | - "免费在线钢琴工具分享"
93 | - "钢琴入门必看教程"
94 |
95 | **发布频率**: 每周3-5篇
96 |
97 | #### B站营销策略
98 | - 制作钢琴教学视频
99 | - 在视频描述中添加网站链接
100 | - 制作"在线钢琴使用教程"
101 |
102 | ---
103 |
104 | ## 🎯 长尾关键词策略
105 |
106 | ### 当前问题
107 | 主关键词"在线钢琴"竞争激烈,难以排名
108 |
109 | ### 解决方案: 长尾关键词布局
110 |
111 | #### 高价值长尾词(建议创建新页面)
112 | 1. **"钢琴指法练习在线"** - 搜索量中等,竞争低
113 | 2. **"五线谱快速入门教程"** - 搜索量高,竞争中等
114 | 3. **"钢琴和弦练习工具"** - 搜索量中等,竞争低
115 | 4. **"儿童钢琴启蒙教程"** - 搜索量高,竞争中等
116 | 5. **"钢琴考级曲目练习"** - 搜索量高,竞争中等
117 |
118 | #### 实施方案
119 | ```
120 | 创建新页面:
121 | - /practice/finger-exercises.html (钢琴指法练习)
122 | - /tutorials/chord-practice.html (和弦练习教程)
123 | - /tutorials/kids-piano.html (儿童钢琴启蒙)
124 | - /practice/exam-songs.html (考级曲目练习)
125 | ```
126 |
127 | ---
128 |
129 | ## 📈 数据追踪与分析
130 |
131 | ### Google Analytics关键指标
132 | 监控以下数据:
133 | - [ ] 每日UV/PV
134 | - [ ] 跳出率(目标: <60%)
135 | - [ ] 平均停留时间(目标: >2分钟)
136 | - [ ] 热门页面
137 | - [ ] 流量来源
138 |
139 | ### Google Search Console关键指标
140 | - [ ] 搜索展示次数
141 | - [ ] 点击次数
142 | - [ ] 平均排名
143 | - [ ] 热门搜索词
144 |
145 | ### 每周检查清单
146 | ```
147 | 周一:
148 | - 查看上周UV数据
149 | - 分析流量来源
150 | - 检查新增索引页面
151 |
152 | 周三:
153 | - 发布1-2篇知乎回答
154 | - 发布1篇小红书笔记
155 |
156 | 周五:
157 | - 检查外链建设进度
158 | - 优化表现差的页面
159 | ```
160 |
161 | ---
162 |
163 | ## 🔧 技术优化待办事项
164 |
165 | ### 高优先级
166 | - [ ] 创建og-image.jpg (1200x630px)
167 | - [ ] 添加图片懒加载
168 | - [ ] 压缩CSS和JS文件
169 | - [ ] 添加Service Worker (PWA)
170 | - [ ] 优化移动端体验
171 |
172 | ### 中优先级
173 | - [ ] 添加面包屑导航
174 | - [ ] 创建404错误页面
175 | - [ ] 添加网站地图HTML版本
176 | - [ ] 优化字体加载
177 |
178 | ### 低优先级
179 | - [ ] 添加深色模式
180 | - [ ] 添加多语言支持
181 | - [ ] 集成社交分享按钮
182 |
183 | ---
184 |
185 | ## 💡 内容创作计划
186 |
187 | ### 每月新增内容
188 | **目标**: 每月新增4-8篇高质量教程
189 |
190 | #### 11月内容计划
191 | 1. "钢琴考级1-10级曲目推荐"
192 | 2. "成人零基础学钢琴完整指南"
193 | 3. "钢琴练习常见错误及纠正方法"
194 | 4. "如何选择适合自己的钢琴教材"
195 | 5. "钢琴即兴伴奏入门教程"
196 | 6. "流行歌曲钢琴改编技巧"
197 | 7. "钢琴演奏中的情感表达"
198 | 8. "如何提高钢琴视奏能力"
199 |
200 | #### 内容创作原则
201 | - 每篇文章1500-3000字
202 | - 包含实用的练习方法
203 | - 添加图片或视频演示
204 | - 优化SEO关键词
205 | - 添加内链到相关教程
206 |
207 | ---
208 |
209 | ## 🎁 用户增长策略
210 |
211 | ### 1. 社群运营
212 | - 创建微信群/QQ群
213 | - 定期分享学习资料
214 | - 组织线上打卡活动
215 |
216 | ### 2. 激励机制
217 | - 添加学习打卡功能
218 | - 设置学习成就系统
219 | - 提供学习证书
220 |
221 | ### 3. 用户留存
222 | - 发送每周学习报告
223 | - 推送个性化练习建议
224 | - 创建学习社区
225 |
226 | ---
227 |
228 | ## 📊 预期效果时间表
229 |
230 | ### 第1周
231 | - Google收录所有页面
232 | - 社交媒体分享效果提升
233 |
234 | ### 第2周
235 | - 开始出现长尾词排名
236 | - UV增长20-30%
237 |
238 | ### 第4周
239 | - 主要关键词排名提升
240 | - UV增长50-100%
241 |
242 | ### 第8周
243 | - 稳定的自然流量
244 | - UV达到200-300
245 |
246 | ### 第12周
247 | - 建立品牌认知度
248 | - UV达到500+
249 |
250 | ---
251 |
252 | ## ⚠️ 注意事项
253 |
254 | ### 避免的错误
255 | 1. ❌ 不要购买垃圾外链
256 | 2. ❌ 不要过度堆砌关键词
257 | 3. ❌ 不要抄袭其他网站内容
258 | 4. ❌ 不要忽视移动端体验
259 | 5. ❌ 不要频繁修改标题和描述
260 |
261 | ### 最佳实践
262 | 1. ✅ 持续产出高质量内容
263 | 2. ✅ 积极回应用户反馈
264 | 3. ✅ 定期更新旧内容
265 | 4. ✅ 建立自然的外链
266 | 5. ✅ 优化用户体验
267 |
268 | ---
269 |
270 | ## 📞 需要帮助?
271 |
272 | 如果在执行过程中遇到问题:
273 | 1. 检查Google Search Console错误报告
274 | 2. 使用PageSpeed Insights检测性能
275 | 3. 使用Google Rich Results Test验证结构化数据
276 | 4. 使用Mobile-Friendly Test检查移动端适配
277 |
278 | ---
279 |
280 | ## 🎯 本月目标
281 |
282 | - [ ] 完成所有技术优化
283 | - [ ] 发布10篇知乎回答
284 | - [ ] 发布15篇小红书笔记
285 | - [ ] 新增4篇教程文章
286 | - [ ] UV达到150+
287 |
288 | **记住**: SEO是长期工作,坚持3个月才能看到显著效果!
289 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | const songs = {
2 | 'twinkle': {
3 | notes: ['C4', 'C4', 'G4', 'G4', 'A4', 'A4', 'G4'],
4 | name: '小星星'
5 | },
6 | 'happy-birthday': {
7 | notes: ['C4', 'C4', 'D4', 'C4', 'F4', 'E4'],
8 | name: '生日快乐'
9 | }
10 | };
11 |
12 | class Piano {
13 | constructor() {
14 | // 初始化音频上下文
15 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
16 | this.masterGain = this.audioContext.createGain();
17 | this.masterGain.connect(this.audioContext.destination);
18 |
19 | // 钢琴音色配置
20 | this.samples = {};
21 | this.isLoading = true;
22 | this.loadSamples();
23 |
24 | // 当前按下的键
25 | this.activeNotes = new Map();
26 |
27 | // 录制相关
28 | this.isRecording = false;
29 | this.recordedNotes = [];
30 | this.startTime = null;
31 |
32 | this.initializeListeners();
33 | }
34 |
35 | async loadSamples() {
36 | const notes = [
37 | 'A0', 'C1', 'D#1', 'F#1', 'A1', 'C2', 'D#2', 'F#2', 'A2', 'C3', 'D#3', 'F#3',
38 | 'A3', 'C4', 'D#4', 'F#4', 'A4', 'C5', 'D#5', 'F#5', 'A5', 'C6', 'D#6', 'F#6', 'A6'
39 | ];
40 |
41 | try {
42 | const loadPromises = notes.map(async note => {
43 | const response = await fetch(`samples/piano-${note}.mp3`);
44 | const arrayBuffer = await response.arrayBuffer();
45 | const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
46 | this.samples[note] = audioBuffer;
47 | });
48 |
49 | await Promise.all(loadPromises);
50 | this.isLoading = false;
51 | console.log('Piano samples loaded successfully');
52 | } catch (error) {
53 | console.error('Error loading piano samples:', error);
54 | }
55 | }
56 |
57 | getNearestSample(note) {
58 | // 获取最接近的采样音符
59 | const noteNumber = this.getNoteNumber(note);
60 | const samples = Object.keys(this.samples);
61 | let closest = samples[0];
62 |
63 | samples.forEach(sample => {
64 | if (Math.abs(this.getNoteNumber(sample) - noteNumber) <
65 | Math.abs(this.getNoteNumber(closest) - noteNumber)) {
66 | closest = sample;
67 | }
68 | });
69 |
70 | return this.samples[closest];
71 | }
72 |
73 | initializeListeners() {
74 | document.addEventListener('keydown', (e) => {
75 | if (e.repeat) return;
76 | this.handleKeyDown(e);
77 | });
78 |
79 | document.addEventListener('keyup', (e) => {
80 | this.handleKeyUp(e);
81 | });
82 |
83 | document.getElementById('record-btn').addEventListener('click', () => {
84 | this.toggleRecording();
85 | });
86 |
87 | document.getElementById('play-btn').addEventListener('click', () => {
88 | this.playRecording();
89 | });
90 | }
91 |
92 | handleKeyDown(e) {
93 | const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
94 | if (!key) return;
95 |
96 | const note = key.dataset.note;
97 | this.playNote(note);
98 | key.classList.add('active');
99 |
100 | if (this.isRecording) {
101 | this.recordNote(note);
102 | }
103 | }
104 |
105 | handleKeyUp(e) {
106 | const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
107 | if (key) {
108 | key.classList.remove('active');
109 | }
110 | }
111 |
112 | playNote(note) {
113 | const sample = this.getNearestSample(note);
114 | const source = this.audioContext.createBufferSource();
115 | source.buffer = sample;
116 | source.connect(this.masterGain);
117 | source.start();
118 | }
119 |
120 | toggleRecording() {
121 | this.isRecording = !this.isRecording;
122 | const btn = document.getElementById('record-btn');
123 |
124 | if (this.isRecording) {
125 | this.recordedNotes = [];
126 | this.startTime = Date.now();
127 | btn.textContent = "停止录制";
128 | btn.classList.add('recording');
129 | } else {
130 | btn.textContent = "开始录制";
131 | btn.classList.remove('recording');
132 | }
133 | }
134 |
135 | recordNote(note) {
136 | const time = Date.now() - this.startTime;
137 | this.recordedNotes.push({
138 | note,
139 | time
140 | });
141 | }
142 |
143 | playRecording() {
144 | if (this.recordedNotes.length === 0) return;
145 |
146 | const now = Tone.now();
147 | this.recordedNotes.forEach(({note, time}) => {
148 | this.playNote(note);
149 | });
150 | }
151 | }
152 |
153 | // 等待页面加载完成后初始化钢琴
154 | window.addEventListener('load', () => {
155 | new Piano();
156 | });
--------------------------------------------------------------------------------
/QUICK-ACTION-CHECKLIST.md:
--------------------------------------------------------------------------------
1 | # 🚀 Piano Online - 快速行动清单
2 |
3 | > **目标**: 2周内UV从100提升到200+
4 |
5 | ---
6 |
7 | ## 📋 今天就做 (Day 1)
8 |
9 | ### 1. 提交到搜索引擎 ⏰ 30分钟
10 | - [ ] 登录 [Google Search Console](https://search.google.com/search-console)
11 | - 添加网站: piano-online.com
12 | - 提交sitemap: `https://piano-online.com/sitemap.xml`
13 | - 请求索引所有页面
14 |
15 | - [ ] 登录 [Bing Webmaster Tools](https://www.bing.com/webmasters)
16 | - 添加网站
17 | - 提交sitemap
18 |
19 | - [ ] 登录 [百度站长平台](https://ziyuan.baidu.com)
20 | - 验证网站
21 | - 提交sitemap
22 |
23 | ### 2. 创建社交媒体图片 ⏰ 20分钟
24 | - [ ] 使用Canva创建og-image.jpg (1200x630px)
25 | - 包含网站logo
26 | - 添加标语: "免费在线钢琴学习平台"
27 | - 保存到 `/images/og-image.jpg`
28 |
29 | ### 3. 验证优化效果 ⏰ 15分钟
30 | - [ ] 使用 [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
31 | - 测试首页分享效果
32 |
33 | - [ ] 使用 [Twitter Card Validator](https://cards-dev.twitter.com/validator)
34 | - 测试Twitter卡片效果
35 |
36 | - [ ] 使用 [Google Rich Results Test](https://search.google.com/test/rich-results)
37 | - 验证结构化数据
38 |
39 | ---
40 |
41 | ## 📅 本周任务 (Week 1)
42 |
43 | ### 周一: 搜索引擎优化
44 | - [x] 提交sitemap到各大搜索引擎
45 | - [ ] 在Google Search Console请求索引所有页面
46 | - [ ] 检查移动端适配性
47 |
48 | ### 周二: 知乎营销
49 | **目标**: 回答3个问题
50 |
51 | 推荐问题:
52 | 1. [零基础如何自学钢琴?](https://www.zhihu.com/search?q=零基础学钢琴)
53 | 2. [有哪些好用的在线钢琴网站?](https://www.zhihu.com/search?q=在线钢琴网站)
54 | 3. [钢琴初学者练习方法?](https://www.zhihu.com/search?q=钢琴初学者练习)
55 |
56 | **回答模板**:
57 | ```
58 | 我学钢琴X年了,分享几个实用建议:
59 |
60 | 1. [具体建议1]
61 | 2. [具体建议2]
62 | 3. [具体建议3]
63 |
64 | 推荐一个我常用的工具: Piano Online (piano-online.com)
65 | - 优点1
66 | - 优点2
67 | - 优点3
68 |
69 | [分享使用心得]
70 | ```
71 |
72 | ### 周三: 小红书营销
73 | **目标**: 发布2篇笔记
74 |
75 | 内容方向:
76 | 1. "上班族的钢琴学习日记 | 每天10分钟进步看得见"
77 | 2. "免费在线钢琴工具分享 | 不花钱也能学钢琴"
78 |
79 | **发布技巧**:
80 | - 使用9宫格图片
81 | - 添加热门话题标签: #钢琴学习 #在线学习 #零基础学钢琴
82 | - 在评论区置顶网站链接
83 |
84 | ### 周四: 内容优化
85 | - [ ] 为首页添加"热门教程"板块
86 | - [ ] 为教程页面添加"相关推荐"
87 | - [ ] 优化移动端显示效果
88 |
89 | ### 周五: 外链建设
90 | 提交到以下网站目录:
91 | - [ ] [音乐教育导航](https://www.google.com/search?q=音乐教育网站导航)
92 | - [ ] [在线工具大全](https://www.google.com/search?q=在线工具导航)
93 | - [ ] [学习资源网](https://www.google.com/search?q=学习资源导航网站)
94 |
95 | ### 周末: 数据分析
96 | - [ ] 查看Google Analytics数据
97 | - [ ] 分析哪些页面表现好
98 | - [ ] 制定下周优化计划
99 |
100 | ---
101 |
102 | ## 📅 第2周任务 (Week 2)
103 |
104 | ### 周一-周三: 持续内容营销
105 | - [ ] 知乎回答5个问题
106 | - [ ] 小红书发布3篇笔记
107 | - [ ] 在相关论坛发帖
108 |
109 | ### 周四-周五: 新内容创作
110 | 创建2篇新教程:
111 | - [ ] "钢琴考级曲目推荐与练习方法"
112 | - [ ] "成人零基础学钢琴30天计划"
113 |
114 | ### 周末: 效果评估
115 | - [ ] 对比两周数据
116 | - [ ] 分析流量来源
117 | - [ ] 调整营销策略
118 |
119 | ---
120 |
121 | ## 🎯 每日15分钟任务
122 |
123 | ### 早上 (5分钟)
124 | - [ ] 查看Google Analytics昨日数据
125 | - [ ] 检查是否有新的搜索词排名
126 |
127 | ### 中午 (5分钟)
128 | - [ ] 回复用户评论/反馈
129 | - [ ] 在社交媒体互动
130 |
131 | ### 晚上 (5分钟)
132 | - [ ] 发布1条社交媒体内容
133 | - [ ] 或回答1个知乎问题
134 |
135 | ---
136 |
137 | ## 📊 关键指标监控
138 |
139 | ### 每天检查
140 | - [ ] UV数量
141 | - [ ] 跳出率
142 | - [ ] 平均停留时间
143 |
144 | ### 每周检查
145 | - [ ] Google收录页面数
146 | - [ ] 关键词排名变化
147 | - [ ] 外链数量增长
148 |
149 | ### 目标值
150 | | 指标 | 当前 | 1周后 | 2周后 |
151 | |------|------|-------|-------|
152 | | 日UV | <100 | 120+ | 200+ |
153 | | 跳出率 | ? | <65% | <60% |
154 | | 停留时间 | ? | >1.5分钟 | >2分钟 |
155 | | 收录页面 | ? | 18页 | 18页 |
156 |
157 | ---
158 |
159 | ## 💰 零成本营销渠道
160 |
161 | ### 1. 知乎 (最重要!)
162 | - 每周回答5-10个问题
163 | - 关注钢琴相关话题
164 | - 点赞优质回答建立人脉
165 |
166 | ### 2. 小红书
167 | - 每周发布3-5篇笔记
168 | - 使用热门标签
169 | - 与其他博主互动
170 |
171 | ### 3. 微信公众号
172 | - 转载网站教程
173 | - 添加原文链接
174 | - 引导用户访问网站
175 |
176 | ### 4. B站
177 | - 制作钢琴教学视频
178 | - 在简介添加网站链接
179 | - 回复评论引导访问
180 |
181 | ### 5. 抖音/快手
182 | - 发布钢琴演奏短视频
183 | - 在个人简介添加网站
184 | - 引导用户搜索"Piano Online"
185 |
186 | ### 6. 豆瓣小组
187 | - 加入钢琴学习小组
188 | - 分享学习经验
189 | - 适度推广网站
190 |
191 | ---
192 |
193 | ## ⚡ 快速增长技巧
194 |
195 | ### 技巧1: 蹭热点
196 | 关注以下热点话题:
197 | - 钢琴考级季节(每年6月、12月)
198 | - 开学季(9月)
199 | - 寒暑假(1月、7-8月)
200 |
201 | ### 技巧2: 互推合作
202 | 寻找以下类型的网站合作:
203 | - 音乐教育网站
204 | - 在线学习平台
205 | - 教育资源网站
206 |
207 | ### 技巧3: 用户激励
208 | - 鼓励用户分享到朋友圈
209 | - 设置分享奖励机制
210 | - 创建学习打卡活动
211 |
212 | ### 技巧4: SEO优化
213 | - 每篇文章包含3-5个长尾关键词
214 | - 文章标题包含数字(如"10个技巧")
215 | - 添加清晰的小标题
216 |
217 | ---
218 |
219 | ## 🚫 避免的错误
220 |
221 | 1. ❌ 不要在短时间内发布大量低质量外链
222 | 2. ❌ 不要复制粘贴其他网站内容
223 | 3. ❌ 不要过度推广,引起用户反感
224 | 4. ❌ 不要忽视用户反馈
225 | 5. ❌ 不要三天打鱼两天晒网
226 |
227 | ---
228 |
229 | ## ✅ 成功的标志
230 |
231 | ### 2周后你应该看到:
232 | - ✅ Google收录所有18个页面
233 | - ✅ 至少10个长尾词有排名
234 | - ✅ 知乎回答获得100+赞同
235 | - ✅ 小红书笔记获得500+浏览
236 | - ✅ 日UV达到150-200
237 |
238 | ### 1个月后你应该看到:
239 | - ✅ 主关键词进入前5页
240 | - ✅ 日UV稳定在200-300
241 | - ✅ 有用户主动推荐网站
242 | - ✅ 搜索品牌词能找到网站
243 |
244 | ### 3个月后你应该看到:
245 | - ✅ 日UV达到500+
246 | - ✅ 多个关键词排名前3页
247 | - ✅ 建立稳定的用户群体
248 | - ✅ 有自然的外链增长
249 |
250 | ---
251 |
252 | ## 📞 遇到问题?
253 |
254 | ### 常见问题解决方案
255 |
256 | **Q: Google一直不收录怎么办?**
257 | A:
258 | 1. 检查robots.txt是否正确
259 | 2. 在Google Search Console手动提交
260 | 3. 增加外链帮助蜘蛛发现
261 | 4. 确保网站可以正常访问
262 |
263 | **Q: 知乎回答没人看怎么办?**
264 | A:
265 | 1. 选择浏览量高的问题
266 | 2. 写在前3个回答
267 | 3. 回答要详细(1000字+)
268 | 4. 添加图片和案例
269 |
270 | **Q: 流量增长很慢怎么办?**
271 | A:
272 | 1. 检查网站加载速度
273 | 2. 优化移动端体验
274 | 3. 增加内容更新频率
275 | 4. 扩大营销渠道
276 |
277 | ---
278 |
279 | ## 🎉 开始行动!
280 |
281 | **记住**:
282 | - 今天就完成Day 1的任务
283 | - 每天坚持15分钟
284 | - 2周后你会看到明显效果
285 | - 3个月后流量会有质的飞跃
286 |
287 | **现在就开始第一步: 提交sitemap到Google Search Console!**
288 |
289 | ---
290 |
291 | 最后更新: 2025-10-26
292 |
--------------------------------------------------------------------------------
/css/practice.css:
--------------------------------------------------------------------------------
1 | /* 练习页面样式 */
2 | .practice-tools {
3 | padding: 60px 0;
4 | background-color: #f8f9fa;
5 | }
6 |
7 | .practice-tools h1 {
8 | text-align: center;
9 | color: #333;
10 | font-size: 2.5rem;
11 | margin-bottom: 40px;
12 | }
13 |
14 | .tool-card {
15 | background: #fff;
16 | border-radius: 10px;
17 | padding: 30px;
18 | margin-bottom: 30px;
19 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
20 | }
21 |
22 | .tool-card h2 {
23 | color: #333;
24 | font-size: 1.5rem;
25 | margin-bottom: 20px;
26 | border-bottom: 2px solid #007bff;
27 | padding-bottom: 10px;
28 | }
29 |
30 | /* 节拍器样式 */
31 | .metronome {
32 | text-align: center;
33 | padding: 20px;
34 | }
35 |
36 | .metronome-display {
37 | font-size: 2rem;
38 | margin-bottom: 20px;
39 | }
40 |
41 | .tempo {
42 | color: #007bff;
43 | font-weight: bold;
44 | }
45 |
46 | .metronome-controls {
47 | display: flex;
48 | justify-content: center;
49 | gap: 20px;
50 | margin-bottom: 20px;
51 | }
52 |
53 | .metronome-controls button {
54 | padding: 10px 20px;
55 | font-size: 1.2rem;
56 | border: none;
57 | border-radius: 5px;
58 | cursor: pointer;
59 | transition: all 0.3s;
60 | }
61 |
62 | .start-stop {
63 | background-color: #007bff;
64 | color: white;
65 | }
66 |
67 | .tempo-up, .tempo-down {
68 | background-color: #e9ecef;
69 | color: #333;
70 | }
71 |
72 | .time-signature select {
73 | padding: 5px 10px;
74 | font-size: 1rem;
75 | border-radius: 5px;
76 | border: 1px solid #ddd;
77 | }
78 |
79 | /* 音阶练习样式 */
80 | .scale-practice {
81 | text-align: center;
82 | padding: 20px;
83 | }
84 |
85 | .scale-selector {
86 | margin-bottom: 20px;
87 | }
88 |
89 | .key-select {
90 | padding: 8px 15px;
91 | font-size: 1rem;
92 | border-radius: 5px;
93 | border: 1px solid #ddd;
94 | }
95 |
96 | .keyboard {
97 | height: 120px;
98 | background: #f8f9fa;
99 | border-radius: 5px;
100 | margin-bottom: 20px;
101 | position: relative;
102 | }
103 |
104 | .play-scale {
105 | padding: 10px 20px;
106 | font-size: 1.1rem;
107 | background-color: #28a745;
108 | color: white;
109 | border: none;
110 | border-radius: 5px;
111 | cursor: pointer;
112 | transition: all 0.3s;
113 | }
114 |
115 | /* 和弦练习样式 */
116 | .chord-practice {
117 | text-align: center;
118 | padding: 20px;
119 | }
120 |
121 | .chord-selector {
122 | margin-bottom: 20px;
123 | }
124 |
125 | .chord-type {
126 | padding: 8px 15px;
127 | font-size: 1rem;
128 | border-radius: 5px;
129 | border: 1px solid #ddd;
130 | }
131 |
132 | .play-chord {
133 | padding: 10px 20px;
134 | font-size: 1.1rem;
135 | background-color: #6f42c1;
136 | color: white;
137 | border: none;
138 | border-radius: 5px;
139 | cursor: pointer;
140 | transition: all 0.3s;
141 | }
142 |
143 | /* 视奏训练样式 */
144 | .sight-reading {
145 | text-align: center;
146 | padding: 20px;
147 | }
148 |
149 | .difficulty-selector {
150 | margin-bottom: 20px;
151 | }
152 |
153 | .difficulty-selector select {
154 | padding: 8px 15px;
155 | font-size: 1rem;
156 | border-radius: 5px;
157 | border: 1px solid #ddd;
158 | }
159 |
160 | .staff-display {
161 | height: 200px;
162 | background: #fff;
163 | border: 1px solid #ddd;
164 | border-radius: 5px;
165 | margin-bottom: 20px;
166 | }
167 |
168 | .generate-notes {
169 | padding: 10px 20px;
170 | font-size: 1.1rem;
171 | background-color: #fd7e14;
172 | color: white;
173 | border: none;
174 | border-radius: 5px;
175 | cursor: pointer;
176 | transition: all 0.3s;
177 | }
178 |
179 | /* 练习建议样式 */
180 | .practice-tips {
181 | padding: 60px 0;
182 | background-color: #fff;
183 | }
184 |
185 | .practice-tips h2 {
186 | text-align: center;
187 | color: #333;
188 | font-size: 2rem;
189 | margin-bottom: 40px;
190 | }
191 |
192 | .tips-grid {
193 | display: grid;
194 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
195 | gap: 30px;
196 | }
197 |
198 | .tip-card {
199 | background: #f8f9fa;
200 | border-radius: 10px;
201 | padding: 30px;
202 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
203 | }
204 |
205 | .tip-card h3 {
206 | color: #333;
207 | font-size: 1.3rem;
208 | margin-bottom: 20px;
209 | border-bottom: 2px solid #007bff;
210 | padding-bottom: 10px;
211 | }
212 |
213 | .tip-card ul {
214 | list-style: none;
215 | padding: 0;
216 | }
217 |
218 | .tip-card li {
219 | color: #666;
220 | margin-bottom: 10px;
221 | padding-left: 20px;
222 | position: relative;
223 | }
224 |
225 | .tip-card li::before {
226 | content: "•";
227 | color: #007bff;
228 | position: absolute;
229 | left: 0;
230 | }
231 |
232 | /* 按钮悬停效果 */
233 | button:hover {
234 | opacity: 0.9;
235 | transform: translateY(-2px);
236 | }
237 |
238 | /* 响应式设计 */
239 | @media (max-width: 768px) {
240 | .practice-tools h1 {
241 | font-size: 2rem;
242 | }
243 |
244 | .tool-card {
245 | padding: 20px;
246 | }
247 |
248 | .tips-grid {
249 | grid-template-columns: 1fr;
250 | }
251 |
252 | .metronome-controls {
253 | flex-direction: column;
254 | gap: 10px;
255 | }
256 |
257 | .metronome-controls button {
258 | width: 100%;
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/tutorial-content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 钢琴基础课程
5 |
6 |
7 |
8 | 认识钢琴
9 |
10 |
11 |
第一课:钢琴的构造
12 |
13 | - 钢琴的基本结构
14 | - 黑白键的排列规律
15 | - 踏板的功能与使用
16 | - 钢琴的保养知识
17 |
18 |
19 |
20 |
21 |
第二课:基本坐姿与手型
22 |
23 | - 正确的坐姿要领
24 | - 手腕和手指姿势
25 | - 常见错误姿势纠正
26 | - 基础手指练习
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 乐理基础
35 |
36 |
37 |
第一课:认识五线谱
38 |
39 | - 五线谱的构成
40 | - 音符的位置与时值
41 | - 拍号与小节线
42 | - 基本记号含义
43 |
44 |
45 |
46 |
47 |
第二课:节奏与节拍
48 |
49 | - 基本节拍类型
50 | - 常见节奏型
51 | - 附点音符
52 | - 休止符使用
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 进阶教程
63 |
64 |
65 |
66 | 技巧训练
67 |
68 |
69 |
音阶练习
70 |
71 | - 大调音阶练习方法
72 | - 和声小调音阶入门
73 | - 旋律小调音阶练习
74 | - 音阶指法规则
75 |
76 |
77 |
78 |
79 |
和弦练习
80 |
81 | - 三和弦构成与练习
82 | - 七和弦基础
83 | - 和弦进行练习
84 | - 分解和弦技巧
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | 曲目解析
93 |
94 |
95 |
入门曲目
96 |
97 | - 《小星星》完整教学
98 | - 《生日快乐》弹奏技巧
99 | - 《致爱丽丝》前奏解析
100 | - 《土耳其进行曲》片段讲解
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | 专项训练
111 |
112 |
113 |
114 | 听力训练
115 |
116 |
117 |
基础听力
118 |
119 | - 单音识别训练
120 | - 音程听辨练习
121 | - 和弦类型辨识
122 | - 旋律记忆训练
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | 即兴演奏入门
131 |
132 |
133 |
基础即兴
134 |
135 | - 和弦进行模式
136 | - 简单旋律创作
137 | - 节奏型变化
138 | - 左手伴奏模式
139 |
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/js/practice.js:
--------------------------------------------------------------------------------
1 | // 初始化音频生成器
2 | let audioGenerator;
3 |
4 | // 初始化音频上下文
5 | async function initAudio() {
6 | try {
7 | audioGenerator = new AudioGenerator();
8 | console.log('Audio context started');
9 | setupMetronome();
10 | setupScalePractice();
11 | setupChordPractice();
12 | setupSightReading();
13 | } catch (error) {
14 | console.error('Failed to start audio context:', error);
15 | }
16 | }
17 |
18 | // 节拍器功能
19 | function setupMetronome() {
20 | const startStopBtn = document.querySelector('.start-stop');
21 | const tempoUpBtn = document.querySelector('.tempo-up');
22 | const tempoDownBtn = document.querySelector('.tempo-down');
23 | const timeSignatureSelect = document.querySelector('.time-signature select');
24 | const tempoDisplay = document.querySelector('.tempo');
25 |
26 | let isPlaying = false;
27 | let tempo = 120;
28 | let timeSignature = 4;
29 | let count = 0;
30 | let intervalId = null;
31 |
32 | // 开始/停止按钮
33 | startStopBtn.addEventListener('click', () => {
34 | if (!isPlaying) {
35 | const interval = 60000 / tempo; // 计算每拍间隔(毫秒)
36 | count = 0;
37 | intervalId = setInterval(() => {
38 | audioGenerator.createMetronomeSound(count % timeSignature === 0);
39 | count = (count + 1) % timeSignature;
40 | }, interval);
41 | startStopBtn.textContent = '停止';
42 | isPlaying = true;
43 | } else {
44 | clearInterval(intervalId);
45 | startStopBtn.textContent = '开始';
46 | isPlaying = false;
47 | count = 0;
48 | }
49 | });
50 |
51 | // 调整速度
52 | tempoUpBtn.addEventListener('click', () => {
53 | tempo = Math.min(tempo + 5, 240);
54 | tempoDisplay.textContent = tempo;
55 | if (isPlaying) {
56 | clearInterval(intervalId);
57 | startStopBtn.click();
58 | }
59 | });
60 |
61 | tempoDownBtn.addEventListener('click', () => {
62 | tempo = Math.max(tempo - 5, 40);
63 | tempoDisplay.textContent = tempo;
64 | if (isPlaying) {
65 | clearInterval(intervalId);
66 | startStopBtn.click();
67 | }
68 | });
69 |
70 | // 拍号选择
71 | timeSignatureSelect.addEventListener('change', (e) => {
72 | timeSignature = parseInt(e.target.value);
73 | count = 0;
74 | if (isPlaying) {
75 | clearInterval(intervalId);
76 | startStopBtn.click();
77 | }
78 | });
79 | }
80 |
81 | // 音阶练习功能
82 | function setupScalePractice() {
83 | const keySelect = document.querySelector('.key-select');
84 | const playScaleBtn = document.querySelector('.play-scale');
85 |
86 | // 定义音阶
87 | const scales = {
88 | C: ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'],
89 | G: ['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F#5', 'G5'],
90 | D: ['D4', 'E4', 'F#4', 'G4', 'A4', 'B4', 'C#5', 'D5'],
91 | A: ['A4', 'B4', 'C#5', 'D5', 'E5', 'F#5', 'G#5', 'A5'],
92 | E: ['E4', 'F#4', 'G#4', 'A4', 'B4', 'C#5', 'D#5', 'E5'],
93 | B: ['B4', 'C#5', 'D#5', 'E5', 'F#5', 'G#5', 'A#5', 'B5'],
94 | F: ['F4', 'G4', 'A4', 'Bb4', 'C5', 'D5', 'E5', 'F5']
95 | };
96 |
97 | // 播放音阶
98 | playScaleBtn.addEventListener('click', () => {
99 | const key = keySelect.value;
100 | const scale = scales[key];
101 |
102 | // 依次播放音阶音符
103 | scale.forEach((note, index) => {
104 | setTimeout(() => {
105 | audioGenerator.playNote(note, 0.5);
106 | }, index * 500);
107 | });
108 | });
109 | }
110 |
111 | // 和弦练习功能
112 | function setupChordPractice() {
113 | const chordSelect = document.querySelector('.chord-type');
114 | const playChordBtn = document.querySelector('.play-chord');
115 |
116 | // 定义和弦
117 | const chords = {
118 | major: ['C4', 'E4', 'G4'],
119 | minor: ['C4', 'Eb4', 'G4'],
120 | diminished: ['C4', 'Eb4', 'Gb4'],
121 | augmented: ['C4', 'E4', 'G#4'],
122 | dominant7: ['C4', 'E4', 'G4', 'Bb4']
123 | };
124 |
125 | // 播放和弦
126 | playChordBtn.addEventListener('click', () => {
127 | const chordType = chordSelect.value;
128 | const chord = chords[chordType];
129 | audioGenerator.playChord(chord, 1);
130 | });
131 | }
132 |
133 | // 视奏训练功能
134 | function setupSightReading() {
135 | const difficultySelect = document.querySelector('.difficulty-selector select');
136 | const generateBtn = document.querySelector('.generate-notes');
137 | const staffContainer = document.getElementById('staff-container');
138 | const currentNotesDisplay = document.querySelector('.current-notes');
139 |
140 | // 创建五线谱渲染器
141 | const staffRenderer = new StaffRenderer(staffContainer);
142 |
143 | // 生成新的视奏练习
144 | generateBtn.addEventListener('click', () => {
145 | const difficulty = difficultySelect.value;
146 | const noteCount = difficulty === 'beginner' ? 4 : (difficulty === 'intermediate' ? 6 : 8);
147 | const notes = NoteUtils.generateRandomNotes(difficulty, noteCount);
148 |
149 | // 渲染五线谱
150 | staffRenderer.renderNotes(notes);
151 |
152 | // 显示音符名称(用于练习)
153 | currentNotesDisplay.textContent = `音符: ${notes.join(' - ')}`;
154 | });
155 |
156 | // 初始生成一个练习
157 | generateBtn.click();
158 | }
159 |
160 | // 页面加载完成后初始化
161 | document.addEventListener('DOMContentLoaded', () => {
162 | // 添加用户交互监听器来初始化音频
163 | document.addEventListener('click', initAudio, { once: true });
164 | document.addEventListener('keydown', initAudio, { once: true });
165 | });
166 |
--------------------------------------------------------------------------------
/js/piano-audio.js:
--------------------------------------------------------------------------------
1 | class PianoAudio {
2 | constructor() {
3 | console.log('PianoAudio constructor called');
4 | this.initialized = false;
5 | this.sampler = null;
6 | this.reverb = null;
7 | this.context = null;
8 | this.samples = {};
9 | this.gainNode = null;
10 | this.sustainedNotes = new Set();
11 |
12 | // 创建所有音符的音频元素
13 | this.audioElements = {};
14 | this.volume = 0.5;
15 |
16 | // 预加载所有音符
17 | this.preloadNotes();
18 | }
19 |
20 | async init() {
21 | if (this.initialized) return true;
22 |
23 | try {
24 | console.log('Initializing PianoAudio');
25 |
26 | // 检查 Tone 是否已定义
27 | if (typeof Tone === 'undefined') {
28 | throw new Error('Tone.js not loaded');
29 | }
30 |
31 | // 创建 AudioContext
32 | this.context = new (window.AudioContext || window.webkitAudioContext)();
33 |
34 | // 创建增益节点
35 | this.gainNode = this.context.createGain();
36 | this.gainNode.connect(this.context.destination);
37 |
38 | // 设置音量
39 | this.setVolume(this.volume);
40 |
41 | console.log('AudioContext created');
42 |
43 | // 标记为已初始化
44 | this.initialized = true;
45 | return true;
46 | } catch (error) {
47 | console.error('Failed to initialize audio:', error);
48 | return false;
49 | }
50 | }
51 |
52 | preloadNotes() {
53 | console.log('Preloading notes');
54 | // 只加载实际存在的音符文件
55 | const notes = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
56 | const octaves = ['3', '4', '5', '6'];
57 |
58 | notes.forEach(note => {
59 | octaves.forEach(octave => {
60 | const noteId = `${note}${octave}`;
61 | const audio = new Audio(`samples/piano-${noteId}.mp3`);
62 | audio.preload = 'auto';
63 | this.audioElements[noteId] = audio;
64 |
65 | // 对于升号音符,使用相邻的音符
66 | if (note !== 'E' && note !== 'B') {
67 | const sharpNoteId = `${note}#${octave}`;
68 | this.audioElements[sharpNoteId] = audio;
69 | }
70 |
71 | console.log(`Preloading note: samples/piano-${noteId}.mp3`);
72 | });
73 | });
74 | }
75 |
76 | async playNote(note) {
77 | console.log('Playing note:', note);
78 | try {
79 | // 如果音频上下文被暂停,等待用户交互时会自动恢复
80 | if (this.context && this.context.state === 'suspended') {
81 | await this.context.resume();
82 | }
83 |
84 | if (this.sampler && this.sampler.loaded) {
85 | this.sampler.triggerAttack(note);
86 | } else {
87 | // 如果是升号音符,使用相邻的音符
88 | const audioElement = this.audioElements[note] || this.audioElements[this.getNearestNote(note)];
89 | if (audioElement) {
90 | audioElement.currentTime = 0;
91 | audioElement.volume = this.volume;
92 | audioElement.play().catch(error => {
93 | console.error('Failed to play note:', error);
94 | // 如果播放失败,尝试重新加载
95 | audioElement.load();
96 | });
97 | } else {
98 | console.warn('No audio element found for note:', note);
99 | }
100 | }
101 | } catch (error) {
102 | console.error('Error playing note:', note, error);
103 | }
104 | }
105 |
106 | stopNote(note) {
107 | console.log('Stopping note:', note);
108 | try {
109 | if (this.sampler && this.sampler.loaded) {
110 | this.sampler.triggerRelease(note);
111 | } else if (this.audioElements[note]) {
112 | // 如果使用的是音频元素,设置淡出效果
113 | const audio = this.audioElements[note];
114 | const fadeOut = setInterval(() => {
115 | if (audio.volume > 0.1) {
116 | audio.volume -= 0.1;
117 | } else {
118 | audio.pause();
119 | audio.volume = this.volume;
120 | clearInterval(fadeOut);
121 | }
122 | }, 50);
123 | }
124 | } catch (error) {
125 | console.error('Error stopping note:', note, error);
126 | }
127 | }
128 |
129 | getNearestNote(note) {
130 | // 将升号音符映射到相邻的音符
131 | const noteMap = {
132 | 'C#': 'D',
133 | 'D#': 'E',
134 | 'F#': 'G',
135 | 'G#': 'A',
136 | 'A#': 'B'
137 | };
138 |
139 | const noteName = note.slice(0, -1); // 去掉最后的数字
140 | const octave = note.slice(-1); // 获取八度数
141 |
142 | if (noteMap[noteName]) {
143 | return noteMap[noteName] + octave;
144 | }
145 | return note;
146 | }
147 |
148 | setVolume(value) {
149 | console.log('Setting volume:', value);
150 | this.volume = value;
151 | if (this.sampler) {
152 | this.sampler.volume.value = Tone.gainToDb(value);
153 | }
154 | if (this.gainNode) {
155 | this.gainNode.gain.value = value;
156 | }
157 | // 设置所有音频元素的音量
158 | Object.values(this.audioElements).forEach(audio => {
159 | audio.volume = value;
160 | });
161 | }
162 |
163 | setSustain(enabled) {
164 | if (enabled) {
165 | this.sustainedNotes.clear();
166 | } else {
167 | // 释放所有持续的音符
168 | this.sustainedNotes.forEach(note => {
169 | this.stopNote(note);
170 | });
171 | this.sustainedNotes.clear();
172 | }
173 | }
174 | }
175 |
176 | export default PianoAudio;
177 |
--------------------------------------------------------------------------------
/css/about.css:
--------------------------------------------------------------------------------
1 | /* 关于页面样式 */
2 | .about-hero {
3 | background-color: #007bff;
4 | color: white;
5 | padding: 80px 0;
6 | text-align: center;
7 | }
8 |
9 | .about-hero h1 {
10 | font-size: 3rem;
11 | margin-bottom: 20px;
12 | }
13 |
14 | .mission-statement {
15 | font-size: 1.2rem;
16 | max-width: 800px;
17 | margin: 0 auto;
18 | line-height: 1.6;
19 | }
20 |
21 | /* 我们的优势 */
22 | .our-advantages {
23 | padding: 80px 0;
24 | background-color: #f8f9fa;
25 | }
26 |
27 | .our-advantages h2 {
28 | text-align: center;
29 | color: #333;
30 | font-size: 2.5rem;
31 | margin-bottom: 50px;
32 | }
33 |
34 | .advantages-grid {
35 | display: grid;
36 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
37 | gap: 30px;
38 | }
39 |
40 | .advantage-card {
41 | background: white;
42 | border-radius: 10px;
43 | padding: 30px;
44 | text-align: center;
45 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
46 | transition: transform 0.3s;
47 | }
48 |
49 | .advantage-card:hover {
50 | transform: translateY(-5px);
51 | }
52 |
53 | .advantage-icon {
54 | font-size: 3rem;
55 | margin-bottom: 20px;
56 | }
57 |
58 | .advantage-card h3 {
59 | color: #333;
60 | font-size: 1.5rem;
61 | margin-bottom: 15px;
62 | }
63 |
64 | .advantage-card p {
65 | color: #666;
66 | line-height: 1.6;
67 | }
68 |
69 | /* 发展历程 */
70 | .our-history {
71 | padding: 80px 0;
72 | background-color: white;
73 | }
74 |
75 | .our-history h2 {
76 | text-align: center;
77 | color: #333;
78 | font-size: 2.5rem;
79 | margin-bottom: 50px;
80 | }
81 |
82 | .timeline {
83 | max-width: 800px;
84 | margin: 0 auto;
85 | position: relative;
86 | }
87 |
88 | .timeline::before {
89 | content: '';
90 | position: absolute;
91 | left: 50%;
92 | transform: translateX(-50%);
93 | width: 2px;
94 | height: 100%;
95 | background-color: #007bff;
96 | }
97 |
98 | .timeline-item {
99 | display: flex;
100 | justify-content: center;
101 | align-items: flex-start;
102 | margin-bottom: 50px;
103 | position: relative;
104 | }
105 |
106 | .year {
107 | background-color: #007bff;
108 | color: white;
109 | padding: 10px 20px;
110 | border-radius: 20px;
111 | font-weight: bold;
112 | position: absolute;
113 | left: 50%;
114 | transform: translateX(-50%);
115 | top: -20px;
116 | }
117 |
118 | .event {
119 | background: #f8f9fa;
120 | border-radius: 10px;
121 | padding: 30px;
122 | width: calc(50% - 50px);
123 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
124 | margin-top: 30px;
125 | }
126 |
127 | .timeline-item:nth-child(odd) .event {
128 | margin-right: auto;
129 | }
130 |
131 | .timeline-item:nth-child(even) .event {
132 | margin-left: auto;
133 | }
134 |
135 | .event h3 {
136 | color: #333;
137 | font-size: 1.3rem;
138 | margin-bottom: 10px;
139 | }
140 |
141 | .event p {
142 | color: #666;
143 | line-height: 1.6;
144 | }
145 |
146 | /* 联系我们 */
147 | .contact-us {
148 | padding: 80px 0;
149 | background-color: #f8f9fa;
150 | }
151 |
152 | .contact-us h2 {
153 | text-align: center;
154 | color: #333;
155 | font-size: 2.5rem;
156 | margin-bottom: 50px;
157 | }
158 |
159 | .contact-info {
160 | display: grid;
161 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
162 | gap: 30px;
163 | }
164 |
165 | .contact-item {
166 | background: white;
167 | border-radius: 10px;
168 | padding: 30px;
169 | text-align: center;
170 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
171 | }
172 |
173 | .contact-icon {
174 | font-size: 2.5rem;
175 | margin-bottom: 20px;
176 | }
177 |
178 | .contact-item h3 {
179 | color: #333;
180 | font-size: 1.3rem;
181 | margin-bottom: 15px;
182 | }
183 |
184 | .social-links {
185 | display: flex;
186 | justify-content: center;
187 | gap: 15px;
188 | }
189 |
190 | .social-links a {
191 | color: #007bff;
192 | text-decoration: none;
193 | transition: color 0.3s;
194 | }
195 |
196 | .social-links a:hover {
197 | color: #0056b3;
198 | }
199 |
200 | /* 加入我们 */
201 | .join-us {
202 | padding: 80px 0;
203 | background-color: white;
204 | text-align: center;
205 | }
206 |
207 | .join-us h2 {
208 | color: #333;
209 | font-size: 2.5rem;
210 | margin-bottom: 20px;
211 | }
212 |
213 | .join-us > p {
214 | color: #666;
215 | max-width: 600px;
216 | margin: 0 auto 50px;
217 | }
218 |
219 | .positions {
220 | display: grid;
221 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
222 | gap: 30px;
223 | margin-bottom: 40px;
224 | }
225 |
226 | .position-card {
227 | background: #f8f9fa;
228 | border-radius: 10px;
229 | padding: 30px;
230 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
231 | }
232 |
233 | .position-card h3 {
234 | color: #333;
235 | font-size: 1.3rem;
236 | margin-bottom: 15px;
237 | }
238 |
239 | .position-card p {
240 | color: #666;
241 | line-height: 1.6;
242 | }
243 |
244 | .join-button {
245 | display: inline-block;
246 | padding: 15px 30px;
247 | background-color: #007bff;
248 | color: white;
249 | text-decoration: none;
250 | border-radius: 5px;
251 | font-size: 1.1rem;
252 | transition: all 0.3s;
253 | }
254 |
255 | .join-button:hover {
256 | background-color: #0056b3;
257 | transform: translateY(-2px);
258 | }
259 |
260 | /* 响应式设计 */
261 | @media (max-width: 768px) {
262 | .about-hero h1 {
263 | font-size: 2rem;
264 | }
265 |
266 | .our-advantages h2,
267 | .our-history h2,
268 | .contact-us h2,
269 | .join-us h2 {
270 | font-size: 2rem;
271 | }
272 |
273 | .timeline::before {
274 | left: 20px;
275 | }
276 |
277 | .timeline-item {
278 | flex-direction: column;
279 | }
280 |
281 | .year {
282 | left: 20px;
283 | transform: none;
284 | }
285 |
286 | .event {
287 | width: calc(100% - 50px);
288 | margin-left: 50px !important;
289 | }
290 |
291 | .contact-info,
292 | .positions {
293 | grid-template-columns: 1fr;
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/js/practice-songs.js:
--------------------------------------------------------------------------------
1 | // 导出的歌曲数据
2 | const songs = {
3 | 'happy-birthday': {
4 | name: '生日快乐',
5 | difficulty: 1,
6 | notes: [
7 | // 第一段
8 | 'G4', 'G4', 'A4', 'G4', 'C5', 'B4',
9 | // 祝你生日快乐
10 | 'G4', 'G4', 'A4', 'G4', 'D5', 'C5',
11 | // 祝你生日快乐
12 | 'G4', 'G4', 'G5', 'E5', 'C5', 'B4', 'A4',
13 | // 祝你生日快乐
14 | 'F5', 'F5', 'E5', 'C5', 'D5', 'C5'
15 | ],
16 | tempo: 120,
17 | maxScore: 100
18 | },
19 | 'twinkle': {
20 | name: '小星星',
21 | difficulty: 1,
22 | notes: [
23 | // 第一段
24 | 'C4', 'C4', 'G4', 'G4', 'A4', 'A4', 'G4',
25 | 'F4', 'F4', 'E4', 'E4', 'D4', 'D4', 'C4',
26 | // 中间段
27 | 'G4', 'G4', 'F4', 'F4', 'E4', 'E4', 'D4',
28 | // 重复第一段
29 | 'G4', 'G4', 'F4', 'F4', 'E4', 'E4', 'D4',
30 | 'C4', 'G4', 'C5'
31 | ],
32 | tempo: 100,
33 | maxScore: 100
34 | },
35 | 'two-tigers': {
36 | name: '两只老虎',
37 | difficulty: 1,
38 | notes: [
39 | // 第一只老虎
40 | 'C4', 'D4', 'E4', 'C4',
41 | 'C4', 'D4', 'E4', 'C4',
42 | 'E4', 'F4', 'G4',
43 | 'E4', 'F4', 'G4',
44 | // 跑得快
45 | 'G4', 'A4', 'G4', 'F4', 'E4', 'C4',
46 | 'G4', 'A4', 'G4', 'F4', 'E4', 'C4',
47 | // 一只没有眼睛
48 | 'C4', 'G3', 'C4',
49 | 'C4', 'G3', 'C4'
50 | ],
51 | tempo: 120,
52 | maxScore: 100
53 | },
54 | 'ode-to-joy': {
55 | name: '欢乐颂',
56 | difficulty: 2,
57 | notes: [
58 | 'E4', 'E4', 'F4', 'G4',
59 | 'G4', 'F4', 'E4', 'D4',
60 | 'C4', 'C4', 'D4', 'E4',
61 | 'E4', 'D4', 'D4',
62 |
63 | 'E4', 'E4', 'F4', 'G4',
64 | 'G4', 'F4', 'E4', 'D4',
65 | 'C4', 'C4', 'D4', 'E4',
66 | 'D4', 'C4', 'C4'
67 | ],
68 | tempo: 120,
69 | maxScore: 100
70 | },
71 | 'mary-lamb': {
72 | name: '玛丽有只小羊羔',
73 | difficulty: 1,
74 | notes: [
75 | 'E4', 'D4', 'C4', 'D4',
76 | 'E4', 'E4', 'E4',
77 | 'D4', 'D4', 'D4',
78 | 'E4', 'G4', 'G4',
79 |
80 | 'E4', 'D4', 'C4', 'D4',
81 | 'E4', 'E4', 'E4',
82 | 'D4', 'D4', 'E4', 'D4',
83 | 'C4'
84 | ],
85 | tempo: 120,
86 | maxScore: 100
87 | },
88 | 'jingle-bells': {
89 | name: '铃儿响叮当',
90 | difficulty: 2,
91 | notes: [
92 | 'E4', 'E4', 'E4',
93 | 'E4', 'E4', 'E4',
94 | 'E4', 'G4', 'C4', 'D4',
95 | 'E4',
96 |
97 | 'F4', 'F4', 'F4', 'F4',
98 | 'F4', 'E4', 'E4', 'E4', 'E4',
99 | 'E4', 'D4', 'D4', 'E4',
100 | 'D4', 'G4'
101 | ],
102 | tempo: 120,
103 | maxScore: 100
104 | },
105 | 'jasmine': {
106 | name: '茉莉花',
107 | difficulty: 3,
108 | notes: [
109 | 'E4', 'G4', 'A4', 'C5',
110 | 'B4', 'A4', 'G4', 'E4',
111 | 'G4', 'A4', 'G4', 'E4',
112 | 'D4', 'E4',
113 |
114 | 'E4', 'G4', 'A4', 'C5',
115 | 'B4', 'A4', 'G4', 'E4',
116 | 'G4', 'E4', 'D4', 'C4',
117 | 'C4', 'E4'
118 | ],
119 | tempo: 90,
120 | maxScore: 100
121 | },
122 | 'butterfly-lovers': {
123 | name: '梁祝',
124 | difficulty: 4,
125 | notes: [
126 | 'E4', 'G4', 'A4', 'B4',
127 | 'C5', 'B4', 'A4', 'G4',
128 | 'E4', 'G4', 'A4', 'B4',
129 | 'C5', 'E5',
130 |
131 | 'D5', 'C5', 'B4', 'A4',
132 | 'G4', 'E4', 'D4', 'E4',
133 | 'G4', 'A4', 'G4', 'E4',
134 | 'D4', 'E4'
135 | ],
136 | tempo: 80,
137 | maxScore: 100
138 | },
139 | 'fur-elise': {
140 | name: '致爱丽丝',
141 | difficulty: 3,
142 | notes: [
143 | // 主题旋律
144 | 'E5', 'D#5', 'E5', 'D#5', 'E5', 'B4', 'D5', 'C5', 'A4',
145 | 'C4', 'E4', 'A4', 'B4',
146 | 'E4', 'G#4', 'B4', 'C5',
147 | // 第二段
148 | 'E5', 'D#5', 'E5', 'D#5', 'E5', 'B4', 'D5', 'C5', 'A4',
149 | 'C4', 'E4', 'A4', 'B4',
150 | 'E4', 'C5', 'B4', 'A4',
151 | // 中间段
152 | 'B4', 'C5', 'D5', 'E5',
153 | 'G4', 'F5', 'E5', 'D5',
154 | 'F4', 'E5', 'D5', 'C5',
155 | // 重复主题
156 | 'E5', 'D#5', 'E5', 'D#5', 'E5', 'B4', 'D5', 'C5', 'A4',
157 | 'C4', 'E4', 'A4', 'B4',
158 | 'E4', 'G#4', 'B4', 'C5'
159 | ],
160 | tempo: 130,
161 | maxScore: 100
162 | },
163 | 'canon': {
164 | name: '卡农',
165 | difficulty: 4,
166 | notes: [
167 | // 主题旋律
168 | 'D4', 'A4', 'B4', 'F#4', 'G4', 'D4', 'G4', 'A4',
169 | 'D4', 'A4', 'B4', 'F#4', 'G4', 'D4', 'G4', 'A4',
170 | // 变奏1
171 | 'F#4', 'D4', 'F#4', 'G4', 'A4', 'F#4', 'A4', 'B4',
172 | 'G4', 'B4', 'A4', 'G4', 'F#4', 'D4', 'E4', 'F#4',
173 | // 变奏2
174 | 'G4', 'E4', 'G4', 'A4', 'B4', 'G4', 'B4', 'C5',
175 | 'A4', 'C5', 'B4', 'A4', 'G4', 'E4', 'F#4', 'G4',
176 | // 高潮部分
177 | 'A4', 'D5', 'C#5', 'D5', 'B4', 'G4', 'A4', 'B4',
178 | 'C5', 'A4', 'B4', 'C5', 'D5', 'B4', 'C5', 'D5',
179 | // 结束段
180 | 'E5', 'C5', 'D5', 'E5', 'F#5', 'D5', 'E5', 'F#5',
181 | 'G5', 'D5', 'G4', 'A4', 'B4', 'G4', 'B4', 'D5'
182 | ],
183 | tempo: 140,
184 | maxScore: 100
185 | }
186 | };
187 |
188 | // 计算分数的函数
189 | function calculateScore(correctNotes, wrongNotes) {
190 | if (correctNotes === 0 && wrongNotes === 0) return 0;
191 |
192 | const totalNotes = correctNotes + wrongNotes;
193 | const accuracy = correctNotes / totalNotes;
194 |
195 | // 基础分数是准确率 * 100
196 | let score = Math.round(accuracy * 100);
197 |
198 | // 如果错误次数过多,额外扣分
199 | if (wrongNotes > totalNotes * 0.5) {
200 | score -= 20;
201 | }
202 |
203 | // 确保分数在0-100之间
204 | return Math.max(0, Math.min(100, score));
205 | }
206 |
207 | // 导出
208 | export { songs, calculateScore };
209 |
210 | console.log('practice-songs.js loaded, available songs:', Object.keys(songs));
211 | console.log('Songs object:', songs);
212 | console.log('Number of songs:', Object.keys(songs).length);
213 | console.log('Song names:', Object.values(songs).map(song => song.name));
214 |
--------------------------------------------------------------------------------
/js/translations.js:
--------------------------------------------------------------------------------
1 | export const translations = {
2 | zh: {
3 | title: "Piano Online - 专业的在线钢琴学习平台",
4 | logo: {
5 | text: "Piano Online",
6 | alt: "Piano Online Logo"ßß
7 | },
8 | nav: {
9 | features: "功能",
10 | tutorial: "教程",
11 | testimonials: "评价",
12 | faq: "常见问题",
13 | startPractice: "开始练习"
14 | },
15 | hero: {
16 | title: "体验专业的钢琴演奏",
17 | subtitle: "在任何地方,随时开始您的钢琴学习之旅",
18 | startNow: "立即开始",
19 | learnMore: "了解更多"
20 | },
21 | practice: {
22 | sustainPedal: "延音踏板",
23 | selectSong: "选择曲目...",
24 | startPractice: "开始练习",
25 | stopPractice: "停止练习",
26 | difficulty: "难度",
27 | difficultyLevels: {
28 | 1: "入门",
29 | 2: "初级",
30 | 3: "中级",
31 | 4: "高级"
32 | },
33 | stats: {
34 | correct: "正确: ",
35 | wrong: "错误: ",
36 | progress: "进度: "
37 | },
38 | nextKey: "下一个键",
39 | complete: {
40 | title: "练习完成!",
41 | score: "得分",
42 | correct: "正确",
43 | wrong: "错误"
44 | }
45 | },
46 | features: {
47 | title: "专业功能,助您进步",
48 | realPianoSound: "真实钢琴音色",
49 | realPianoSoundDesc: "采用专业录音的钢琴音源,还原真实演奏体验",
50 | intelligentPractice: "智能练习系统",
51 | intelligentPracticeDesc: "根据您的水平自动调整练习难度",
52 | richSongLibrary: "丰富曲库",
53 | richSongLibraryDesc: "包含从入门到高级的各类经典曲目"
54 | },
55 | howItWorks: {
56 | title: "如何使用",
57 | step1Title: "选择曲目",
58 | step1Desc: "从我们精心准备的曲库中选择适合您水平的曲目",
59 | step2Title: "跟随提示",
60 | step2Desc: "按照屏幕上的提示,一步步完成练习",
61 | step3Title: "查看进度",
62 | step3Desc: "实时追踪您的练习进度,掌握学习效果"
63 | },
64 | testimonials: {
65 | title: "用户评价",
66 | testimonial1: "这是我用过最好的在线钢琴学习平台,界面简洁,功能强大。",
67 | author1: "张小明",
68 | title1: "钢琴爱好者",
69 | testimonial2: "智能提示系统让我的练习效率提高了很多。",
70 | author2: "李华",
71 | title2: "音乐教师"
72 | },
73 | faq: {
74 | title: "常见问题",
75 | question1: "需要真实的钢琴键盘吗?",
76 | answer1: "不需要,您可以使用电脑键盘进行练习。当然,如果有MIDI键盘会获得更好的体验。",
77 | question2: "适合零基础学习吗?",
78 | answer2: "完全适合!我们的课程从最基础的内容开始,循序渐进地引导您学习。"
79 | },
80 | footer: {
81 | title: "Piano Online",
82 | desc: "让钢琴学习变得简单有趣",
83 | contactTitle: "联系我们",
84 | contactEmail: "Email: contact@piano-online.com",
85 | followTitle: "关注我们",
86 | wechat: "微信",
87 | weibo: "微博",
88 | copyright: " 2024 Piano Online. 保留所有权利。"
89 | }
90 | },
91 | en: {
92 | title: "Piano Online - Professional Online Piano Learning Platform",
93 | logo: {
94 | text: "Piano Online",
95 | alt: "Piano Online Logo"
96 | },
97 | nav: {
98 | features: "Features",
99 | tutorial: "Tutorial",
100 | testimonials: "Testimonials",
101 | faq: "FAQ",
102 | startPractice: "Start Practice"
103 | },
104 | hero: {
105 | title: "Experience Professional Piano Playing",
106 | subtitle: "Start your piano learning journey anywhere, anytime",
107 | startNow: "Start Now",
108 | learnMore: "Learn More"
109 | },
110 | practice: {
111 | sustainPedal: "Sustain Pedal",
112 | selectSong: "Select a song...",
113 | startPractice: "Start Practice",
114 | stopPractice: "Stop Practice",
115 | difficulty: "Difficulty",
116 | difficultyLevels: {
117 | 1: "Beginner",
118 | 2: "Elementary",
119 | 3: "Intermediate",
120 | 4: "Advanced"
121 | },
122 | stats: {
123 | correct: "Correct: ",
124 | wrong: "Wrong: ",
125 | progress: "Progress: "
126 | },
127 | nextKey: "Next Key",
128 | complete: {
129 | title: "Practice Complete!",
130 | score: "Score",
131 | correct: "Correct",
132 | wrong: "Wrong"
133 | }
134 | },
135 | features: {
136 | title: "Professional Features for Your Progress",
137 | realPianoSound: "Real Piano Sound",
138 | realPianoSoundDesc: "Professional piano samples for authentic playing experience",
139 | intelligentPractice: "Intelligent Practice System",
140 | intelligentPracticeDesc: "Automatically adjusts difficulty based on your level",
141 | richSongLibrary: "Rich Song Library",
142 | richSongLibraryDesc: "Contains classic pieces from beginner to advanced levels"
143 | },
144 | howItWorks: {
145 | title: "How It Works",
146 | step1Title: "Choose a Song",
147 | step1Desc: "Select a song that matches your skill level from our curated library",
148 | step2Title: "Follow the Guide",
149 | step2Desc: "Follow the on-screen prompts to complete your practice",
150 | step3Title: "Track Progress",
151 | step3Desc: "Monitor your practice progress in real-time"
152 | },
153 | testimonials: {
154 | title: "Testimonials",
155 | testimonial1: "This is the best online piano learning platform I've used, with a clean interface and powerful features.",
156 | author1: "John Smith",
157 | title1: "Piano Enthusiast",
158 | testimonial2: "The intelligent prompt system has greatly improved my practice efficiency.",
159 | author2: "Sarah Johnson",
160 | title2: "Music Teacher"
161 | },
162 | faq: {
163 | title: "FAQ",
164 | question1: "Do I need a real piano keyboard?",
165 | answer1: "No, you can practice using your computer keyboard. However, a MIDI keyboard will provide a better experience.",
166 | question2: "Is it suitable for beginners?",
167 | answer2: "Absolutely! Our courses start from the basics and guide you step by step."
168 | },
169 | footer: {
170 | title: "Piano Online",
171 | desc: "Making piano learning simple and fun",
172 | contactTitle: "Contact Us",
173 | contactEmail: "Email: contact@piano-online.com",
174 | followTitle: "Follow Us",
175 | wechat: "WeChat",
176 | weibo: "Weibo",
177 | copyright: " 2024 Piano Online. All rights reserved."
178 | }
179 | }
180 | };
181 |
--------------------------------------------------------------------------------
/articles/tutorials/finger-numbers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 钢琴手指编号与基本指法 - Piano Online
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
23 |
24 |
25 |
26 |
27 |
40 |
41 |
42 |
43 |
44 |
45 | 钢琴手指编号与基本指法
46 |
47 | 发布于:2025-03-22
48 | 阅读时间:12分钟
49 | 难度:入门
50 |
51 |
52 |
53 |
54 |
55 | 手指编号基础
56 | 钢琴手指编号是一个简单但重要的概念。每个手指都有固定的编号,这种编号系统在世界范围内都是统一的:
57 |
58 |
59 |
手指编号对照表
60 |
61 | - 大拇指 = 1
62 | - 食指 = 2
63 | - 中指 = 3
64 | - 无名指 = 4
65 | - 小指 = 5
66 |
67 |
注意:左右手的编号规则是相同的,都是从大拇指开始为1。
68 |
69 |
70 | 为什么手指编号很重要?
71 |
72 | - 帮助初学者快速掌握正确的指法
73 | - 确保演奏时的手指位置准确
74 | - 提高演奏的流畅性和效率
75 | - 减少不必要的手腕移动
76 |
77 |
78 |
79 |
80 | 基本指法原则
81 | 良好的指法是流畅演奏的基础,以下是一些基本原则:
82 |
83 | 1. 自然放松原则
84 |
85 | - 手指自然弯曲,保持放松
86 | - 避免不必要的手指伸展
87 | - 大拇指不要悬空或过分弯曲
88 |
89 |
90 | 2. 位置固定原则
91 |
92 | - 尽量保持手的位置稳定
93 | - 避免频繁的手位移动
94 | - 合理利用手指跨越来减少换手
95 |
96 |
97 |
98 |
五指位置练习
99 |
从中央C开始,右手五个手指分别放在C D E F G位置:
100 |
101 | - 保持手型,依次按下每个键
102 | - 练习不同的按键顺序
103 | - 确保其他手指保持放松
104 |
105 |
106 |
107 |
108 |
109 | 常见指法练习
110 |
111 | 1. 基本音阶指法
112 | 以C大调音阶为例:
113 |
114 |
右手上行指法:
115 |
C(1) D(2) E(3) F(1) G(2) A(3) B(4) C(5)
116 |
左手下行指法:
117 |
C(5) B(4) A(3) G(2) F(1) E(3) D(2) C(1)
118 |
119 |
120 | 2. 和弦指法
121 | 三和弦的基本指法:
122 |
123 | - 根音位:1-3-5
124 | - 第一转位:1-2-5
125 | - 第二转位:1-3-5
126 |
127 |
128 |
129 |
练习建议
130 |
131 | - 先慢速练习,确保指法正确
132 | - 注意手指的独立性
133 | - 每天坚持练习基本指法
134 |
135 |
136 |
137 |
138 |
139 | 进阶指法技巧
140 |
141 | 1. 手指交叉
142 | 在演奏音阶和琶音时,经常需要使用手指交叉技巧:
143 |
144 | - 大拇指下穿:其他手指按键时,大拇指快速移到下一个位置
145 | - 手指跨越:在不移动手腕的情况下,手指跨越其他手指按键
146 |
147 |
148 | 2. 指法替换
149 | 在保持音符持续的情况下更换手指:
150 |
151 | - 长音符的指法替换
152 | - 和弦中的指法替换
153 | - 旋律线中的无声替换
154 |
155 |
156 |
157 |
每日练习计划
158 |
159 | - 五指位置练习:10分钟
160 | - 音阶指法练习:15分钟
161 | - 和弦指法练习:10分钟
162 | - 指法替换练习:5分钟
163 |
164 |
165 |
166 |
167 |
168 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/articles/tutorials/reading-notes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 五线谱入门:如何读懂音符 - Piano Online
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
23 |
24 |
25 |
26 |
27 |
40 |
41 |
42 |
43 |
44 |
45 | 五线谱入门:如何读懂音符
46 |
47 | 发布于:2025-03-22
48 | 阅读时间:20分钟
49 | 难度:入门
50 |
51 |
52 |
53 |
54 |
55 | 五线谱基础概念
56 | 五线谱是记录音乐的标准符号系统,由五条平行的横线组成。理解五线谱是学习钢琴的重要基础。
57 |
58 | 五线谱的构成
59 |
60 | - 五条横线和四个间隔组成主体
61 | - 上下可添加短线表示更高或更低的音符
62 | - 每条线和间隔都代表特定的音高
63 |
64 |
65 |
66 |
记忆技巧
67 |
从下往上数线和间:
68 |
69 | - 线:E G B D F(Every Good Boy Does Fine)
70 | - 间:F A C E(FACE)
71 |
72 |
73 |
74 |
75 |
76 | 音符的识别
77 |
78 | 1. 音符的位置
79 | 音符在五线谱上的位置决定了它的音高:
80 |
81 | - 位置越高,音调越高
82 | - 位置越低,音调越低
83 | - 中央C位于第一条加线上
84 |
85 |
86 | 2. 音符的时值
87 |
88 |
基本音符时值:
89 |
90 | - 全音符:空心圆形,持续四拍
91 | - 二分音符:空心圆形加符干,持续两拍
92 | - 四分音符:实心圆形加符干,持续一拍
93 | - 八分音符:四分音符加一个符尾,持续半拍
94 | - 十六分音符:四分音符加两个符尾,持续四分之一拍
95 |
96 |
97 |
98 |
99 |
练习建议
100 |
初学者可以这样练习:
101 |
102 | - 先识别音符位置,说出音名
103 | - 再观察音符形状,确定时值
104 | - 最后结合位置和时值进行弹奏
105 |
106 |
107 |
108 |
109 |
110 | 调号与拍号
111 |
112 | 1. 调号
113 | 调号出现在五线谱开头,表示乐曲的调式:
114 |
115 | - 升号(#):将音符升高半音
116 | - 降号(b):将音符降低半音
117 | - 还原号(♮):取消升降效果
118 |
119 |
120 | 2. 拍号
121 | 拍号用两个数字表示,例如4/4、3/4等:
122 |
123 | - 上面的数字表示每小节的拍数
124 | - 下面的数字表示以几分音符为一拍
125 |
126 |
127 |
128 |
常见拍号说明
129 |
130 | - 4/4拍:每小节4拍,以四分音符为一拍
131 | - 3/4拍:每小节3拍,以四分音符为一拍
132 | - 6/8拍:每小节6拍,以八分音符为一拍
133 |
134 |
135 |
136 |
137 |
138 | 实践练习方法
139 |
140 | 1. 基础识谱练习
141 |
142 |
每日练习步骤:
143 |
144 | - 识别五线谱上的音符位置(10分钟)
145 | - 练习不同音符的时值(10分钟)
146 | - 结合简单曲目进行实践(15分钟)
147 |
148 |
149 |
150 | 2. 进阶练习
151 |
152 | - 视唱练习:看谱唱出音高
153 | - 节奏练习:拍打或读出节奏
154 | - 默写练习:听音写谱
155 |
156 |
157 |
158 |
注意事项
159 |
160 | - 保持耐心,循序渐进
161 | - 每天固定时间练习
162 | - 从简单的曲目开始
163 | - 多听多看多练
164 |
165 |
166 |
167 |
168 |
169 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/tutorials.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 钢琴教程与知识库 - Piano Online
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
40 |
42 |
43 |
44 |
45 |
58 |
59 |
60 |
61 |
62 |
钢琴教程与知识库
63 |
从零基础到进阶,系统化的钢琴学习资源
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
80 |
81 |
90 |
91 |
100 |
101 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
最新教程
118 |
119 |
120 |
121 |
进阶乐理知识详解
122 |
深入讲解和声学、对位法、曲式分析等进阶乐理知识,帮助你更好地理解音乐作品的结构和创作技巧。
123 |
阅读更多
124 |
125 |
126 |
127 |
128 |
129 |
经典曲目解析
130 |
从入门到高级的经典钢琴曲目详细解析,包括技术难点、练习方法和演奏技巧的讲解。
131 |
阅读更多
132 |
133 |
134 |
135 |
136 |
137 |
演奏技巧提升
138 |
全面提升你的钢琴演奏技巧,包括触键方法、踏板使用、音色控制和表现力的培养。
139 |
阅读更多
140 |
141 |
142 |
143 |
144 |
145 |
科学的练习方法
146 |
介绍高效的钢琴练习方法,帮助你合理安排练习时间,提高练习效率。
147 |
阅读更多
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/articles/tutorials/piano-basics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 钢琴基础知识:认识键盘与正确坐姿 - Piano Online
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
40 |
42 |
43 |
44 |
45 |
46 |
59 |
60 |
61 |
62 |
63 |
64 | 钢琴基础知识:认识键盘与正确坐姿
65 |
66 | 发布于:2025-03-22
67 | 阅读时间:15分钟
68 | 难度:入门
69 |
70 |
71 |
72 |
73 |
74 | 钢琴键盘布局
75 | 钢琴键盘共有88个键,包括52个白键和36个黑键。白键代表自然音(C D E F G A B),黑键代表升降音(C#/Db D#/Eb F#/Gb G#/Ab A#/Bb)。了解键盘布局是学习钢琴的第一步。
76 |
77 | 如何快速识别键位
78 | 观察黑键的分组是识别键位的关键。黑键以2个和3个为一组交替排列,其中:
79 |
80 | - 两个黑键组合前的白键是C
81 | - 三个黑键组合前的白键是F
82 | - 找到一个C键后,往右依次是D E F G A B
83 |
84 |
85 |
86 |
实用技巧
87 |
初学者可以在中央C(位于键盘中间位置)贴上标记,以此为参照点学习其他键位。中央C通常位于钢琴琴键的正中间,也是五线谱中间C的位置。
88 |
89 |
90 |
91 |
92 | 正确的弹琴坐姿
93 | 良好的坐姿不仅能提高演奏效果,还能预防疲劳和损伤。以下是正确坐姿的要点:
94 |
95 | 坐姿要点
96 |
97 | -
98 | 座椅高度:
99 |
调整琴凳高度,使手肘略高于键盘面。坐在琴凳前半部分,保持身体稳定但不僵硬。
100 |
101 | -
102 | 距离:
103 |
与琴键保持一臂的距离,身体能自然伸展。手肘不要贴近身体,保持适当空间。
104 |
105 | -
106 | 背部姿势:
107 |
保持背部挺直但放松,肩膀自然下垂,不要耸肩。
108 |
109 | -
110 | 手腕位置:
111 |
手腕要保持水平,既不要过分抬高也不要下垂,保持自然放松的状态。
112 |
113 |
114 |
115 |
116 |
注意事项
117 |
练琴时要注意:
118 |
119 | - 每练习30分钟要起来活动一下
120 | - 感觉疲劳时及时休息
121 | - 保持手指放松,不要过分用力
122 |
123 |
124 |
125 |
126 |
127 | 基本手型
128 | 正确的手型是演奏的基础,需要注意以下几点:
129 |
130 | 手型要点
131 |
132 | - 手指自然弯曲,如同握住一个小球
133 | - 指尖垂直按键,而不是用指腹
134 | - 大拇指自然弯曲,不要翘起
135 | - 手腕保持柔软,不要僵硬
136 |
137 |
138 |
139 |
练习建议
140 |
初学者可以先进行以下练习:
141 |
142 | - 在桌面上练习手型,感受手指的放松状态
143 | - 用单个手指轻柔地按键,体会触键的感觉
144 | - 练习五个手指依次按键,保持其他手指放松
145 |
146 |
147 |
148 |
149 |
150 | 日常练习建议
151 |
152 |
初学者每日练习计划
153 |
154 | - 热身练习:5-10分钟
155 | - 指法练习:10-15分钟
156 | - 曲目练习:20-30分钟
157 | - 复习与巩固:10分钟
158 |
159 |
160 |
161 |
162 |
重要提示
163 |
正确的基础比快速的进步更重要。建议:
164 |
165 | - 每天保持固定的练习时间
166 | - 循序渐进,不要急于求成
167 | - 多观察、多聆听、多思考
168 |
169 |
170 |
171 |
172 |
173 |
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/articles/tutorials/rhythm-basics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 节奏基础:认识拍子与节拍 - Piano Online
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
23 |
24 |
25 |
26 |
27 |
40 |
41 |
42 |
43 |
44 |
45 | 节奏基础:认识拍子与节拍
46 |
47 | 发布于:2025-03-22
48 | 阅读时间:15分钟
49 | 难度:入门
50 |
51 |
52 |
53 |
54 |
55 | 什么是节奏?
56 | 节奏是音乐的时间组织,它决定了音乐的律动感和时间规律。良好的节奏感是演奏钢琴的重要基础。
57 |
58 | 节奏的基本要素
59 |
60 | - 拍子:音乐的基本时间单位
61 | - 节拍:拍子的组织方式
62 | - 速度:音乐进行的快慢
63 | - 强弱:拍子的重音规律
64 |
65 |
66 |
67 |
重要提示
68 |
好的节奏感需要:
69 |
70 | - 稳定的节拍感
71 | - 准确的时值控制
72 | - 合理的强弱变化
73 |
74 |
75 |
76 |
77 |
78 | 常见拍子类型
79 |
80 | 1. 二拍子
81 |
82 |
特点:
83 |
84 | - 每小节两拍
85 | - 强弱规律:强-弱
86 | - 常见于进行曲
87 |
88 |
89 |
90 | 2. 三拍子
91 |
92 |
特点:
93 |
94 | - 每小节三拍
95 | - 强弱规律:强-弱-弱
96 | - 常见于华尔兹
97 |
98 |
99 |
100 | 3. 四拍子
101 |
102 |
特点:
103 |
104 | - 每小节四拍
105 | - 强弱规律:强-弱-次强-弱
106 | - 最常见的拍子类型
107 |
108 |
109 |
110 |
111 |
练习方法
112 |
113 | - 用节拍器打拍子
114 | - 跟随音乐拍手
115 | - 数拍子练习
116 |
117 |
118 |
119 |
120 |
121 | 基本节奏型
122 |
123 | 1. 均匀节奏
124 | 所有音符时值相同的节奏型:
125 |
126 | - 四分音符连续
127 | - 八分音符连续
128 | - 十六分音符连续
129 |
130 |
131 | 2. 附点节奏
132 | 带有附点的节奏型:
133 |
134 | - 附点四分音符加八分音符
135 | - 附点八分音符加十六分音符
136 |
137 |
138 | 3. 切分节奏
139 | 强弱关系与正常拍子相反的节奏型:
140 |
141 | - 弱拍重音
142 | - 跨拍音符
143 |
144 |
145 |
146 |
练习建议
147 |
148 | - 先用节拍器慢速练习
149 | - 掌握后逐渐加快速度
150 | - 注意保持稳定的拍子
151 |
152 |
153 |
154 |
155 |
156 | 节奏训练方法
157 |
158 | 1. 基础训练
159 |
160 |
每日练习计划:
161 |
162 | - 节拍器练习(10分钟)
163 |
164 | - 从慢速开始(60拍/分)
165 | - 保持稳定的拍子感
166 | - 逐渐提高速度
167 |
168 |
169 | - 拍手练习(5分钟)
170 |
171 | - 跟随音乐拍手
172 | - 体会强弱变化
173 |
174 |
175 | - 数拍练习(10分钟)
176 |
177 | - 大声数拍
178 | - 强调重拍
179 |
180 |
181 |
182 |
183 |
184 | 2. 进阶训练
185 |
186 | - 双手不同节奏训练
187 | - 复杂节奏型练习
188 | - 变速练习
189 |
190 |
191 |
192 |
重要提示
193 |
培养节奏感的关键:
194 |
195 | - 坚持每天练习
196 | - 从简单节奏开始
197 | - 保持耐心和专注
198 | - 多听多练多感受
199 |
200 |
201 |
202 |
203 |
204 |
215 |
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------
/practice.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 钢琴练习 - Piano Online
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
42 |
44 |
45 |
46 |
47 |
60 |
61 |
62 |
154 |
155 |
156 |
157 |
158 |
练习建议
159 |
160 |
161 |
制定练习计划
162 |
163 | - 设定明确的练习目标
164 | - 合理安排练习时间
165 | - 循序渐进提高难度
166 | - 保持练习记录
167 |
168 |
169 |
170 |
171 |
正确的练习方法
172 |
173 | - 从慢速开始练习
174 | - 使用节拍器保持节奏
175 | - 分手练习困难片段
176 | - 注意指法规范
177 |
178 |
179 |
180 |
181 |
避免常见错误
182 |
183 | - 不要急于求成
184 | - 保持正确的坐姿
185 | - 适时休息放松
186 | - 注意听音辨音
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
--------------------------------------------------------------------------------
/test-improvements.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Piano UX Improvements Test Page
7 |
97 |
98 |
99 | 🎹 Piano Online - UX Improvements Test Page
100 | Date: 2025-11-04
101 |
102 |
103 |
Quick Actions
104 |
105 |
108 |
111 |
114 |
115 |
116 |
117 |
118 |
1. Layout Changes MANUAL TEST
119 |
120 | - Piano keyboard visible on first screen (no scroll needed)
121 | - Hero section compressed to ~2 lines
122 | - Clear call-to-action: "按键盘上的字母键开始演奏"
123 |
124 |
125 |
126 |
127 |
2. Keyboard Labels MANUAL TEST
128 |
129 | - White keys show letters: A, S, D, F, G, H, J, K, L
130 | - Black keys show numbers: 1, 2, 4, 5, 6, 8, 9
131 | - Labels are clear and readable
132 | - Labels don't overlap with key design
133 |
134 |
135 |
136 |
137 |
3. Tutorial Overlay MANUAL TEST
138 |
139 | - Tutorial appears on first visit (~1 second delay)
140 | - Shows clear instruction: "按下键盘上的 A 键试试!"
141 | - Pressing A key triggers success feedback
142 | - "跳过" button closes tutorial
143 | - "开始体验" button closes tutorial
144 | - Tutorial doesn't appear on subsequent visits
145 |
146 |
147 | Test Command: localStorage.removeItem('piano-tutorial-completed')
148 |
149 |
150 |
153 |
154 |
155 |
156 |
157 |
4. Simplified Terminology MANUAL TEST
158 |
159 | - "延音踏板" → "声音延长 🎵"
160 | - "节奏大师模式" → "🎮 游戏模式"
161 | - "开始练习" → "✨ 跟着提示弹"
162 | - Tooltips appear on hover
163 | - Tooltips provide clear explanations
164 |
165 |
166 |
167 |
168 |
5. Visual Feedback MANUAL TEST
169 |
170 | - Correct key press: green glow animation
171 | - Incorrect key press: shake + red border
172 | - Animations are smooth and clear
173 | - Feedback is immediate
174 |
175 |
176 |
177 |
178 |
6. Song Selection MANUAL TEST
179 |
180 | - Songs labeled with emoji + difficulty
181 | - Example: "⭐ 小星星 (简单)"
182 | - Dropdown is easy to understand
183 |
184 |
185 |
186 |
187 |
Browser Console Check
188 |
189 | Expected: No errors in console
190 | Check for: Piano initialized, Practice Mode initialized, Tutorial initialized
191 |
192 |
193 |
196 |
197 |
198 |
199 |
200 |
239 |
240 |
241 |
--------------------------------------------------------------------------------
/js/recorder.js:
--------------------------------------------------------------------------------
1 | class PianoRecorder {
2 | constructor(piano) {
3 | console.log('PianoRecorder constructor called');
4 | this.piano = piano;
5 | this.mediaRecorder = null;
6 | this.audioContext = null;
7 | this.audioChunks = [];
8 | this.recordingStartTime = null;
9 | this.recordingTimer = null;
10 | this.audioBlob = null;
11 | this.audioUrl = null;
12 |
13 | // 等待 DOM 加载完成后再初始化
14 | if (document.readyState === 'complete') {
15 | console.log('DOM already loaded, initializing recorder UI immediately');
16 | this.initializeUI();
17 | } else {
18 | console.log('Waiting for DOM to load before initializing recorder UI');
19 | document.addEventListener('DOMContentLoaded', () => {
20 | console.log('DOM loaded, initializing recorder UI');
21 | this.initializeUI();
22 | });
23 | }
24 | }
25 |
26 | initializeUI() {
27 | console.log('Initializing recorder UI...');
28 |
29 | // 获取 UI 元素
30 | this.startButton = document.getElementById('start-recording');
31 | this.stopButton = document.getElementById('stop-recording');
32 | this.recordingStatus = document.getElementById('recording-status');
33 | this.recordingTime = document.getElementById('recording-time');
34 | this.sharePanel = document.getElementById('share-panel');
35 | this.recordingPreview = document.getElementById('recording-preview');
36 | this.shareJikeButton = document.getElementById('share-jike');
37 | this.shareTwitterButton = document.getElementById('share-twitter');
38 | this.downloadButton = document.getElementById('download-recording');
39 |
40 | // 检查是否所有元素都存在
41 | if (!this.startButton || !this.stopButton || !this.recordingStatus ||
42 | !this.recordingTime || !this.sharePanel || !this.recordingPreview ||
43 | !this.shareJikeButton || !this.shareTwitterButton || !this.downloadButton) {
44 | console.error('Some recorder UI elements are missing');
45 | return;
46 | }
47 |
48 | console.log('All recorder UI elements found, setting up event listeners');
49 | this.setupEventListeners();
50 | }
51 |
52 | setupEventListeners() {
53 | this.startButton.addEventListener('click', () => this.startRecording());
54 | this.stopButton.addEventListener('click', () => this.stopRecording());
55 | this.shareJikeButton.addEventListener('click', () => this.shareToJike());
56 | this.shareTwitterButton.addEventListener('click', () => this.shareToTwitter());
57 | this.downloadButton.addEventListener('click', () => this.downloadRecording());
58 | console.log('Recorder event listeners set up');
59 | }
60 |
61 | async initAudioContext() {
62 | try {
63 | // 创建音频上下文
64 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
65 |
66 | // 创建音频处理节点
67 | const destination = this.audioContext.createMediaStreamDestination();
68 |
69 | // 连接钢琴音频到录音目标
70 | this.piano.audio.gainNode.connect(destination);
71 |
72 | // 创建 MediaRecorder
73 | this.mediaRecorder = new MediaRecorder(destination.stream);
74 |
75 | // 设置数据处理
76 | this.mediaRecorder.ondataavailable = (event) => {
77 | this.audioChunks.push(event.data);
78 | };
79 |
80 | this.mediaRecorder.onstop = () => {
81 | this.finalizeRecording();
82 | };
83 |
84 | return true;
85 | } catch (error) {
86 | console.error('初始化录音失败:', error);
87 | alert('无法初始化录音功能。请确保您的浏览器支持音频录制。');
88 | return false;
89 | }
90 | }
91 |
92 | async startRecording() {
93 | try {
94 | if (!this.audioContext && !(await this.initAudioContext())) {
95 | return;
96 | }
97 |
98 | // 重置录音数据
99 | this.audioChunks = [];
100 | this.recordingStartTime = Date.now();
101 |
102 | // 开始录音
103 | this.mediaRecorder.start();
104 | this.startTimer();
105 |
106 | // 更新 UI
107 | this.startButton.classList.add('hidden');
108 | this.stopButton.classList.remove('hidden');
109 | this.recordingStatus.classList.remove('hidden');
110 | this.sharePanel.classList.add('hidden');
111 |
112 | console.log('开始录音');
113 | } catch (error) {
114 | console.error('开始录音失败:', error);
115 | alert('无法开始录音。请确保已授予录音权限。');
116 | }
117 | }
118 |
119 | stopRecording() {
120 | if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
121 | this.mediaRecorder.stop();
122 | this.stopTimer();
123 |
124 | // 更新 UI
125 | this.startButton.classList.remove('hidden');
126 | this.stopButton.classList.add('hidden');
127 | this.recordingStatus.classList.add('hidden');
128 |
129 | console.log('停止录音');
130 | }
131 | }
132 |
133 | startTimer() {
134 | this.stopTimer(); // 清除可能存在的旧计时器
135 | this.recordingTimer = setInterval(() => {
136 | const duration = Date.now() - this.recordingStartTime;
137 | const minutes = Math.floor(duration / 60000);
138 | const seconds = Math.floor((duration % 60000) / 1000);
139 | this.recordingTime.textContent =
140 | `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
141 | }, 1000);
142 | }
143 |
144 | stopTimer() {
145 | if (this.recordingTimer) {
146 | clearInterval(this.recordingTimer);
147 | this.recordingTimer = null;
148 | }
149 | }
150 |
151 | finalizeRecording() {
152 | // 创建音频 Blob
153 | this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
154 |
155 | // 创建音频 URL
156 | if (this.audioUrl) {
157 | URL.revokeObjectURL(this.audioUrl);
158 | }
159 | this.audioUrl = URL.createObjectURL(this.audioBlob);
160 |
161 | // 设置预览
162 | this.recordingPreview.src = this.audioUrl;
163 |
164 | // 显示分享面板
165 | this.sharePanel.classList.remove('hidden');
166 | }
167 |
168 | async shareToJike() {
169 | try {
170 | // 创建一个 FormData 对象来上传音频文件
171 | const formData = new FormData();
172 | formData.append('audio', this.audioBlob, 'piano-recording.wav');
173 | formData.append('text', '我在 Piano Online 上录制了一段钢琴演奏!');
174 |
175 | // 这里需要替换成实际的即刻 API 端点
176 | const response = await fetch('YOUR_JIKE_API_ENDPOINT', {
177 | method: 'POST',
178 | body: formData,
179 | headers: {
180 | 'Authorization': 'Bearer YOUR_JIKE_TOKEN'
181 | }
182 | });
183 |
184 | if (response.ok) {
185 | alert('成功分享到即刻!');
186 | } else {
187 | throw new Error('分享失败');
188 | }
189 | } catch (error) {
190 | console.error('分享到即刻失败:', error);
191 | alert('分享到即刻失败,请稍后重试。');
192 | }
193 | }
194 |
195 | shareToTwitter() {
196 | // 创建分享链接
197 | const text = encodeURIComponent('我在 Piano Online 上录制了一段钢琴演奏!');
198 | const url = encodeURIComponent(window.location.href);
199 | const twitterUrl = `https://twitter.com/intent/tweet?text=${text}&url=${url}`;
200 |
201 | // 打开 Twitter 分享窗口
202 | window.open(twitterUrl, '_blank', 'width=550,height=420');
203 | }
204 |
205 | downloadRecording() {
206 | if (!this.audioBlob) {
207 | alert('没有可下载的录音');
208 | return;
209 | }
210 |
211 | // 创建下载链接
212 | const downloadUrl = window.URL.createObjectURL(this.audioBlob);
213 | const a = document.createElement('a');
214 | a.style.display = 'none';
215 | a.href = downloadUrl;
216 | a.download = 'piano-recording.wav';
217 |
218 | // 触发下载
219 | document.body.appendChild(a);
220 | a.click();
221 |
222 | // 清理
223 | setTimeout(() => {
224 | document.body.removeChild(a);
225 | window.URL.revokeObjectURL(downloadUrl);
226 | }, 100);
227 | }
228 |
229 | // 释放资源
230 | dispose() {
231 | this.stopRecording();
232 | this.stopTimer();
233 | if (this.audioUrl) {
234 | URL.revokeObjectURL(this.audioUrl);
235 | }
236 | if (this.audioContext) {
237 | this.audioContext.close();
238 | }
239 | }
240 | }
241 |
242 | export { PianoRecorder };
243 |
--------------------------------------------------------------------------------