Browse Source

Merge branch 'hqyDevNewVersion' into feature-wxl-newVersion

lex 11 months ago
parent
commit
ad459fe664
41 changed files with 916 additions and 545 deletions
  1. 203 199
      src/constant/instruments.ts
  2. 5 0
      src/helpers/communication.ts
  3. 35 32
      src/helpers/customMusicScore.ts
  4. 21 1
      src/helpers/formateMusic.ts
  5. 1 1
      src/helpers/midiPlay.tsx
  6. 48 0
      src/page-instrument/evaluat-model/evaluat-result/index.module.less
  7. 30 26
      src/page-instrument/evaluat-model/evaluat-result/index.tsx
  8. 38 6
      src/page-instrument/evaluat-model/index.tsx
  9. 8 0
      src/page-instrument/follow-model/index.module.less
  10. 4 0
      src/page-instrument/follow-model/index.tsx
  11. 18 4
      src/page-instrument/header-top/index.module.less
  12. 81 52
      src/page-instrument/header-top/index.tsx
  13. 80 53
      src/page-instrument/header-top/modeView.tsx
  14. 27 17
      src/page-instrument/header-top/settting/index.tsx
  15. 9 5
      src/page-instrument/header-top/speed/index.tsx
  16. 7 1
      src/page-instrument/simple-detail/index.module.less
  17. 11 6
      src/page-instrument/simple-detail/index.tsx
  18. 0 0
      src/page-instrument/view-detail/images/refresh_anim.gif
  19. 0 0
      src/page-instrument/view-detail/images/refresh_anim.json
  20. 23 0
      src/page-instrument/view-detail/index.module.less
  21. 17 25
      src/page-instrument/view-detail/index.tsx
  22. 4 5
      src/page-instrument/view-detail/loading.tsx
  23. 16 30
      src/page-instrument/view-detail/smoothAnimation/index.ts
  24. 133 28
      src/state.ts
  25. 5 1
      src/style.css
  26. 1 1
      src/view/abnormal-pop/index.tsx
  27. 0 23
      src/view/audio-list/index.module.less
  28. 11 3
      src/view/audio-list/index.tsx
  29. 19 7
      src/view/evaluating/index.tsx
  30. 0 0
      src/view/fingering/fingering-img/hulusi-flute/index.json
  31. 0 0
      src/view/fingering/fingering-img/pan-flute/index.json
  32. 3 0
      src/view/fingering/index.module.less
  33. 1 1
      src/view/fingering/index.tsx
  34. 4 1
      src/view/follow-practice/index.tsx
  35. 11 0
      src/view/music-score/index.module.less
  36. 23 12
      src/view/music-score/index.tsx
  37. 2 3
      src/view/selection/index.module.less
  38. 13 0
      src/view/selection/index.tsx
  39. 1 1
      src/view/transfer-to-img/index.module.less
  40. 2 0
      src/view/transfer-to-img/index.tsx
  41. 1 1
      vite.config.ts

+ 203 - 199
src/constant/instruments.ts

@@ -1,203 +1,207 @@
 const instruments: any = {
-	"Acoustic Grand Piano": "大钢琴",
-	"Bright Acoustic Piano": "明亮的钢琴",
-	"Electric Grand Piano": "电钢琴",
-	"Rhodes Piano": "柔和的电钢琴",
-	"Chorused Piano": "加合唱效果的电钢琴",
-	Harpsichord: "羽管键琴",
-	Clavichord: "科拉维科特琴",
-	Celesta: "钢片琴",
-	Glockenspiel: "钢片琴",
-	"Music box": "八音盒",
-	Vibraphone: "颤音琴",
-	Marimba: "马林巴",
-	Xylophone: "木琴",
-	"Tubular Bells": "管钟",
-	Dulcimer: "大扬琴",
-	"Hammond Organ": "击杆风琴",
-	"Percussive Organ": "打击式风琴",
-	"Rock Organ": "摇滚风琴",
-	"Church Organ": "教堂风琴",
-	"Reed Organ": "簧管风琴",
-	Accordian: "手风琴",
-	Harmonica: "口琴",
-	"Tango Accordian": "探戈手风琴",
-	"Acoustic Guitar": "钢弦吉他",
-	"Electric Guitar": "闷音电吉他",
-	"Overdriven Guitar": "加驱动效果的电吉他",
-	"Distortion Guitar": "加失真效果的电吉他",
-	"Guitar Harmonics": "吉他和音",
-	"Acoustic Bass": "大贝司",
-	"Electric Bass": "电贝司",
-	"Fretless Bass": "无品贝司",
-	"Slap Bass": "掌击",
-	"Synth Bass": "电子合成",
-	Violin: "小提琴",
-	Viola: "中提琴",
-	Cello: "大提琴",
-	Contrabass: "低音大提琴",
-	"Tremolo Strings": "弦乐群颤音音色",
-	"Pizzicato Strings": "弦乐群拨弦音色",
-	"Orchestral Harp": "竖琴",
-	Timpani: "定音鼓",
-	"String Ensemble": "弦乐合奏音色",
-	"Synth Strings": "合成弦乐合奏音色",
-	"Choir Aahs": "人声合唱",
-	"Voice Oohs": "人声",
-	"Synth Voice": "合成人声",
-	"Orchestra Hit": "管弦乐敲击齐奏",
-	Trumpet: "小号",
-	Trombone: "长号",
-	Tuba: "大号",
-	"Muted Trumpet": "加弱音器小号",
-	"French Horn": "法国号",
-	"Brass Section": "铜管组",
-	"Synth Brass": "合成铜管音色",
-	"Soprano Sax": "高音萨克斯管",
-	"Alto Sax": "中音萨克斯管",
-	"Tenor Sax": "次中音萨克斯管",
-	"Baritone Sax": "低音萨克斯管",
-	Oboe: "双簧管",
-	"English Horn": "英国管",
-	Bassoon: "巴松",
-	"Soprano Saxophone": "高音萨克斯管",
-	"Alto Saxophone": "中音萨克斯管",
-	"Tenor Saxophone": "次中音萨克斯管",
-	"Baritone Saxophone": "低音萨克斯管",
-	Piccolo: "短笛",
-	Flute: "长笛",
-	Recorder: "竖笛",
-	"Soprano Recorder": "高音竖笛",
-	"Pan Flute": "排箫",
-	"Bottle Blow": "瓶木管",
-	Whistle: "口哨声",
-	Ocarina: "陶笛",
-	Lead: "合成主音",
-	"Lead lead": "合成主音",
-	"Pad age": "合成音色",
-	Pad: "合成音色",
-	FX: "合成效果  科幻",
-	Sitar: "西塔尔",
-	Banjo: "班卓琴",
-	Shamisen: "三昧线",
-	Koto: "十三弦筝",
-	Kalimba: "卡林巴",
-	Bagpipe: "风笛",
-	Fiddle: "民族提琴",
-	Shanai: "山奈",
-	"Tinkle Bell": "叮当铃",
-	Agogos: "阿戈戈铃",
-	"Steel Drums": "钢鼓",
-	"Taiko Drum": "太鼓",
-	"Melodic Toms": "嗵嗵鼓",
-	"Synth Drums": "合成鼓",
-	"Reverse Cymbals": "反向镲",
-	"Agogo Bells": "阿戈戈铃",
-	"Taiko Drums": "太鼓",
-	Bongos: "邦戈鼓",
-	"Bongo Bell": "邦戈铃",
-	Congas: "康加鼓",
-	Guiro: "刮壶",
-	"Guitar Fret Noise": "吉他换把杂音",
-	"Breath Noise": "呼吸声",
-	Seashore: "海浪声",
-	"Bird Tweet": "鸟鸣",
-	"Telephone Ring": "电话铃",
-	Helicopter: "直升机",
-	Applause: "鼓掌声",
-	Gunshot: "枪声",
-	"Acoustic Bass Drum": "大鼓",
-	"Bass Drum": "大鼓",
-	"Side Drum": "小鼓鼓边",
-	"Acoustic Snare": "小鼓",
-	"Hand Claps": "拍手",
-	"Electric Snare": "小鼓",
-	"Low Floor Tom": "低音嗵鼓",
-	"Closed Hi-Hat": "闭合踩镲",
-	"High Floor Tom": "高音落地嗵鼓",
-	"Pedal Hi-Hat": "脚踏踩镲",
-	"Low Tom": "低音嗵鼓",
-	"Open Hi-Hat": "开音踩镲",
-	"Low-Mid Tom": "中低音嗵鼓",
-	"Hi Mid Tom": "高音鼓",
-	"Crash Cymbals": "对镲",
-	"High Tom": "高音嗵鼓",
-	"Ride Cymbals": "叮叮镲",
-	"Chinese Cymbals": "中国镲",
-	"Ride Bell": "圆铃",
-	Tambourine: "铃鼓",
-	"Splash Cymbal": "溅音镲",
-	Cowbell: "牛铃",
-	"Crash Cymbal": "强音钹",
-	"Vibra-Slap": "颤音器",
-	"Ride Cymbal": "打点钹",
-	"Hi Bongo": "高音邦戈鼓",
-	"Low Bongo": "低音邦戈鼓",
-	"Mute Hi Conga": "弱音高音康加鼓",
-	"Open Hi Conga": "强音高音康加鼓",
-	"Low Conga": "低音康加鼓",
-	"High Timbale": "高音天巴鼓",
-	"Low Timbale": "低音天巴鼓",
-	"High Agogo": "高音阿戈戈铃",
-	"Low Agogo": "低音阿戈戈铃",
-	Cabasa: "卡巴萨",
-	Maracas: "沙锤",
-	"Short Whistle": "短口哨",
-	"Long Whistle": "长口哨",
-	"Short Guiro": "短刮壶",
-	"Long Guiro": "长刮壶",
-	Claves: "响棒",
-	"Hi Wood Block": "高音木鱼",
-	"Low Wood Block": "低音木鱼",
-	"Mute Triangle": "弱音三角铁",
-	"Open Triangle": "强音三角铁",
-	"Drum Set": "架子鼓",
-	"Hulusi flute": "葫芦丝",
-	Melodica: "口风琴",
-	Nai: "口风琴",
-	"Snare Drum": "小军鼓",
-	"Horn in F": "圆号",
-	Triangle: "三角铁",
-	Vibrato: "颤音琴",
-	"Suspend Cymbals": "吊镲",
-	"Suspended Cymbals": "吊镲",
-	"Tom-Toms": "嗵嗵鼓",
-	Bell: "铃铛",
-	Bells: "铃铛",
-	"Alto Clarinet": "中音单簧管",
-	"Bass Clarinet": "低音单簧管",
-	Clarinet: "单簧管",
-	Cornet: "短号",
-	Euphonium: "上低音号",
-	"crash cymbals": "对镲",
-	Castanets: "响板",
-	Shaker: "沙锤",
-	"Mark tree": "音树",
-	Chimes: "管钟",
-	"Mark Tree": "音树",
-	"Tom-toms": "嗵嗵鼓",
-	"Hi-Hat": "踩镲",
-	"Sleigh Bells": "雪橇铃",
-	Flexatone: "弹音器",
-	"Brake drum": "闸鼓",
-	Gong: "锣",
-	"concert tom": "音乐会嗵嗵鼓",
-	"brake drum": "车轮鼓",
-	"finger cymbal": "指钹",
-	"ride cymbal": "叮叮镲",
-	"Concert Toms": "音乐会嗵嗵鼓",
-	Vibraslap: "弹音器",
-	"Wood Blocks": "木鱼",
-	"Temple Blocks": "木鱼",
-	"Wood Block": "木鱼",
-	"Field Drum": "军鼓",
-	"Quad-Toms": "筒鼓",
-	Quads: "筒鼓",
-	"Drums set": "架子鼓",
-	"High Bongo": "邦戈",
-	Timbales: "天巴鼓",
-	Cymbal: "镲",
-	Cymbals: "镲",
+	'Acoustic Grand Piano': '大钢琴',
+	'Bright Acoustic Piano': '明亮的钢琴',
+	'Electric Grand Piano': '电钢琴',
+	'Rhodes Piano': '柔和的电钢琴',
+	'Chorused Piano': '加合唱效果的电钢琴',
+	Harpsichord: '羽管键琴',
+	Clavichord: '科拉维科特琴',
+	Celesta: '钢片琴',
+	Glockenspiel: '钢片琴',
+	'Music box': '八音盒',
+	Vibraphone: '颤音琴',
+	Marimba: '马林巴',
+	Xylophone: '木琴',
+	'Tubular Bells': '管钟',
+	Dulcimer: '大扬琴',
+	'Hammond Organ': '击杆风琴',
+	'Percussive Organ': '打击式风琴',
+	'Rock Organ': '摇滚风琴',
+	'Church Organ': '教堂风琴',
+	'Reed Organ': '簧管风琴',
+	Accordian: '手风琴',
+	Harmonica: '口琴',
+	'Tango Accordian': '探戈手风琴',
+	'Acoustic Guitar': '钢弦吉他',
+	'Electric Guitar': '闷音电吉他',
+	'Overdriven Guitar': '加驱动效果的电吉他',
+	'Distortion Guitar': '加失真效果的电吉他',
+	'Guitar Harmonics': '吉他和音',
+	'Acoustic Bass': '大贝司',
+	'Electric Bass': '电贝司',
+	'Fretless Bass': '无品贝司',
+	'Slap Bass': '掌击',
+	'Synth Bass': '电子合成',
+	Violin: '小提琴',
+	Viola: '中提琴',
+	Cello: '大提琴',
+	Contrabass: '低音大提琴',
+	'Tremolo Strings': '弦乐群颤音音色',
+	'Pizzicato Strings': '弦乐群拨弦音色',
+	'Orchestral Harp': '竖琴',
+	Timpani: '定音鼓',
+	'String Ensemble': '弦乐合奏音色',
+	'Synth Strings': '合成弦乐合奏音色',
+	'Choir Aahs': '人声合唱',
+	'Voice Oohs': '人声',
+	'Synth Voice': '合成人声',
+	'Orchestra Hit': '管弦乐敲击齐奏',
+	Trumpet: '小号',
+	Trombone: '长号',
+	Tuba: '大号',
+	'Muted Trumpet': '加弱音器小号',
+	'French Horn': '法国号',
+	'Brass Section': '铜管组',
+	'Synth Brass': '合成铜管音色',
+	'Soprano Sax': '高音萨克斯管',
+	'Alto Sax': '中音萨克斯管',
+	'Tenor Sax': '次中音萨克斯管',
+	'Baritone Sax': '低音萨克斯管',
+	Oboe: '双簧管',
+	'English Horn': '英国管',
+	Bassoon: '巴松',
+	'Soprano Saxophone': '高音萨克斯管',
+	'Alto Saxophone': '中音萨克斯管',
+	'Tenor Saxophone': '次中音萨克斯管',
+	'Baritone Saxophone': '低音萨克斯管',
+	Piccolo: '短笛',
+	Flute: '长笛',
+	Recorder: '竖笛',
+	'Soprano Recorder': '高音竖笛',
+	'Pan Flute': '排箫',
+	'Bottle Blow': '瓶木管',
+	Whistle: '口哨声',
+	Ocarina: '陶笛',
+	Lead: '合成主音',
+	'Lead lead': '合成主音',
+	'Pad age': '合成音色',
+	Pad: '合成音色',
+	FX: '合成效果  科幻',
+	Sitar: '西塔尔',
+	Banjo: '班卓琴',
+	Shamisen: '三昧线',
+	Koto: '十三弦筝',
+	Kalimba: '卡林巴',
+	Bagpipe: '风笛',
+	Fiddle: '民族提琴',
+	Shanai: '山奈',
+	'Tinkle Bell': '叮当铃',
+	Agogos: '阿戈戈铃',
+	'Steel Drums': '钢鼓',
+	'Taiko Drum': '太鼓',
+	'Melodic Toms': '嗵嗵鼓',
+	'Synth Drums': '合成鼓',
+	'Reverse Cymbals': '反向镲',
+	'Agogo Bells': '阿戈戈铃',
+	'Taiko Drums': '太鼓',
+	Bongos: '邦戈鼓',
+	'Bongo Bell': '邦戈铃',
+	Congas: '康加鼓',
+	Guiro: '刮壶',
+	'Guitar Fret Noise': '吉他换把杂音',
+	'Breath Noise': '呼吸声',
+	Seashore: '海浪声',
+	'Bird Tweet': '鸟鸣',
+	'Telephone Ring': '电话铃',
+	Helicopter: '直升机',
+	Applause: '鼓掌声',
+	Gunshot: '枪声',
+	'Acoustic Bass Drum': '大鼓',
+	'Bass Drum': '大鼓',
+	'Side Drum': '小鼓鼓边',
+	'Acoustic Snare': '小鼓',
+	'Hand Claps': '拍手',
+	'Electric Snare': '小鼓',
+	'Low Floor Tom': '低音嗵鼓',
+	'Closed Hi-Hat': '闭合踩镲',
+	'High Floor Tom': '高音落地嗵鼓',
+	'Pedal Hi-Hat': '脚踏踩镲',
+	'Low Tom': '低音嗵鼓',
+	'Open Hi-Hat': '开音踩镲',
+	'Low-Mid Tom': '中低音嗵鼓',
+	'Hi Mid Tom': '高音鼓',
+	'Crash Cymbals': '对镲',
+	'High Tom': '高音嗵鼓',
+	'Ride Cymbals': '叮叮镲',
+	'Chinese Cymbals': '中国镲',
+	'Ride Bell': '圆铃',
+	Tambourine: '铃鼓',
+	'Splash Cymbal': '溅音镲',
+	Cowbell: '牛铃',
+	'Crash Cymbal': '强音钹',
+	'Vibra-Slap': '颤音器',
+	'Ride Cymbal': '打点钹',
+	'Hi Bongo': '高音邦戈鼓',
+	'Low Bongo': '低音邦戈鼓',
+	'Mute Hi Conga': '弱音高音康加鼓',
+	'Open Hi Conga': '强音高音康加鼓',
+	'Low Conga': '低音康加鼓',
+	'High Timbale': '高音天巴鼓',
+	'Low Timbale': '低音天巴鼓',
+	'High Agogo': '高音阿戈戈铃',
+	'Low Agogo': '低音阿戈戈铃',
+	Cabasa: '卡巴萨',
+	Maracas: '沙锤',
+	'Short Whistle': '短口哨',
+	'Long Whistle': '长口哨',
+	'Short Guiro': '短刮壶',
+	'Long Guiro': '长刮壶',
+	Claves: '响棒',
+	'Hi Wood Block': '高音木鱼',
+	'Low Wood Block': '低音木鱼',
+	'Mute Triangle': '弱音三角铁',
+	'Open Triangle': '强音三角铁',
+	'Drum Set': '架子鼓',
+	'Hulusi flute': '葫芦丝',
+	Melodica: '口风琴',
+	'Snare Drum': '小军鼓',
+	'Horn in F': '圆号',
+	Triangle: '三角铁',
+	Vibrato: '颤音琴',
+	'Suspend Cymbals': '吊镲',
+	'Suspended Cymbals': '吊镲',
+	'Tom-Toms': '嗵嗵鼓',
+	Bell: '铃铛',
+	Bells: '铃铛',
+	'Alto Clarinet': '中音单簧管',
+	'Bass Clarinet': '低音单簧管',
+	Clarinet: '单簧管',
+	Cornet: '短号',
+	Euphonium: '上低音号',
+	'crash cymbals': '对镲',
+	Castanets: '响板',
+	Shaker: '沙锤',
+	'Mark tree': '音树',
+	Chimes: '管钟',
+	'Mark Tree': '音树',
+	'Tom-toms': '嗵嗵鼓',
+	'Hi-Hat': '踩镲',
+	'Sleigh Bells': '雪橇铃',
+	Flexatone: '弹音器',
+	'Brake drum': '闸鼓',
+	Gong: '锣',
+	'concert tom': '音乐会嗵嗵鼓',
+	'brake drum': '车轮鼓',
+	'finger cymbal': '指钹',
+	'ride cymbal': '叮叮镲',
+	'Concert Toms': '音乐会嗵嗵鼓',
+	Vibraslap: '弹音器',
+	'Wood Blocks': '木鱼',
+	'Temple Blocks': '木鱼',
+	'Wood Block': '木鱼',
+	'Field Drum': '军鼓',
+	'Quad-Toms': '筒鼓',
+	Quads: '筒鼓',
+	'Drums set': '架子鼓',
+	'High Bongo': '邦戈',
+	Timbales: '天巴鼓',
+	'rain stick': '雨棒',
+	'String Bass': '弦乐低音',
+	'Floor Tom': '侧嗵鼓',
+	'Brake Drum': '闸鼓',
+	'Tam-tam': '大锣',
+	Cymbal: '镲',
+	Cymbals: '镲'
 };
 /** 获取分轨名称 */
 export const getInstrumentName = (name = '') => {

+ 5 - 0
src/helpers/communication.ts

@@ -506,6 +506,11 @@ export const api_finishDelayCheck = (callback: any) => {
 	listenerMessage("finishDelayCheck", callback);
 };
 
+/** 监听延迟检测成功的回调 */
+export const api_remove_finishDelayCheck = (callback: any) => {
+	removeListenerMessage("finishDelayCheck", callback);
+};
+
 /** 监听APP播放进度 */
 export const simple_playProgress = (callback: any) => {
 	listenerMessage("api_playProgress", callback);

+ 35 - 32
src/helpers/customMusicScore.ts

@@ -534,38 +534,41 @@ export const resetFormate = () => {
 		};
 
 		// 给小节添加背景色
-		staves.forEach((stave: any) => {
-			const list = [
-				Array.from(stave?.querySelectorAll(".vf-StaveSection") || []),
-				Array.from(stave?.querySelectorAll(".vf-Volta") || []),
-				Array.from(stave?.querySelectorAll(".vf-clef") || []),
-				Array.from(stave?.querySelectorAll(".vf-keysignature") || []),
-				Array.from(stave?.getElementsByTagName("text") || []),
-			].flat();
-			try {
-				if (list.length) {
-					list.forEach((_el: any) => {
-						stave?.removeChild(_el)
-						_el?.style?.setProperty("display", "none");
-					});
-				}
-			} catch (error) {}
-			const bbox = stave?.getBBox() || {};
-			const rect = `<rect class="vf-custom-bg" x="${bbox.x}" y="${bbox.y}" width="${bbox.width}" height="${bbox.height}" fill="#609FCF" />`
-			const rectBottom = `<rect class="vf-custom-bot" x="${bbox.x}" y="${bbox.y+bbox.height}" width="${bbox.width}" height="12" fill="#2B70A5" />`
-			const customG = `<g>${rect}${rectBottom}</g>`
-			try {
-				if (list.length) {
-					list.forEach((_el: any) => {
-						stave?.appendChild(_el)
-						_el?.style?.removeProperty("display");
-					});
-				}
-			} catch (error) {}
-			stave.innerHTML = customG + stave.innerHTML;
-		});
-		
-		state.vfmeasures = state.vfmeasures.concat(vfmeasures);
+		if (!state.isCreateImg) {
+			staves.forEach((stave: any) => {
+				const list = [
+					Array.from(stave?.querySelectorAll(".vf-StaveSection") || []),
+					Array.from(stave?.querySelectorAll(".vf-Volta") || []),
+					Array.from(stave?.querySelectorAll(".vf-clef") || []),
+					Array.from(stave?.querySelectorAll(".vf-keysignature") || []),
+					Array.from(stave?.querySelectorAll(".vf-Repetition") || []),
+					Array.from(stave?.getElementsByTagName("text") || []),
+				].flat();
+				try {
+					if (list.length) {
+						list.forEach((_el: any) => {
+							stave?.removeChild(_el)
+							_el?.style?.setProperty("display", "none");
+						});
+					}
+				} catch (error) {}
+				const bbox = stave?.getBBox() || {};
+				const rect = `<rect class="vf-custom-bg" x="${bbox.x}" y="${bbox.y}" width="${bbox.width}" height="${bbox.height}" fill="#609FCF" />`
+				const rectBottom = `<rect class="vf-custom-bot" x="${bbox.x}" y="${bbox.y+bbox.height}" width="${bbox.width}" height="12" fill="#2B70A5" />`
+				const customG = `<g>${rect}${rectBottom}</g>`
+				try {
+					if (list.length) {
+						list.forEach((_el: any) => {
+							stave?.appendChild(_el)
+							_el?.style?.removeProperty("display");
+						});
+					}
+				} catch (error) {}
+				stave.innerHTML = customG + stave.innerHTML;
+			});
+			state.vfmeasures = state.vfmeasures.concat(vfmeasures);
+		}
+
 	}
 	
 	// setTimeout(() => this.resetGlobalText());

+ 21 - 1
src/helpers/formateMusic.ts

@@ -876,6 +876,9 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			if (note.IsGraceNote) {
 				// 如果是装饰音, 取不是装饰音的时值
 				const voice = note.parentStaffEntry.voiceEntries.find((_v: any) => !_v.isGrace);
+				if (!voice) {
+					continue;
+				}
 				note = voice.notes[0];
 			}
 			note.fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments[0].fixedKey || 0;
@@ -969,6 +972,18 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			
 			activeVerticalMeasureList = [note.sourceMeasure?.verticalMeasureList?.[0]] || [];
 
+			/**
+			 * TODO:多分轨合并的小节,音符可能没有id,此时就去其它分轨找
+			 */
+			const vmLength = note.sourceMeasure?.verticalMeasureList?.length
+			let currentVmIndex = 0;
+			let hasSvgElement = activeVerticalMeasureList[0]?.vfVoices['1']?.tickables[staveNoteIndex];
+			while (!hasSvgElement && vmLength > 1 && currentVmIndex <= vmLength - 1) {
+				currentVmIndex += 1;
+				activeVerticalMeasureList = [note.sourceMeasure?.verticalMeasureList?.[currentVmIndex]] || [];
+				hasSvgElement = activeVerticalMeasureList[0]?.vfVoices['1']?.tickables[staveNoteIndex];
+			}
+
 			const { realValue } = iterator.currentTimeStamp;
 			const { RealValue: vRealValue, Denominator: vDenominator } = formatDuration(
 				iterator.currentMeasure.activeTimeSignature,
@@ -1195,6 +1210,11 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 			svgElement?.attrs.id && noteIds.push(svgElement?.attrs.id)
 
+			// 如果该音符包含倚音,添加标记
+			let hasGraceNote = false;
+			if (svgElement?.modifiers?.length) {
+				hasGraceNote = svgElement?.modifiers.some((item: any) => item?.attrs?.type === "GraceNoteGroup")
+			}
 			const nodeDetail = {
 				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
@@ -1219,6 +1239,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				measureLength,
 				relaMeasureLength,
 				id: svgElement?.attrs.id,
+				hasGraceNote,
 				note: note.halfTone + 12, // see issue #224
 				fixtime, // 弱起补充的时间
 				relativeTime: retain(relativeTime),
@@ -1302,7 +1323,6 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	try {
 		osmd.cursor.reset();
 	} catch (error) {}
-	state.activeMeasureIndex = sortArray[0].MeasureNumberXML;
 	return sortArray;
 };
 

+ 1 - 1
src/helpers/midiPlay.tsx

@@ -27,7 +27,7 @@ export const initMidi = (durationNum: number, midiUrl?: string) => {
       denominator: duration.denominator,
       numerator: duration.numerator,
       originalSpeed: state.originSpeed,
-      interval: 50,
+      interval: 16,
       duration: durationNum * 1000,
     }, () => {
       state.midiPlayIniting = false

+ 48 - 0
src/page-instrument/evaluat-model/evaluat-result/index.module.less

@@ -177,6 +177,54 @@
     }
 }
 
+.saveBtn {
+    position: relative;
+}
+.noSaveTip {
+    background: rgba(0,0,0,0.7);
+    font-size: 13Px;
+    border-radius: 8Px;
+    display: flex;
+    align-items: center;
+    padding: 8Px 12Px;
+    position: absolute;
+    left: 50%;
+    top: -45Px;
+    transform: translateX(-50%);
+    z-index: 1;
+    color: #fff;
+    width: auto;
+    word-break: keep-all;
+    > i {
+      display: inline-block;
+      min-width: 12Px;
+      width: 12Px;
+      height: 12Px;
+      background-image: url('./icons/close_icon.png');
+      background-size: 100% 100%;
+      background-position: center center;
+      background-repeat: no-repeat;
+      margin-left: 16Px;
+      cursor: pointer;
+    }
+    span {
+      word-break: keep-all;
+      width: max-content;
+    }
+    .arrowIcon {
+      position: absolute;
+      left: 50%;
+      bottom: -8PX;
+      transform: translateX(-50%);
+      width: 0;
+      height: 0;
+      border-top: 8PX solid rgba(0,0,0,0.7);
+      border-right: 8PX solid transparent;
+      border-left: 8PX solid transparent;  
+      z-index: 2;      
+    }
+}
+
 :global {
     .savePopoverClose {
         &.van-popover {

+ 30 - 26
src/page-instrument/evaluat-model/evaluat-result/index.tsx

@@ -1,4 +1,4 @@
-import { defineComponent, onMounted, reactive, watch } from "vue";
+import { defineComponent, onMounted, reactive, watch, computed } from "vue";
 import { Popover } from "vant";
 import styles from "./index.module.less";
 import state from "/src/state";
@@ -79,6 +79,16 @@ export default defineComponent({
       emit("close", "update");
     };
 
+    // 播放倍率不等于1,或者是选段评测,APP暂时不支持保存演奏,需要给出提示
+    const noSaveTips = computed(() => {
+      let tipContent = '';
+      const rate = state.basePlayRate * state.originAudioPlayRate; // 播放倍率
+      if (query.workRecord || state.section.length === 2 || state.isAppPlay || rate != 1 || !state.accompany) {
+        tipContent = query.workRecord ? '评测作业暂不支持保存作品噢~' : (!state.accompany || state.isAppPlay) ? '该曲目暂不支持保存作品噢~' : state.section.length === 2 ? '选段后暂不支持保存作品噢~' : rate != 1 ? '调速后暂不支持保存作品噢~' : '';
+      }
+      return tipContent
+    })
+
     onMounted(() => {
       if (!evaluatingData.isErrorState) {
         handleAddRecord();
@@ -112,12 +122,12 @@ export default defineComponent({
                 {evaluatingData.resultData.score > 79 && <img class={styles.badge} src={iconBadge} />}
                 <div class={[styles.text, evaluatingData.resultData.score > 79 && styles.badgeText]}>
                   <div class={[styles.scoreSection, "evaluting-result-1"]}>
-                    <div class={styles.num}>{evaluatingData.resultData.score}00</div>
+                    <div class={styles.num}>{evaluatingData.resultData.score}</div>
                     <div class={styles.score}>分</div>
                     <div class={styles.level}>
                       <div>{level[evaluatingData.resultData.heardLevel]}</div>
                       <span>|</span>
-                      <div>速度{evaluatingData.resultData.speed}</div>
+                      <div>速度{evaluatingData.resultData.speed || state.speed}</div>
                     </div>
                   </div>
                 </div>
@@ -155,29 +165,23 @@ export default defineComponent({
               <div class={styles.tips}>{evaluatingData.resultData.clxtip}</div>
               <div class={styles.ctrls}>
                 <img src={zlycImg} class={[styles.ctrlsBtn, "evaluting-result-2"]} onClick={() => emit("close", "tryagain")} />
-                {!state.isHideEvaluatReportSaveBtn && evaluatingData.resultData.recordId ? (
-                  <img src={bczpImg} class={[styles.ctrlsBtn, "evaluting-result-3"]} onClick={debounce(saveResult, 300)} />
-                ) : (
-                  <Popover
-                    class={"savePopoverClose"}
-                    placement={"top"}
-                    v-model:show={data.showPopover}
-                    v-slots={{
-                      reference: () => <img src={bczpJzImg} class={[styles.ctrlsBtn, "evaluting-result-3"]} />,
-                    }}
-                    theme="dark"
-                  >
-                    <div class={"popoverClose"}>
-                      <div>该曲目暂不支持保存作品噢~</div>
-                      <img
-                        src={closeImg}
-                        onClick={() => {
-                          data.showPopover = false;
-                        }}
-                      />
-                    </div>
-                  </Popover>
-                )}
+                {evaluatingData.resultData.recordId ? (
+                  <div class={styles.saveBtn}>
+                    <img src={noSaveTips.value ? bczpJzImg : bczpImg} class={[styles.ctrlsBtn, "evaluting-result-3"]} style={{ opacity: state.isHideEvaluatReportSaveBtn ? 0.4 : 1 }} onClick={() => {
+                      if (!noSaveTips.value && !state.isHideEvaluatReportSaveBtn) {
+                        saveResult()
+                      }
+                    }} />
+                    {
+                      noSaveTips.value && state.noSavePopShow ? 
+                      <div class={[styles.noSaveTip]}>
+                        <span class={styles.arrowIcon}></span>
+                        <span>{noSaveTips.value}</span>
+                        <i onClick={() => state.noSavePopShow = false}></i>
+                      </div> : null                   
+                    }
+                  </div>
+                ) : null }
                 <img src={ckzpImg} class={[styles.ctrlsBtn, "evaluting-result-4", data.saveLoading ? styles.disablued : ""]} onClick={() => emit("close", "look")} />
               </div>
             </div>

+ 38 - 6
src/page-instrument/evaluat-model/index.tsx

@@ -1,16 +1,16 @@
-import { Transition, defineComponent, onMounted, reactive, watch, defineAsyncComponent, computed } from "vue";
+import { Transition, defineComponent, onMounted, reactive, watch, defineAsyncComponent, computed, onUnmounted } from "vue";
 import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay, checkUseEarphone, handleCancelEvaluat } from "/src/view/evaluating";
 import Earphone from "./earphone";
 import styles from "./index.module.less";
 import SoundEffect from "./sound-effect";
-import state, { handleRessetState, resetPlaybackToStart, musicalInstrumentCodeInfo } from "/src/state";
+import state, { handleRessetState, resetPlaybackToStart, musicalInstrumentCodeInfo, clearSelection } from "/src/state";
 import { storeData } from "/src/store";
 import { browser } from "/src/utils";
 import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
 import { Icon, Popup, showToast, closeToast, showLoadingToast } from "vant";
 import EvaluatResult from "./evaluat-result";
 import EvaluatAudio from "./evaluat-audio";
-import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone, api_back, api_startDelayCheck, api_cancelDelayCheck, api_closeDelayCheck, api_finishDelayCheck, api_retryEvaluating } from "/src/helpers/communication";
+import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone, api_back, api_startDelayCheck, api_cancelDelayCheck, api_closeDelayCheck, api_finishDelayCheck, api_retryEvaluating, api_remove_finishDelayCheck } from "/src/helpers/communication";
 import EvaluatShare from "./evaluat-share";
 import { Vue3Lottie } from "vue3-lottie";
 import startData from "./data/start.json";
@@ -38,6 +38,7 @@ type TCriteria = "frequency" | "amplitude" | "decibels";
  */
 let actualBeatLength = 0;
 let calculateInfo: any = {};
+let checkErjiTimer: any = null
 
 export default defineComponent({
   name: "evaluat-model",
@@ -125,6 +126,8 @@ export default defineComponent({
 
     /** 校验耳机状态 */
     const checkEarphoneStatus = async (type?: string) => {
+      clearTimeout(checkErjiTimer);
+      checkErjiTimer = null;
       if (type !== "start") {
         // const erji = await checkUseEarphone();
         const res = await getEarphone();
@@ -133,9 +136,16 @@ export default defineComponent({
         evaluatingData.earphoneMode = true;
         evaluatingData.earPhoneType = res?.content?.type || "";
         if (evaluatingData.earPhoneType === "有线耳机") {
+          clearTimeout(checkErjiTimer);
+          checkErjiTimer = null;
           setTimeout(() => {
             evaluatingData.earphoneMode = false;
           }, 3000);
+        } else {
+          // 如果没有佩戴有限耳机,需要持续检测耳机状态
+          checkErjiTimer = setTimeout(() => {
+            checkEarphoneStatus();
+          }, 1000);
         }
       }
       console.log("检测结束,生成数据", evaluatingData.websocketState, evaluatingData.startBegin, evaluatingData.checkEnd);
@@ -219,6 +229,12 @@ export default defineComponent({
           measureIndex++;
           recordMeasure = note.measureOpenIndex;
         }
+        // 是否是需要延续、不停顿演奏的音符
+        let isTenutoSound = false;
+        if (item?.noteElement?.tie && item.noteElement.tie?.StartNote) {
+          const startId = item.noteElement.tie?.StartNote?.NoteToGraphicalNoteObjectId
+          isTenutoSound = item.NoteToGraphicalNoteObjectId === startId ? false : true
+        }
         const data = {
           timeStamp: (start * 1000) / rate,
           duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
@@ -228,10 +244,12 @@ export default defineComponent({
           // 重复的情况index会自然累加,render的index是谱面渲染的index
           measureIndex: measureIndex,
           measureRenderIndex: item.measureListIndex,
-          dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
+          dontEvaluating: item.hasGraceNote || ListenMode || dontEvaluatingMode || !!item?.voiceEntry?.ornamentContainer || !!item.noteElement?.speedInfo?.startWord?.includes('rit.') || item.skipMode,
           musicalNotesIndex: index,
           denominator: note.noteElement?.Length.denominator,
-          isOrnament: !!note?.voiceEntry?.ornamentContainer,
+          // isOrnament: !!note?.voiceEntry?.ornamentContainer,
+          isTenutoSound,
+          isStaccato: item?.voiceEntry?.isStaccato, // 是否是重音
         };
         datas.push(data);
       }
@@ -349,6 +367,10 @@ export default defineComponent({
     };
 
     const startBtnHandle = async () => {
+      // 选段未完成时,清除选段状态
+      if (state.sectionStatus && state.section.length < 2) {
+        clearSelection();
+      }
       // 如果是异常状态,先等待500ms再执行后续流程
       if (evaluatingData.isErrorState && !state.setting.soundEffect) {
         // console.log('异常流程1')
@@ -393,6 +415,7 @@ export default defineComponent({
     // 监听APP延迟成功的回调
     const handleFinishDelayCheck = async (res?: IPostMessage) => {
       console.log("监听延迟检测成功", res);
+      evaluatingData.socketErrorPop = false;
       if (res?.content) {
         evaluatingData.checkEnd = true;
         checkEarphoneStatus();
@@ -405,7 +428,7 @@ export default defineComponent({
     };
 
     const earPhonePopShow = computed(() => {
-			return evaluatingData.earphoneMode && state.audioDone && !state.hasDriverPop;
+			return evaluatingData.earphoneMode && !state.isLoading && !state.hasDriverPop;
 		});
 
     onMounted(async () => {
@@ -422,6 +445,13 @@ export default defineComponent({
       api_finishDelayCheck(handleFinishDelayCheck);
       api_retryEvaluating(handRetryEvaluating);
     });
+    
+    onUnmounted(() => {
+      api_remove_finishDelayCheck(handleFinishDelayCheck);
+			clearTimeout(checkErjiTimer);
+      checkErjiTimer = null;
+		});
+
     return () => (
       <div>
         <div class={styles.operatingBtn}>
@@ -462,6 +492,8 @@ export default defineComponent({
           <Earphone
             earphoneType={evaluatingData.earPhoneType}
             onClose={() => {
+              clearTimeout(checkErjiTimer);
+              checkErjiTimer = null;
               evaluatingData.earphoneMode = false;
               // handlePerformDetection();
               checkEarphoneStatus("start");

+ 8 - 0
src/page-instrument/follow-model/index.module.less

@@ -84,4 +84,12 @@
           margin-left: 20px;
       }
   }
+}
+.beginMask{
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 10000;
 }

+ 4 - 0
src/page-instrument/follow-model/index.tsx

@@ -37,6 +37,10 @@ export default defineComponent({
 						</div>
 					)}
 				</Transition> */}
+        {/* 遮罩 */}
+        {
+          followData.isBeginMask && <div class={styles.beginMask}></div>
+        }        
         <div class={styles.operatingBtn}>
           {!followData.start && (
             <img

+ 18 - 4
src/page-instrument/header-top/index.module.less

@@ -7,6 +7,9 @@
     margin-left: calc(-1 * var(--detailDataPaddingLeft));
     padding: 0 30px;
     background: linear-gradient( 180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%);
+    &.headerTopRight {
+        justify-content: flex-end;
+    }
 }
 .modeWarn{
     position: fixed;
@@ -30,6 +33,9 @@
         width: 18px;
         height: 18px;
     }
+    &.modeWarnRight {
+        right: 30px;
+    }
 }
 .headTopLeftBox{
     position: fixed;
@@ -40,12 +46,13 @@
     .img{
         width: 32px;
         height: 32px;
-        &:first-child{
-            margin-right: 10px;
-        }
+    }
+    .listImg{
+        margin-left: 16px;
     }
     .title{
         width: 216px;
+        margin-left: 10px;
         &.isMusicList{
             :global{
                 .van-notice-bar .van-notice-bar__content::after{
@@ -212,7 +219,7 @@
     }
 
     &.pauseLeftButton {
-        left: 88px !important;
+        left: 108px !important;
         right: auto !important;
         bottom: 12px !important;
     }
@@ -288,4 +295,11 @@
             max-width: 220px;
         }
     }
+}
+
+.hiddenPop {
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+    opacity: 0;
 }

+ 81 - 52
src/page-instrument/header-top/index.tsx

@@ -80,7 +80,7 @@ export const headTopData = reactive({
       state.playIngSpeed = state.originSpeed;
       handleStartEvaluat();
       // 开发模式,把此处打开
-      //state.modeType = "evaluating";
+      // state.modeType = "evaluating";
       // evaluatingData.rendered = true;
       // evaluatingData.soundEffectMode = true;
     } else if (value === "follow") {
@@ -248,7 +248,7 @@ export default defineComponent({
     // 是否显示引导
     const showGuide = ref(false);
     const showStudentGuide = ref(false);
-
+    let  displayFingeringCache = false // 指法缓存
     /** 设置按钮 */
     const settingBtn = computed(() => {
       // 音频播放中 禁用
@@ -372,12 +372,8 @@ export default defineComponent({
     });
     /** 播放类型按钮 */
     const playTypeBtn = computed(() => {
-      // 选择模式,跟练模式 不显示
-      if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: false };
-      // 评测开始 禁用
-      if (state.modeType === "evaluating") return { display: false, disabled: true };
-      // 音频播放中 禁用
-      if (state.playState === "play") return { display: true, disabled: true };
+      // 选择模式,跟练模式,评测模式 不显示
+      if (headTopData.modeType !== "show" || state.modeType === "follow" || state.modeType === "evaluating") return { display: false, disabled: false };
       if (!state.isAppPlay) {
         let index = 0;
         state.music && index++;
@@ -388,16 +384,22 @@ export default defineComponent({
         state.mingSong && songIndex++;
         // 演唱和演奏 都有数据的时间不禁用
         if (songIndex > 0 && index > 0) {
+          // 音频播放中 禁用
+          if(state.playState === "play"){
+            return { display: true, disabled: true }
+          }
           return { display: true, disabled: false };
         }
       }
       return {
-        disabled: true,
-        display: true,
+        disabled: false,
+        display: false,
       };
     });
     /** 模式切换按钮 */
     const toggleBtn = computed(() => {
+      // 不是演奏模式 影藏
+      if(state.playType !== "play") return { display: false, disabled: false }
       // 选择模式, url设置模式 不显示
       if (headTopData.modeType !== "show" || !headTopData.showBack) return { display: false, disabled: false };
       // 跟练开始, 评测开始 播放开始 隐藏
@@ -589,7 +591,7 @@ export default defineComponent({
     return () => (
       <>
         <div
-          class={[styles.headerTop]}
+          class={[styles.headerTop, state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.headerTopRight : ""]}
           onClick={(e: Event) => {
             e.stopPropagation();
             if (state.platform === IPlatform.PC) {
@@ -604,53 +606,50 @@ export default defineComponent({
           }}
         >
           {/* 返回和标题 */}
-          {!(state.playState == "play" || followData.start || evaluatingData.startBegin) && (
-            <div class={styles.headTopLeftBox}>
-              <img src={iconBack} class={["headTopBackBtn", styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
-              {smoothAnimationState.isShow.value ? (
-                <div
-                  class={[styles.title, isMusicList.value && styles.isMusicList, "driver-8"]}
-                  onClick={() => {
-                    isMusicList.value && (musicListShow.value = true);
-                  }}
-                >
-                  <NoticeBar text={state.examSongName} background="none" />
-                </div>
-              ) : (
-                isMusicList.value && (
-                  <img
-                    src={listImg}
-                    class={[styles.img, "driver-8"]}
-                    onClick={() => {
-                      musicListShow.value = true;
-                    }}
-                  />
-                )
-              )}
-            </div>
-          )}
+          {
+            !(state.playState == "play" || followData.start || evaluatingData.startBegin) &&
+              <div class={styles.headTopLeftBox}>
+                <img src={iconBack} class={['headTopBackBtn', styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
+                {
+                  smoothAnimationState.isShow.value ?
+                    <div class={[styles.title,isMusicList.value && styles.isMusicList, "driver-8"]} onClick={()=>{
+                        isMusicList.value && (musicListShow.value = true)
+                      }}>
+                        <NoticeBar
+                          text={state.examSongName}
+                          background="none"
+                        />
+                    </div> :
+                    isMusicList.value &&
+                    <img src={listImg} class={[styles.img, styles.listImg, "driver-8"]} onClick={()=>{
+                      musicListShow.value = true
+                    }} />
+                }
+              </div>
+          }
           {/* 模式切换 */}
-          {state.playType === "play" && (
-            <div
+          {
+            <div 
               id={state.platform === IPlatform.PC ? "teacherTop-0" : "studnetT-0"}
               style={{ display: toggleBtn.value.display ? "" : "none" }}
-              class={["driver-9", styles.modeChangeBox, toggleBtn.value.disabled && styles.disabled]}
+              class={["driver-9", styles.modeChangeBox, toggleBtn.value.disabled && styles.disabled]} 
               onClick={() => {
-                handleRessetState();
-                headTopData.modeType = "init";
+                  handleRessetState();
+                  headTopData.modeType = "init";
               }}
             >
               <img class={styles.img} src={iconMode} />
-              <div class={styles.title}>{state.modeType === "practise" ? "练习模式" : state.modeType === "follow" ? "跟练模式" : state.modeType === "evaluating" ? "评测模式" : ""}</div>
+              <div class={styles.title}>{state.modeType==="practise" ? '练习模式' : state.modeType==="follow" ? "跟练模式" : state.modeType==="evaluating" ? "评测模式" : ""}</div>
             </div>
-          )}
+          }
           {/* 模式提醒 */}
-          {state.modeType === "practise" && (
-            <div class={[styles.modeWarn, "practiseModeWarn"]}>
-              <img src={state.playType === "play" ? headImg("perform1.png") : headImg("sing1.png")} />
-              <div>{state.playType === "play" ? "演奏场景" : "演唱场景"}</div>
-            </div>
-          )}
+          {
+            state.modeType === "practise" &&
+              <div class={[styles.modeWarn, "practiseModeWarn", state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.modeWarnRight : ""]}>
+                <img src={state.playType === "play" ? headImg("perform1.png") : headImg("sing1.png")} />
+                <div>{state.playType === "play" ? "演奏场景" : "演唱场景"}</div>
+              </div>
+          }
           {/* 功能按钮 */}
           <div
             class={[styles.headRight]}
@@ -698,7 +697,32 @@ export default defineComponent({
                   state.playType = "play";
                   state.playSource = state.music ? "music" : "background";
                 }
-                handlerModeChange(oldPlayType, oldPlaySource, true);
+                // 有指法并且显示指法的时候 切换到演唱模式 需要影藏指法
+                let isRefresh = false
+                if(state.isShowFingering && state.fingeringInfo.name && (state.setting.displayFingering || displayFingeringCache)){
+                  if(state.playType === "sing"){
+                    state.setting.displayFingering = false
+                    displayFingeringCache = true
+                  } else {
+                    state.setting.displayFingering = displayFingeringCache
+                    displayFingeringCache = false
+                  }
+                  // 如果是竖屏指法和一行谱的时候 改变指法值的时候state 会调用刷新 refreshMusicSvg 所以下面不调用
+                  if (state.fingeringInfo.direction === "vertical" && !state.isSingleLine) {
+                    isRefresh = true
+                  }
+                }
+                // 有歌词的时候,切换播放模式,需要重新渲染谱面  指法不刷新谱面的时候
+                if (state.xmlHasLyric && !isRefresh) {
+                  refreshMusicSvg();
+                } else if(!isRefresh) {
+                  handlerModeChange(oldPlayType, oldPlaySource, true);
+                }
+                showToast({
+                  message: state.playType === "play" ? "已切换为演奏场景" : "已切换为演唱场景",
+                  position: "top",
+                  className: "selectionToast",
+                });
               }}
             >
               <img style={{ display: state.playType === "play" ? "" : "none" }} class={styles.iconBtn} src={headImg(`perform.png`)} />
@@ -724,6 +748,11 @@ export default defineComponent({
                   }
                 }
                 handlerModeChange(oldPlayType, oldPlaySource);
+                showToast({
+                  message: state.playType === "play" ? (state.playSource === "music"?"已切换为原声":"已切换为伴奏") : (state.playSource === "music"?"已切换为范唱":(state.playSource === "background"?"已切换为伴唱":"已切换为唱名")),
+                  position: "top",
+                  className: "selectionToast",
+                });
               }}
             >
               <img style={{ display: state.playSource === "music" ? "" : "none" }} class={styles.iconBtn} src={state.playType === "play" ? headImg(`music.png`) : headImg(`music1.png`)} />
@@ -751,7 +780,7 @@ export default defineComponent({
                   <span style={{ whiteSpace: "nowrap" }}>节拍</span>
                   <div class={styles.speedCon}>
                     <img src={headImg("speed.png")} />
-                    <div>{state.speed}</div>
+                    <div>{Math.floor(state.speed)}</div>
                   </div>
                 </div>
                 {
@@ -812,7 +841,7 @@ export default defineComponent({
             playBtn.value.disabled && styles.disabled,
             state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.playLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === "right" ? styles.playRightButton : "",
           ]}
-          onClick={() => togglePlay()}
+          onClick={() => togglePlay(state.playState === "play" ? "paused" : "play")}
         >
           <div class={styles.btnWrap}>
             <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg("icon_play.png")} />

+ 80 - 53
src/page-instrument/header-top/modeView.tsx

@@ -13,6 +13,9 @@ import state from "/src/state";
 import { studentQueryUserInfo } from "../api";
 import { usePageVisibility } from "@vant/use";
 import { Vue3Lottie } from "vue3-lottie";
+import { popImgs, hanldeConfirmPop, hanldeClosePop, evaluatingData } from "/src/view/evaluating"
+import { Popup } from "vant";
+import AbnormalPop from "/src/view/abnormal-pop";
 
 export default defineComponent({
   name: "modeView",
@@ -54,56 +57,80 @@ export default defineComponent({
         state.isVip = false;
         openGuid();
       }
-    };
-    const pageVisible = usePageVisibility();
-    watch(
-      () => pageVisible.value,
-      (val) => {
-        if (val === "visible") {
-          if (storeData.user.vipMember) return;
-          console.log("页面显示");
-          getUserInfo();
-        }
-      }
-    );
-    watch(
-      () => headTopData.modeType,
-      (value, oldValue) => {
-        // headTopData.modeType 值 刚开始是 ""  所以 第一次切换时候不触发播放动画
-        if (!oldValue) return;
-        nextTick(() => {
-          if (value === "show") {
-            modeImgDom1.value?.pause();
-            modeImgDom2.value?.pause();
-            modeImgDom3.value?.pause();
-          } else if (value === "init") {
-            modeImgDom1.value?.play();
-            modeImgDom2.value?.play();
-            modeImgDom3.value?.play();
-          }
-        });
-      }
-    );
-    onMounted(() => {
-      openGuid();
-    });
-    return () => (
-      <div class={[styles.modeView, headTopData.modeType !== "init" && styles.hidden]}>
-        <img
-          src={backImg}
-          class={styles.back}
-          onClick={() => {
-            headTopData.modeType = "show";
-          }}
-        />
-        <img src={nameImg} class={styles.name} />
-        <div class={[styles.modeBox, ((!state.isPercussion && !state.enableEvaluation) || (state.isPercussion && state.enableEvaluation) || (state.isPercussion && !state.enableEvaluation)) && styles.twoModeBox]}>
-          <Vue3Lottie ref={modeImgDom1} class={styles.modeImg} animationData={lxMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("practise")}></Vue3Lottie>
-          {!state.isPercussion && <Vue3Lottie ref={modeImgDom2} class={styles.modeImg} animationData={glMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("follow")}></Vue3Lottie>}
-          {state.enableEvaluation && <Vue3Lottie ref={modeImgDom3} class={styles.modeImg} animationData={pcMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("evaluating")}></Vue3Lottie>}
-        </div>
-        {data.showVip && <TheVip />}
-      </div>
-    );
-  },
-});
+      const pageVisible = usePageVisibility()
+      watch(
+         () => pageVisible.value,
+         val => {
+            if (val === "visible") {
+               if (storeData.user.vipMember) return
+               console.log("页面显示")
+               getUserInfo()
+            }
+         }
+      )
+      watch(() => headTopData.modeType, (value,oldValue) => {
+         // headTopData.modeType 值 刚开始是 ""  所以 第一次切换时候不触发播放动画
+         if(!oldValue) return
+         nextTick(()=>{
+            if(value === "show"){
+               modeImgDom1.value?.pause()
+               modeImgDom2.value?.pause()
+               modeImgDom3.value?.pause()
+            }else if(value === "init"){
+               modeImgDom1.value?.play()
+               modeImgDom2.value?.play()
+               modeImgDom3.value?.play()
+            }
+         })
+      })
+      onMounted(() => {
+         openGuid()
+      })
+      watch(
+         () => evaluatingData.socketErrorStatus,
+         () => {
+           if (evaluatingData.socketErrorStatus === 2) {
+             setTimeout(() => {
+               evaluatingData.socketErrorPop = false;
+             }, 1000);
+           }
+         }
+       );      
+      return () => (
+         <div class={[styles.modeView, headTopData.modeType !== "init" && styles.hidden]}>
+            <img
+               src={backImg}
+               class={styles.back}
+               onClick={() => {
+                  headTopData.modeType = "show"
+               }}
+            />
+            <img src={nameImg} class={styles.name} />
+            <div
+               class={[
+                  styles.modeBox,
+                  ((!state.isPercussion && !state.enableEvaluation) ||
+                     (state.isPercussion && state.enableEvaluation) ||
+                     (state.isPercussion && !state.enableEvaluation)) &&
+                     styles.twoModeBox
+               ]}
+            >
+               <Vue3Lottie ref={modeImgDom1} class={styles.modeImg} animationData={lxMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("practise")}></Vue3Lottie>
+               {
+                  !state.isPercussion && <Vue3Lottie ref={modeImgDom2} class={styles.modeImg} animationData={glMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("follow")}></Vue3Lottie>
+               }
+               {
+                  state.enableEvaluation && <Vue3Lottie ref={modeImgDom3} class={styles.modeImg} animationData={pcMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("evaluating")}></Vue3Lottie>
+               }
+            </div>
+            {data.showVip && <TheVip />}
+            {/** 延迟检测中途,socket出错,网络提示弹窗 */}
+            <div>
+               <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatingData.socketErrorPop}>
+                  <AbnormalPop onConfirm={hanldeConfirmPop} onClose={hanldeClosePop} />
+               </Popup>
+            </div>            
+         </div>
+      )
+   }
+})

+ 27 - 17
src/page-instrument/header-top/settting/index.tsx

@@ -1,4 +1,4 @@
-import { defineComponent, reactive } from "vue";
+import { defineComponent, reactive, computed } from "vue";
 import styles from "./index.module.less"
 import { headImg } from "../image";
 import { headTopData } from "../index"
@@ -33,6 +33,12 @@ export default defineComponent({
 			state.setting.frequency = currentFrequency >= 0 ? currentFrequency : 0
 		}
         const formatterTimeMs = (value: any) => value = String(Math.min(3000, value));
+
+        const notationList = computed(() => {
+            const list = state.enableNotation ? [{name:'五线谱',value:'staff'},{name:'首调',value:'firstTone'},{name:'固定调',value:'fixedTone'}] : [{name:'首调',value:'firstTone'},{name:'固定调',value:'fixedTone'}];
+            return list;
+        });
+
 		return () => (
 			<div class={[styles.settting, styles[state.modeType]]}>
                 <div class={styles.head}>
@@ -42,7 +48,7 @@ export default defineComponent({
                 <div class={styles.content}>
                     <div class={styles.conBox}>
                         {
-                            state.isShowFingering && state.fingeringInfo.name && ["practise", "follow"].includes(state.modeType) &&
+                            state.isShowFingering && state.fingeringInfo.name && ["practise", "follow"].includes(state.modeType) && state.playType === "play" &&
                                 <div class={styles.cellBox}>
                                 <div class={styles.tit}>指法</div>
                                     <Switch v-model={state.setting.displayFingering}></Switch>
@@ -158,21 +164,25 @@ export default defineComponent({
                                 </div>
                             </div> : null                        
                         }
-                        <div class={styles.cellBox}>
-                            <div class={styles.tit}>转谱</div>
-                            <div class={styles.radioBox}>
-                                {
-                                    [{name:'五线谱',value:'staff'},{name:'首调',value:'firstTone'},{name:'固定谱',value:'fixedTone'}].map(item=>{
-                                        return <div class={ state.musicRenderType===item.value && styles.active } onClick={ ()=>{ 
-                                            state.musicRenderType = item.value as any
-                                            // resetRenderMusicScore(state.musicRenderType)
-                                            headTopData.settingMode = false
-                                            refreshMusicSvg();
-                                        } }>{item.name}</div>
-                                    })
-                                }
-                            </div>
-                        </div>
+                        {
+                            state.enableNotation || state.specialShowNotation ? 
+                            <div class={styles.cellBox}>
+                                <div class={styles.tit}>转谱</div>
+                                <div class={styles.radioBox}>
+                                    {
+                                        notationList.value.map(item=>{
+                                            return <div class={ state.musicRenderType===item.value && styles.active } onClick={ ()=>{ 
+                                                state.musicRenderType = item.value as any
+                                                // resetRenderMusicScore(state.musicRenderType)
+                                                headTopData.settingMode = false
+                                                refreshMusicSvg();
+                                            } }>{item.name}</div>
+                                        })
+                                    }
+                                </div>
+                            </div> : null
+                        }
+
                         <div class={styles.cellBtnBox}>
                             <img  src={headImg("tpbz.png")} onClick={() => (helperData.screenModelShow = true)} />
                             <img  src={headImg("yjfk.png")} onClick={() => (helperData.recommendationShow = true)} />

+ 9 - 5
src/page-instrument/header-top/speed/index.tsx

@@ -3,7 +3,7 @@ import { Switch, Slider } from "vant";
 import styles from "./index.module.less"
 import { headData } from "../index" 
 import { headImg } from "../image";
-import state, { handleSetSpeed } from "../../../state";
+import state, { handleSetSpeed, resetBaseRate } from "../../../state";
 import { metronomeData } from "../../../helpers/metronome"; 
 
 export default defineComponent({
@@ -22,11 +22,15 @@ export default defineComponent({
 			canSpeed = Math.max(canSpeed, 45);
 			speed.value = canSpeed;
 		};
+		// 重置当前小节的速度
+		const resetCurrentSpeed = () => {
+			resetBaseRate(state.activeNoteIndex);
+		};
 		watch(
 			() => speed.value,
 			() => {
 				// handleSetSpeed(speed.value);
-				state.speed = speed.value;
+				state.speed = Math.floor(speed.value);
 			}
 		);
 		watch(
@@ -57,11 +61,11 @@ export default defineComponent({
 						<div class={styles.spendCon}>
 							<img src={headImg("cutImg.png")} class={[styles.btn]} onClick={minusSpeed} />
 							<div class={styles.sliderCon}>
-								<Slider class={styles.slider} max={270} min={45} v-model={speed.value}>
+								<Slider class={styles.slider} max={270} min={speed.value < 45 ? speed.value : 45} v-model={speed.value}>
 									{{
 										button: () => 
 										<div class={styles.customButton}>
-											<div class={styles.speedVal}>{ speed.value }</div>
+											<div class={styles.speedVal}>{ Math.floor(speed.value) }</div>
 											<div class={styles.speedBtn}></div>
 										</div>
 									}}
@@ -70,7 +74,7 @@ export default defineComponent({
 							<img src={headImg("addImg.png")} class={[styles.btn]} onClick={plusSpeed} />	
 						</div>
 						<div class={styles.speedSel}>
-							<div onClick={()=>{ speed.value = state.originSpeed }}>原速</div>
+							<div onClick={resetCurrentSpeed}>原速</div>
 							<div onClick={()=>{ speed.value = 70 }}>70</div>
 							<div onClick={()=>{ speed.value = 80 }}>80</div>
 							<div onClick={()=>{ speed.value = 90 }}>90</div>

+ 7 - 1
src/page-instrument/simple-detail/index.module.less

@@ -17,7 +17,13 @@
     --header-height: 62px;
     // background: var(--container-background);
     background: transparent;
-
+    position: relative;
+    .mask{
+        position: absolute;
+        z-index: 6;
+        width: 100%;
+        height: 100%;
+    }
     .container {
         margin: 0;
         border-radius: 10px;

+ 11 - 6
src/page-instrument/simple-detail/index.tsx

@@ -1,5 +1,5 @@
-import { defineComponent, onMounted, onUnmounted, reactive } from "vue";
-import state, { getMusicDetail, handleSetSpeed, addNoteBBox, getNote, gotoNext } from "/src/state";
+import { defineComponent, onMounted, onUnmounted, reactive, nextTick } from "vue";
+import state, { getMusicDetail, handleSetSpeed, addNoteBBox, getNote, gotoNext, fillWordColor } from "/src/state";
 import MusicScore from "../../view/music-score";
 import styles from "./index.module.less";
 import { getQuery } from "/src/utils/queryString";
@@ -45,7 +45,7 @@ export default defineComponent({
 				console.log('拖动的进度')
 				if (state.playState === 'paused') {
 					detailData.currentTime = resInfo?.content?.currentTime ? resInfo?.content?.currentTime : detailData.currentTime;
-					handlePlaying();
+					handlePlaying(true);
 				}
 			}
 			// 播放进度
@@ -101,7 +101,11 @@ export default defineComponent({
 			setCustomGradual();
 			setCustomNoteRealValue();
 			state.times = formateTimes(osmd);
-			console.log("🚀 ~ state.times:", state.times, state.subjectId, state);
+			console.log("🚀 ~ state.times:", state.times, state);
+			nextTick(() => {
+				state.activeMeasureIndex = state.times[0].MeasureNumberXML;
+				fillWordColor();
+			})
 			// 音符添加位置信息bbox
 			addNoteBBox(state.times);
 			// 一行谱创建 动画
@@ -126,13 +130,13 @@ export default defineComponent({
 		/**
 		 * 播放一直触发的事件
 		 */
-		const handlePlaying = () => {
+		const handlePlaying = (skipNote?: boolean) => {
 			//detailData.currentTime += 0.03
 			const currentTime = detailData.currentTime;
 			// console.log('👀~播放进度',currentTime)
 			let item = getNote(currentTime);
 			if (item) {
-				gotoNext(item);
+				gotoNext(item, skipNote);
 			}
 			state.activeNoteIndex = item?.i || 0
 			// 一行谱,需要滚动小节
@@ -166,6 +170,7 @@ export default defineComponent({
 
 		return () => (
 			<div class={styles.detail}>
+				<div class={styles.mask}></div>
 				<div id="scrollContainer" class={[styles.container, "hideCursor"]}>
 					{/* 曲谱渲染 */}
 					{!detailData.isLoading && 

+ 0 - 0
src/view/audio-list/img/refresh_anim.gif → src/page-instrument/view-detail/images/refresh_anim.gif


+ 0 - 0
src/view/audio-list/img/refresh_anim.json → src/page-instrument/view-detail/images/refresh_anim.json


+ 23 - 0
src/page-instrument/view-detail/index.module.less

@@ -191,4 +191,27 @@
     100% {
         transform: translateY(0%);
     }
+}
+
+.loadingPop {
+    position: fixed;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    width: 100vw;
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    z-index: 10000;
+    background: rgba(0, 0, 0, .6);
+    .lottie{
+        width: 120px;
+    }
+    .loadingTip {
+        font-size: 14px;
+        color: #fff;
+    }
 }

+ 17 - 25
src/page-instrument/view-detail/index.tsx

@@ -37,6 +37,7 @@ import AuthorName from "../component/authorName"
 import { initSmoothAnimation } from "./smoothAnimation"
 import EmptyMusic, { isEmptyMusicShow } from "./emptyMusic"
 import { position } from "html2canvas/dist/types/css/property-descriptors/position";
+import Loading from "./loading"
 
 // const DelayCheck = defineAsyncComponent(() =>
 //   import('/src/page-instrument/evaluat-model/delay-check')
@@ -142,7 +143,8 @@ export default defineComponent({
       if (state.isPreView) {
         state.zoom = 0.65
       }
-      state.isSingleLine = query.isSingleLine === "true" ? true : false; // 一行谱模式
+      // 只有总控平台和预览 默认是多行谱
+      (state.isPreView || query.isCbs) && (state.isSingleLine = false)
       // Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
       //   getMusicInfo(values[0]);
       // });
@@ -150,6 +152,7 @@ export default defineComponent({
         await getMusicDetail(id);
       } catch (err) {
         console.error(err)
+        state.isLoading = false;
         isEmptyMusicShow.value = true
         return
       }
@@ -181,7 +184,11 @@ export default defineComponent({
       state.times = formateTimes(osmd);
       // state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);
+      state.xmlHasLyric = state.times.some((item: any) => item?.formatLyricsEntries?.length)
       console.log("🚀 ~ state.times:", state.times, state.subjectId, state);
+      nextTick(() => {
+        state.activeMeasureIndex = state.times[0].MeasureNumberXML;
+      })
       // 一行谱
       if (state.isSingleLine) {
         // 音符添加位置信息bbox
@@ -245,24 +252,8 @@ export default defineComponent({
         );
         throw new Error("webApi_beatTimes 完成");
       }
-      // 根据当前文件有没有 设置当前的播放模式
-      if(!state.music){
-        if(state.accompany){
-          state.playSource = "background"
-        }else{
-          if(state.fanSong){
-            state.playType = "sing"
-            state.playSource = "music"
-          }else if(state.banSong){
-            state.playType = "sing"
-            state.playSource = "background"
-          }else if(state.mingSong){
-            state.playType = "sing"
-            state.playSource = "mingSong"
-          }
-          handlerModeChange("play", "music")
-        }
-      }
+      // 刷新时值
+      handlerModeChange("play", "music")
       /**
        * 2024.1.25
        * 设置节拍器,跟练需要播放系统节拍器,所以不需要判断needTick状态
@@ -322,7 +313,7 @@ export default defineComponent({
               fingerBox: {
                 position: "absolute",
                 width: state.fingeringInfo.width,
-                height: "100%",
+                height: "80%",
                 right: state.playBtnDirection === "right" ? "initial" : 0,
                 left: state.playBtnDirection === "right" ? 0 : "initial",
                 top: 0,
@@ -331,13 +322,13 @@ export default defineComponent({
           } else {
             return {
               container: {
-                paddingRight: state.fingeringInfo.width,
+                paddingLeft: state.fingeringInfo.width,
               },
               fingerBox: {
                 position: "absolute",
                 width: state.fingeringInfo.width,
-                height: "100%",
-                right: 0,
+                height: "80%",
+                left: 0,
                 top: 0,
               },
             };
@@ -462,13 +453,13 @@ export default defineComponent({
         }}
       >
         {/* 骨架屏 */}
-        <Transition name="van-fade">
+        {/* <Transition name="van-fade">
           {detailData.skeletonLoading && (
             <div class={styles.skeleton}>
               <Skeleton row={8} />
             </div>
           )}
-        </Transition>
+        </Transition> */}
         {/* 曲目加载错误的缺省 */}
         <EmptyMusic></EmptyMusic>
         {/** 功能按钮 */}
@@ -563,6 +554,7 @@ export default defineComponent({
             {isMusicList.value && <TheMusicList />}
           </>
         )}
+        <Loading tipText={state.loadingText} />
         <Popup
           zIndex={5050}
           teleport="body"

+ 4 - 5
src/view/audio-list/loading.tsx → src/page-instrument/view-detail/loading.tsx

@@ -1,11 +1,10 @@
 import { defineComponent, ref, watch } from "vue"
-import icon_loading_img from "./img/icon_loading_img.png"
 import { Progress } from "vant"
 import styles from "./index.module.less"
 import state from "/src/state"
 import { Vue3Lottie } from "vue3-lottie";
-import animBg from "./img/refresh_anim.json";
-import animGif from "./img/refresh_anim.gif";
+import animBg from "./images/refresh_anim.json";
+import animGif from "./images/refresh_anim.gif";
 
 export default defineComponent({
    name: "loading",
@@ -18,8 +17,8 @@ export default defineComponent({
 	},
    setup(props) {
       return () =>
-         !state.audioDone && (
-            <div class={styles.loadingPop}>
+         (
+            <div class={styles.loadingPop} style={{display:state.isLoading? "flex" : "none"}}>
                <img class={styles.lottie} src={animGif} />
                {/* <Vue3Lottie class={styles.lottie} animationData={animBg}></Vue3Lottie> */}
                <div class={styles.loadingTip}>{props.tipText}</div>

+ 16 - 30
src/page-instrument/view-detail/smoothAnimation/index.ts

@@ -74,10 +74,11 @@ export function initSmoothAnimation() {
       }
       return path
    }, 0)
-   smoothAnimationState.aveSpeed = (canvasDomPath / (state.times[state.times.length - 1].time - state.times[0].time) / 1000) * 16.67
+   // 20 是屏幕多长时间刷新一次的时间,本来是16.67的,但是有些手机帧率比较低,所以这里给小一点的值,宁愿慢一点偏屏幕左边一点
+   smoothAnimationState.aveSpeed = (canvasDomPath / (state.times[state.times.length - 1].time - state.times[0].time) / 1000) * 20
    // 当前屏幕的宽度
    calcClientWidth()
-   document.addEventListener("resize", calcClientWidth)
+   window.addEventListener("resize", calcClientWidth)
    // 初始化 只有练习模式 才显示
    state.modeType === "practise" && (smoothAnimationState.isShow.value = true)
    console.log(smoothAnimationState, "一行谱小鸟数据")
@@ -88,7 +89,7 @@ export function initSmoothAnimation() {
  */
 export function destroySmoothAnimation() {
    smoothAnimationState.isShow.value = false
-   document.removeEventListener("resize", calcClientWidth)
+   window.removeEventListener("resize", calcClientWidth)
    smoothAnimationState.smoothAnimationBoxDom?.remove()
    Object.assign(smoothAnimationState, {
       canvasDom: null,
@@ -104,11 +105,6 @@ export function destroySmoothAnimation() {
       aveSpeed: 0,
       clientWidth: 0
    })
-   Object.assign(moveState, {
-      oldIndex: -1,
-      progress: 0,
-      activeIndex: 0
-   })
 }
 
 /**
@@ -134,7 +130,9 @@ export function moveSmoothAnimationByPlayTime(time?: number) {
       (nextIndex > state.times.length - 1 ? state.times[state.activeNoteIndex]?.endtime : state.times[nextIndex].time) -
       state.times[state.activeNoteIndex]?.time
    // 当前时值在该区间的占比
-   const playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration
+   let playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration
+   // 华为手机 fixtime 有个默认0.08的值,所以进度可能为负数 ,这里兼容一下
+   playProgress < 0 && (playProgress = 0)
    moveSmoothAnimation(playProgress, state.activeNoteIndex)
 }
 
@@ -143,14 +141,7 @@ export function moveSmoothAnimationByPlayTime(time?: number) {
  * progress  当前音符到下一个音符的距离百分比
  * activeIndex 当前
  */
-const moveState = {
-   oldIndex: -1, // 上一次index
-   progress: 0,
-   activeIndex: 0
-}
-export function moveSmoothAnimation(progress: number, activeIndex: number) {
-   moveState.progress = progress
-   moveState.activeIndex = activeIndex
+export function moveSmoothAnimation(progress: number, activeIndex: number, isMoveOsmd = true) {
    // if (!smoothAnimationState.isShow.value) {
    //    return
    // }
@@ -160,11 +151,6 @@ export function moveSmoothAnimation(progress: number, activeIndex: number) {
    const progressCalcIndex = Math.round(progress * _numberOfSegments)
    // // 当前的index
    let nowIndex = nextPointsIndex - _numberOfSegments + progressCalcIndex
-   // 当前计算的位置和上一次值一样时候不运行
-   if (moveState.oldIndex === nowIndex) {
-      return
-   }
-   moveState.oldIndex = nowIndex
    const nowPointsPos = smoothAnimationState.pointsPos[nowIndex]
    smoothAnimationState.canvasCtx?.clearRect(0, 0, smoothAnimationState.canvasDomWith, smoothAnimationState.canvasDomHeight)
    // 移动
@@ -176,14 +162,14 @@ export function moveSmoothAnimation(progress: number, activeIndex: number) {
       smoothAnimationState.pointsPos,
       smoothAnimationState.pointsPos.slice(0, nowIndex)
    )
-   // 当移动到屏幕最右边时候 就不进行移动了
-   if (
-      (smoothAnimationState.osdmScrollDom?.scrollLeft || 0) + smoothAnimationState.translateXNum + smoothAnimationState.osdmScrollDomWith >=
-      smoothAnimationState.canvasDomWith
-   ) {
-      return
-   }
-   move_osmd(nowPointsPos)
+   // 当移动到屏幕最右边时候 就不进行移动了    存在移动到屏幕最右边时候  有反复的情况需要屏幕移动。所以这里注释掉了
+   // if (
+   //    (smoothAnimationState.osdmScrollDom?.scrollLeft || 0) + smoothAnimationState.translateXNum + smoothAnimationState.osdmScrollDomWith >=
+   //    smoothAnimationState.canvasDomWith
+   // ) {
+   //    return
+   // }
+   isMoveOsmd && move_osmd(nowPointsPos)
 }
 
 /**

+ 133 - 28
src/state.ts

@@ -19,6 +19,7 @@ import { moveSmoothAnimation, smoothAnimationState, moveSmoothAnimationByPlayTim
 import { storeData } from "/src/store";
 import { downloadXmlStr } from "./view/music-score"
 import { musicScoreRef } from "/src/page-instrument/view-detail/index"
+import { headTopData } from "/src/page-instrument/header-top/index";
 
 const query: any = getQuery();
 
@@ -28,7 +29,7 @@ export type IDifficulty = "BEGINNER" | "ADVANCED" | "PERFORMER";
 export enum EnumMusicRenderType {
   /** 五线谱 */
   staff = "staff",
-  /** 简谱 */
+  /** 简谱(首调) */
   firstTone = "firstTone",
   /** 固定音高 */
   fixedTone = "fixedTone",
@@ -228,21 +229,41 @@ export const musicalInstrumentCodeInfo = [
     id: 34
   },
   {
+    name: '陶笛',
+    code: 'Alto Ocarina',
+    id: 34
+  },
+  {
     name: '葫芦丝',
     code: 'Woodwind',
     id: 35
   },
   {
+    name: '葫芦丝',
+    code: 'Hulusi',
+    id: 35
+  },
+  {
     name: '口风琴',
     code: 'Nai',
     id: 36
   },
   {
+    name: '口风琴',
+    code: 'Melodica',
+    id: 36
+  },
+  {
     name: '德式竖笛',
     code: 'Tenor Recorder',
     id: 37
   },
   {
+    name: '德式竖笛',
+    code: 'German Recorder',
+    id: 37
+  },
+  {
     name: '英式竖笛',
     code: 'Baroque Recorder',
     id: 38
@@ -252,6 +273,11 @@ export const musicalInstrumentCodeInfo = [
     code: 'Whistling',
     id: 39
   },
+  {
+    name: '高音陶笛',
+    code: 'Soprano Ocarina',
+    id: 39
+  },
 ]
 
 const state = reactive({
@@ -277,6 +303,8 @@ const state = reactive({
   enableEvaluation: true,
   /** 是否支持转谱 */
   enableNotation: false,
+  /** 后台设置不能转谱,但是默认谱面不是五线谱时,需要显示转谱按钮,此时只能转首调和固定调 */
+  specialShowNotation: false,
   /** 曲谱ID */
   examSongId: "",
   /** 内容平台的曲谱ID,可能会和业务端的id不一样 */
@@ -475,7 +503,7 @@ const state = reactive({
   /** 音频文件是否加载完成 */
   audioDone: false,
   /** 是否为单行谱渲染模式 */
-  isSingleLine: false,
+  isSingleLine: true,
   /** 是否是evxml */
   isEvxml: false,
   noTimes: [] as any,
@@ -501,6 +529,8 @@ const state = reactive({
   musicComposer: '',
   /** 作词家 */
   musicLyricist: '',
+  // 加载条
+  isLoading: true,
   /** 加载中的文案 */
   loadingText: '音频资源加载中,请稍后…',
   /** 是否是简单的单行谱模式页面 */
@@ -511,6 +541,12 @@ const state = reactive({
   basePlayRate: 1,
   /** 引导页显示状态 */
   hasDriverPop: false,
+  /** 播放倍率不等于1,或者是选段评测,APP暂时不支持保存演奏,需要给出提示 */
+  noSavePopShow: true,
+  /** xml里面是否有歌词 */
+  xmlHasLyric: false,
+  /** 生成图片的模式 */
+  isCreateImg: false,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -607,8 +643,9 @@ export const initSetPlayRate = () => {
 }
 
 // 重置播放倍率
-export const resetBaseRate = () => {
-  const currentItem: any = state.times[0];
+export const resetBaseRate = (idx?: number) => {
+  const index = idx ? idx : 0;
+  const currentItem: any = state.times[index];
   const currentSpeed = currentItem?.measureSpeed ? currentItem.measureSpeed : state.originSpeed;
   // console.log('速度2',currentSpeed)
   state.speed = currentSpeed
@@ -724,16 +761,16 @@ export const skipNotePlay = async (itemIndex: number, isStart = false) => {
 
 /**
  * 切换曲谱播放状态
- * @param playState 可选: 默认 undefined, 需要切换的状态 play:播放, paused: 暂停
+ * @param playState 需要切换的状态 play:播放, paused: 暂停
  */
-export const togglePlay = async (playState?: "play" | "paused", sourceType?: string) => {
+export const togglePlay = async (playState: "play" | "paused", sourceType?: string) => {
   // 如果mp3资源还在加载中,给出提示
   if (!state.isAppPlay && !state.audioDone) {
     if (sourceType !== 'courseware') showToast('音频资源加载中,请稍后')
     return
   }
   // 播放之前  当为评测模式和不为MIDI时候按  是否禁用节拍器  切换音源
-  if ((playState ? playState : state.playState === "paused" ? "play" : "paused") === 'play' && state.modeType === "practise" && state.playMode !== "MIDI") {
+  if (playState === 'play' && state.modeType === "practise" && state.playMode !== "MIDI") {
     console.log("设置音源")
     changeSongSourceByBate(metronomeData.disable)
   }
@@ -744,6 +781,8 @@ export const togglePlay = async (playState?: "play" | "paused", sourceType?: str
         songID: state.examSongId,
       })
       state.playState = 'paused'
+      // 当在节拍器播放期间暂停的话 就暂停节拍器
+      closeTick()
       return
     }
     skipNotePlay(state.activeNoteIndex, false);
@@ -756,7 +795,7 @@ export const togglePlay = async (playState?: "play" | "paused", sourceType?: str
     const status = cloudGetMediaStatus?.content.status === "suspend" ? "play" : "paused"
     state.playState = status
   } else {
-    state.playState = playState ? playState : state.playState === "paused" ? "play" : "paused";
+    state.playState = playState;
   }
   if (state.playState === "play" && state.sectionStatus && state.section.length == 2 && state.playProgress === 0) {
     resetPlaybackToStart();
@@ -1222,6 +1261,7 @@ export const isRhythmicExercises = () => {
 /** 重置状态 */
 export const handleRessetState = () => {
   // 切换模式,清除选段
+  state.noSavePopShow = true;
   clearSelection();
   skipNotePlay(0, true);
   resetBaseRate();
@@ -1322,7 +1362,13 @@ function initMusicSource(data: any, track?: string) {
   })
   // 当没有任何曲目的时候报错
   if (!musicObj?.audioFileUrl && !accompanyObj?.audioFileUrl && !fanSongObj?.audioFileUrl && !banSongObj?.audioFileUrl && !fanSongObj?.solmizationFileUrl) {
-    throw new Error("该曲目无任何音源");
+    // 并且是midi没有midi文件的时候
+    if(data.playMode === "MIDI" && !data.midiFileUrl) {
+      // 是预览的时候 不报错
+      if(!query.isPreView){
+        throw new Error("该曲目无任何音源");
+      }
+    }
   }
   Object.assign(state, {
     music: musicObj?.audioFileUrl,
@@ -1341,6 +1387,23 @@ function initMusicSource(data: any, track?: string) {
   return musicObj
 }
 const setState = (data: any, index: number) => {
+  // 根据当前文件有没有 设置当前的播放模式
+  if(!state.music){
+    if(state.accompany){
+      state.playSource = "background"
+    }else{
+      if(state.fanSong){
+        state.playType = "sing"
+        state.playSource = "music"
+      }else if(state.banSong){
+        state.playType = "sing"
+        state.playSource = "background"
+      }else if(state.mingSong){
+        state.playType = "sing"
+        state.playSource = "mingSong"
+      }
+    }
+  }
   state.appName = "COLEXIU";
   state.detailId = data.bizId;
   state.xmlUrl = data.xmlFileUrl;
@@ -1351,7 +1414,11 @@ const setState = (data: any, index: number) => {
   // 声部code
   const subjectCode = data.subjectCodes ? data.subjectCodes.split(',')?.[0] : '';
   // 乐器code
-  let musicalCode = data.musicalInstrumentIdCodes ? data.musicalInstrumentIdCodes.split(',')?.[0] : '';
+  // let musicalCode = data.musicalInstrumentIdCodes ? data.musicalInstrumentIdCodes.split(',')?.[0] : '';
+  /**
+   * 单曲,指法根据用户当前的乐器来显示,如果没有则取musicSheetSoundList第一个track
+   */
+  let musicalCode = !storeData.user?.instrumentId ? data.musicSheetSoundList.find((item:any)=>{ return item.audioPlayType === "PLAY" })?.track || '' : data.musicSheetSoundList?.find((item: any) => item?.musicalInstrumentId == storeData.user?.instrumentId && item.audioPlayType === "PLAY")?.track || '';
   const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase())
   const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase())
   state.subjectCodeId = pitchSubject ? pitchSubject.id : 0
@@ -1468,10 +1535,17 @@ const setState = (data: any, index: number) => {
     pitchTrack = data.musicalInstruments?.find((item: any) => item.code === musicalCode)
   }
   let musicalRenderType = ''
-  if (pitchTrack?.defaultScore) {
-    musicalRenderType = pitchTrack?.defaultScore === 'STAVE' ? 'staff' : pitchTrack?.defaultScore === 'JIAN' ? 'fixedTone' : pitchTrack?.defaultScore === 'FIRST' ? 'firstTone' : ''
-  }
-  state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone;
+  // if (pitchTrack?.defaultScore) {
+  //   musicalRenderType = pitchTrack?.defaultScore === 'STAVE' ? 'staff' : pitchTrack?.defaultScore === 'JIAN' ? 'fixedTone' : pitchTrack?.defaultScore === 'FIRST' ? 'firstTone' : ''
+  // }
+  // state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone;
+  /**
+   * 2024.7.30,使用新的字段
+   * 谱面类型,scoreType
+   * STAVE("五线谱"),JIAN("固定调"),FIRST("首调"),
+   */
+  musicalRenderType = data.scoreType === 'STAVE' ? 'staff' : data.scoreType === 'JIAN' ? 'fixedTone' : data.scoreType === 'FIRST' ? '' : 'firstTone';
+  state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone;  
   /**
    * TODO:摇篮曲特殊处理
    */
@@ -1480,7 +1554,15 @@ const setState = (data: any, index: number) => {
       state.musicRenderType = EnumMusicRenderType.firstTone;
     }
   }
-  state.enableNotation = pitchTrack ? data.isConvertibleScore && pitchTrack.transferFlag : data.isConvertibleScore
+  /**
+   * 2024.7.30,使用新的字段
+   * 能否转谱,isConvertibleScore
+   * true:能转谱,false:不能转谱
+   * 额外的逻辑:后台设置不能转谱时,如果默认谱面不是五线谱,需要显示转谱按钮,但是只能转首调和固定调
+   */  
+  // state.enableNotation = pitchTrack ? data.isConvertibleScore && pitchTrack.transferFlag : data.isConvertibleScore
+  state.enableNotation = data.isConvertibleScore
+  state.specialShowNotation = !data.isConvertibleScore && data.scoreType !== 'STAVE';
   console.log("state对象", state);
   // 评测基准频率
   state.baseFrequency = data.evaluationFrequency ? data.evaluationFrequency.split(",")[0] : 440
@@ -1587,21 +1669,23 @@ export const addNoteBBox = (list: any[]) => {
     //  todo  连续修止小节bug
     note.bbox = bbox;
   }
-
 }
 
 // 给歌词和音符添加动态颜色
-const fillWordColor = () => {
+export const fillWordColor = () => {
   // console.log('当前音符',state.activeNoteIndex)
   state.times.forEach((item: any, idx: number) => {
     const svgEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}`)
     const stemEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-stem`)
+    const stemLine = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-lines`)
     if ((item.i === state.activeNoteIndex || item.id === state.times[state.activeNoteIndex].id) && item.svgElement) {
       svgEl?.classList.add('noteActive')
       stemEl?.classList.add('noteActive')
+      stemLine?.classList.add('noteActive')
     } else {
       svgEl?.classList.remove('noteActive')
       stemEl?.classList.remove('noteActive')
+      stemLine?.classList.remove('noteActive')
     }
   })
 
@@ -1626,10 +1710,15 @@ export const moveSvgDom = (skipNote?: boolean) => {
    * 当前选中的音符和第一个音符之间的间距
    */
   if (skipNote) {
-    const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 2 - state.times[0].bbox?.width / 2;
     // 点击 清空translateXNum
     smoothAnimationState.translateXNum = 0
-    moveSmoothAnimation(0, state.activeNoteIndex)
+    moveTranslateXNum(0)
+    // 移动小鸟的位置
+    moveSmoothAnimation(0, state.activeNoteIndex, false)
+    // 移动谱面当当前音符的位置
+    const noteWidth = state.times[state.activeNoteIndex].bbox?.originWidth || state.times[state.activeNoteIndex].bbox?.width;
+    const firstNoteWidth = state.times[0].bbox?.originWidth || state.times[0].bbox?.width;
+    const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + noteWidth / 2 - firstNoteWidth / 2;
     smoothAnimationState.osdmScrollDom!.scrollTo({
       left: distance,
       behavior: "smooth",
@@ -1646,9 +1735,12 @@ watch(
       // 当在播放中暂停 执行这个方法
       if (!state.playEnd && state.playState === "paused") {
         moveTranslateXNum(0)
-        const scrollLeft = smoothAnimationState.osdmScrollDom!.scrollLeft
-        smoothAnimationState.osdmScrollDom!.scrollLeft = scrollLeft + smoothAnimationState.translateXNum
-        smoothAnimationState.translateXNum = 0
+        // 因为safari浏览器scrollWidth的值一直变化,scrollLeft + smoothAnimationState.translateXNum 为最大宽度的时候,实际上scrollLeft滚不到最大宽度,所以在下一帧处理滚动,能滚动到最大滚动位置
+        requestAnimationFrame(() => {
+          const scrollLeft = smoothAnimationState.osdmScrollDom!.scrollLeft
+          smoothAnimationState.osdmScrollDom!.scrollLeft = scrollLeft + smoothAnimationState.translateXNum
+          smoothAnimationState.translateXNum = 0
+        });
       }
     }
   }
@@ -1683,6 +1775,11 @@ watch(
       if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex)) ) {
         item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
         item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")
+        // 预备小节
+        if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML){
+          item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
+          item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+        }
       } else {
         // 有选段只清除选段处的
         if (state.section.length === 2) {
@@ -1762,7 +1859,7 @@ watch(
 /** 刷新谱面 */
 export const refreshMusicSvg = () => {
   resetBaseRate();
-  state.loadingText = '正在加载中,请稍等…'
+  state.activeMeasureIndex = 0;
   // 销毁旋律线
   destroySmoothAnimation()
   musicScoreRef.value?.refreshMusicScore()
@@ -1772,10 +1869,18 @@ export const refreshMusicSvg = () => {
 watch(
   () => state.setting.displayFingering,
   () => {
-    nextTick(() => {
-      if (smoothAnimationState.osdmScrollDom) {
-        smoothAnimationState.osdmScrollDomWith = smoothAnimationState.osdmScrollDom.offsetWidth | 0
-      }
-    })
+    // 有字符 并且是竖向指法 并且是一行谱
+    if(state.fingeringInfo?.name && state.fingeringInfo.direction === "vertical" && state.isSingleLine){
+      nextTick(() => {
+        if (smoothAnimationState.osdmScrollDom) {
+          smoothAnimationState.osdmScrollDomWith = smoothAnimationState.osdmScrollDom.offsetWidth | 0
+        }
+      })
+    }
+    // 如果有指法,并且是竖向指法时,切换指法时,谱面宽度变化,需要重新渲染谱面
+    if (state.fingeringInfo?.name && state.fingeringInfo.direction === "vertical" && !state.isSingleLine) {
+      headTopData.settingMode = false;
+      refreshMusicSvg();
+    }
   }
 )

+ 5 - 1
src/style.css

@@ -7,7 +7,11 @@ img {
   -webkit-touch-callout: none;
 }
 body {
-  user-select: none;
+  -webkit-user-select: none; /* Safari */
+  -moz-user-select: none; /* Firefox */
+  -ms-user-select: none; /* Internet Explorer/Edge */
+  user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera,禁用文本选择 */
+  -webkit-touch-callout: none; /* iOS Safari,禁用长按行为 */
 }
 
 :root {

+ 1 - 1
src/view/abnormal-pop/index.tsx

@@ -5,7 +5,7 @@ import { popImgs } from "/src/view/evaluating";
 import { evaluatingData } from "/src/view/evaluating";
 import { Vue3Lottie } from "vue3-lottie";
 import loading from "./loading.json";
-import animBg from "../audio-list/img/refresh_anim.json";
+import animBg from "/src/page-instrument/view-detail/images/refresh_anim.gif";
 
 export default defineComponent({
   name: "abnormal-pop",

+ 0 - 23
src/view/audio-list/index.module.less

@@ -65,27 +65,4 @@
             background-size: 100% 100%;
         }
     }
-}
-
-.loadingPop {
-    position: fixed;
-    left: 0;
-    top: 0;
-    right: 0;
-    bottom: 0;
-    width: 100vw;
-    height: 100vh;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-    z-index: 10000;
-    background: rgba(0, 0, 0, .6);
-    .lottie{
-        width: 120px;
-    }
-    .loadingTip {
-        font-size: 14px;
-        color: #fff;
-    }
 }

+ 11 - 3
src/view/audio-list/index.tsx

@@ -12,7 +12,6 @@ import state, { IPlayState, onEnded, onPlay } from "/src/state";
 import { api_playProgress, api_cloudTimeUpdae, api_cloudplayed, api_remove_cloudplayed, api_remove_cloudTimeUpdae } from "/src/helpers/communication";
 import { evaluatingData } from "/src/view/evaluating";
 import { cloudToggleState } from "/src/helpers/midiPlay"
-import Loading from "./loading"
 
 export const audioData = reactive({
 	songEle: null as HTMLAudioElement | null, // 原生
@@ -207,7 +206,10 @@ export default defineComponent({
 			}
 		);
 
-		const createAudio = (src: string): Promise<HTMLAudioElement | null> => {
+		const createAudio = (src?: string): Promise<HTMLAudioElement | null> => {
+			if(!src){
+				return Promise.resolve(null)
+			}
 			return new Promise((resolve) => {
 				const a = new Audio(src + '?v=' + Date.now());
 				a.load();
@@ -279,6 +281,11 @@ export default defineComponent({
 			return Promise.all([createAudio(state.beatSong.music), createAudio(state.beatSong.accompany), createAudio(state.beatSong.fanSong), createAudio(state.beatSong.banSong), createAudio(state.beatSong.mingSong)])
 		}
 		onMounted(async () => {
+			// 预览的时候不走音频加载逻辑
+			if(state.isPreView){
+				state.isLoading = false;
+				return
+			}			
 			if (state.playMode !== "MIDI") {
 				console.time("音频加载时间")
 				// 处理音源
@@ -344,11 +351,13 @@ export default defineComponent({
 					beatMingSong.addEventListener("ended", onEnded);
 				}
 				state.audioDone = true;
+				state.isLoading = false
 				console.timeEnd("音频加载时间")
 				console.log("音频数据:",audioData)
 				api_playProgress(progress);
 			} else {
 				state.audioDone = true;
+				state.isLoading = false
 				const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
 				audioData.duration = songEndTime
 				// 监听midi播放进度
@@ -365,7 +374,6 @@ export default defineComponent({
 		// console.log(state.playMode, state.midiUrl);
 		return () => (
 			<>
-				<Loading tipText={state.loadingText} />
 				<div class={styles.audioList}>
 					{state.playMode === "MIDI" && state.speed != 0 && (
 						<iframe

+ 19 - 7
src/view/evaluating/index.tsx

@@ -35,7 +35,7 @@ import {
   api_startDelayCheck,
   api_closeDelayCheck,
 } from "/src/helpers/communication";
-import state, { IPlayState, clearSelection, handleStopPlay, onPlay, resetPlaybackToStart, togglePlay } from "/src/state";
+import state, { IPlayState, clearSelection, handleStopPlay, onPlay, resetPlaybackToStart, togglePlay, initSetPlayRate } from "/src/state";
 import { IPostMessage } from "/src/utils/native-message";
 import { usePageVisibility } from "@vant/use";
 import { browser } from "/src/utils";
@@ -128,7 +128,11 @@ const sendOffsetTime = async (offsetTime: number) => {
 export const handleStartEvaluat = async () => {
   if (state.modeType === "evaluating") {
     handleCancelEvaluat();
+    // 放下面会在异步之后执行 旋律线可能在会隐藏不了
+    state.modeType = "practise";
   } else {
+    // 放下面会在异步之后执行 旋律线可能在会隐藏不了
+    state.modeType = "evaluating";
     if (state.platform !== "PC") {
       // 评测前先检查APP端的websocket状态
       const res = await api_checkSocketStatus();
@@ -142,7 +146,7 @@ export const handleStartEvaluat = async () => {
       handleStopPlay();
     }
   }
-  state.modeType = state.modeType === "evaluating" ? "practise" : "evaluating";
+  //state.modeType = state.modeType === "evaluating" ? "practise" : "evaluating";
   if (state.modeType !== "evaluating") {
     // 切换到练习模式,卸载评测模块
     evaluatingData.rendered = false;
@@ -351,7 +355,6 @@ export const handleStartBegin = async (preTimes?: number) => {
 	evaluatingData.evaluatings = {};
 	evaluatingData.resultData = {};
 	evaluatingData.backtime = 0;
-	resetPlaybackToStart();
 	evaluatingData.isAudioPlayEnd = false;
 	const res = await startEvaluating(evaluatingData.contentData);
 	if (res?.api !== "startEvaluating") {
@@ -364,6 +367,8 @@ export const handleStartBegin = async (preTimes?: number) => {
 		evaluatingData.startBegin = false;
 		return;
 	}
+  initSetPlayRate();
+	resetPlaybackToStart();
 	evaluatingData.startBegin = true;
 	if (evaluatingData.isDisabledPlayMusic) {
 		evaluatingData.isBeginMask = true
@@ -621,6 +626,10 @@ const handleAccompanyError = (res?: IPostMessage) => {
         }
         // 关闭节拍器
         closeTick();
+        // socketerrror,才发送关闭延迟检测的消息
+        if (type === "socketError") {
+          api_closeDelayCheck({});
+        }
         evaluatingData.socketErrorStatus = 0;
         evaluatingData.socketErrorPop = type === "socketError" ? true : false;
         evaluatingData.isErrorState = true;
@@ -644,7 +653,6 @@ const handleSocketStatus = (res?: IPostMessage) => {
     const diffTime = currentTime - socketStartTime;
     if (diffTime < 1000) {
       const remainingTime = 1000 - diffTime;
-      console.log(remainingTime, 99999);
       setTimeout(() => {
         evaluatingData.socketErrorStatus = 2;
       }, remainingTime);
@@ -653,14 +661,14 @@ const handleSocketStatus = (res?: IPostMessage) => {
 };
 
 // 评测出现异常,再试一次
-const hanldeConfirmPop = async () => {
+export const hanldeConfirmPop = async () => {
   api_checkSocketStatus();
   evaluatingData.socketErrorStatus = 1;
   socketStartTime = +new Date();
 };
 
 // 关闭异常弹窗
-const hanldeClosePop = () => {
+export const hanldeClosePop = () => {
   evaluatingData.socketErrorPop = false;
   evaluatingData.socketErrorStatus = 0;
 };
@@ -762,7 +770,11 @@ export default defineComponent({
       api_remove_recordStartTime(recordStartTimePoint);
       handle_reduction();
       removeAccompanyError(handleAccompanyError);
-      removeSocketStatus(handleSocketStatus);
+      if (evaluatingData.socketErrorPop && state.setting.soundEffect) {
+        console.log('延迟检测出错')
+      } else {
+        removeSocketStatus(handleSocketStatus);
+      }
       api_disconnectSocket();
       console.log("卸载评测模块成功");
     });

File diff suppressed because it is too large
+ 0 - 0
src/view/fingering/fingering-img/hulusi-flute/index.json


File diff suppressed because it is too large
+ 0 - 0
src/view/fingering/fingering-img/pan-flute/index.json


+ 3 - 0
src/view/fingering/index.module.less

@@ -37,6 +37,9 @@
   //   background: linear-gradient(360deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%);
   //   z-index: 1;
   // }
+  &.hulusiTop {
+    padding-top: 20px;
+  }
 }
 
 .imgs {

+ 1 - 1
src/view/fingering/index.tsx

@@ -57,7 +57,7 @@ export default defineComponent({
               )}
             </div>
           ) : (
-            <div onClick={() => doubeClick()} class={[styles.fingeringContainer, styles.vertical, state.fingeringInfo.name]}>
+            <div onClick={() => doubeClick()} class={[styles.fingeringContainer, styles.vertical, state.fingeringInfo.name, state.fingeringInfo.name === 'hulusi-flute' ? styles.hulusiTop : '']}>
               <div class={styles.imgs}>
                 <img class="driver-7" src={fingerData.subject?.json?.full} />
                 {rs.map((key: number | string, index: number) => {

+ 4 - 1
src/view/follow-practice/index.tsx

@@ -21,6 +21,7 @@ export const followData = reactive({
 	rendered: false,
 	/** 麦克风权限 */
 	earphone: false,
+	isBeginMask: false // 倒计时和系统节拍器时候的遮罩,防止用户点击
 });
 
 // 记录跟练时长
@@ -97,6 +98,7 @@ const onClear = () => {
 
 /** 开始跟练 */
 export const handleFollowStart = async () => {
+	followData.isBeginMask = true
 	checking = false;
 	const res = await api_cloudToggleFollow("start");
 	// 用户没有授权,需要重置状态
@@ -104,15 +106,16 @@ export const handleFollowStart = async () => {
 		// 
 	} else {
 		// 跟练模式开始前,增加播放系统节拍器
-		followData.start = true;
 		const tickend = await handleStartTick();
 		// console.log("🚀 ~ tickend:", tickend)
 		// 节拍器返回false, 取消播放
 		if (!tickend) {
+			followData.isBeginMask = false
 			followData.start = false;
 			return false;
 		}
 		onClear();
+		followData.isBeginMask = false
 		followData.start = true;
 		followData.index = 0;
 		followData.list = [];

+ 11 - 0
src/view/music-score/index.module.less

@@ -28,6 +28,9 @@
             fill: #FFC121;
             stroke: #FFC121;
         }
+        rect {
+            stroke: #FFC121;
+        }
         transform-box: fill-box;
         transform-origin: center;
         // animation: noteAnimate 0.3s linear;
@@ -40,6 +43,14 @@
             stroke: #FFC121;
         }
     }
+    .blueMusicXml {
+        .vf-stave {
+            >path {
+                fill: rgba(255,255,255,0.5);
+                stroke: rgba(255,255,255,0.5);
+            }
+        }
+    }
     .vf-custom-rect {
         position: relative;
         // stroke: grey;

+ 23 - 12
src/view/music-score/index.tsx

@@ -94,7 +94,9 @@ export default defineComponent({
 				defaultColorMusic: props.musicColor, // 颜色
 				// pageBackgroundColor: '#609FCF',
 				renderSingleHorizontalStaffline: state.isSingleLine ? true : false,
-				autoGenerateMultipleRestMeasuresFromRestMeasures: state.isSingleLine ? false : true, // 连续休止小节是否合并显示
+				// autoGenerateMultipleRestMeasuresFromRestMeasures: state.isSingleLine ? false : true, // 连续休止小节是否合并显示
+				autoGenerateMultipleRestMeasuresFromRestMeasures: true,
+				drawLyrics: (state.playType === 'sing' && !state.isSimplePage) ? true : false, // 演唱模式才渲染歌词,simple页面不显示歌词
 				// darkMode: true, // 暗黑模式
 				// pageFormat: 'A4_P',
 				// autoBeam: true,
@@ -138,7 +140,11 @@ export default defineComponent({
 			osmd.EngravingRules.DYMusicScoreId = state.examSongId || ''
 			osmd.EngravingRules.DYCustomRepeatCount = state.maxLyricNum || 0;
 			await osmd.load(musicData.score);
-			osmd.zoom = state.isSimplePage ? 0.6 : state.zoom;
+			// 对外暴露 一行谱时候 缩小谱面
+			if(state.isSimplePage){
+				state.zoom = 0.6
+			}
+			osmd.zoom = state.zoom;
 			osmd.render();
 			console.log("🚀 ~ osmd:", osmd)
 			emit("rendered", osmd);
@@ -187,15 +193,19 @@ export default defineComponent({
 			state.osmd.clear();
 			musicData.isRenderLoading = true;
 			musicData.isRefreshLoading = true;
-			state.audioDone = false;
-			getContainerWidth();
-			setRenderType();
-			await getXML();
-			await init();
-			musicData.isRenderLoading = false;
-			musicData.isRefreshLoading = false;
-			state.audioDone = true;
-			musicData.showSelection = true;
+			state.loadingText = '正在加载中,请稍等…'
+			state.isLoading = true;
+			// 在下一帧再执行,确保出现loading
+			requestAnimationFrame(async ()=>{
+				getContainerWidth();
+				setRenderType();
+				await getXML();
+				await init();
+				musicData.isRenderLoading = false;
+				musicData.isRefreshLoading = false;
+				state.isLoading = false;
+				musicData.showSelection = true;
+			})
 		}
 		expose({
 			refreshMusicScore,
@@ -208,7 +218,8 @@ export default defineComponent({
 				class={[
 					isInTheGradualRange.value && styles.inGradualRange,
 					state.musicRenderType == EnumMusicRenderType.staff ? "staff" : "jianpuTone",
-					state.isSingleLine && "singleLineMusicBox"
+					state.isSingleLine && "singleLineMusicBox",
+					!state.isCreateImg ? "blueMusicXml" : ""
 				]}
 			>
 				{slots.default?.()}

+ 2 - 3
src/view/selection/index.module.less

@@ -138,9 +138,8 @@
     width: 20px;
     height: 20px;
     border-radius: 50%;
-    background-color: rgb(255, 145, 0);
-    color: #fff;
-    font-weight: bold;
+    background-color: #FFC121;
+    color: #673207;
     font-size: 14px;
 }
 

+ 13 - 0
src/view/selection/index.tsx

@@ -59,6 +59,9 @@ const calcNoteData = () => {
 							top: noteBbox.y - parentTop - noteBbox.height + "px",
 							width: noteBbox.width * 1.5 + "px",
 							height: noteBbox.height * 3 + "px",
+							x: item.bbox?.x,
+							y: item.bbox?.y,
+							originWidth: item.bbox?.width
 						};
 						const noteHead = noteEle.querySelector(".vf-numbered-note-head");
 						const noteHeadBbox = noteHead?.getBoundingClientRect?.();
@@ -66,6 +69,9 @@ const calcNoteData = () => {
 							item.bbox = {
 								left: noteHeadBbox.x - parentLeft - noteHeadBbox.width / 4,
 								width: noteHeadBbox.width * 1.5,
+								x: item.bbox?.x,
+								y: item.bbox?.y,
+								originWidth: item.bbox?.width
 							}
 						}
 						
@@ -75,6 +81,9 @@ const calcNoteData = () => {
 							top: staveBbox.y - parentTop + "px",
 							width: noteBbox.width * 1.5 + "px",
 							height: staveBbox.height + "px",
+							x: item.bbox?.x,
+							y: item.bbox?.y,
+							originWidth: item.bbox?.width
 						};
 					}
 					
@@ -153,6 +162,10 @@ const calcNoteData = () => {
 			}
 		}
 	}
+	// 部分浏览器渲染的第一小节的位置信息会包含拍号、调号,需要处理一下,剔除掉拍号、调号的位置
+	if (selectData.staves[0]?.staveBox?.top !== selectData.staves[1]?.staveBox?.top) {
+		selectData.staves[0].staveBox.top = selectData.staves[1]?.staveBox?.top || selectData.staves[0]?.staveBox?.top
+	}
 	console.log("🚀 ~ selectData.notes:", selectData.notes, selectData.staves);
 };
 

+ 1 - 1
src/view/transfer-to-img/index.module.less

@@ -29,4 +29,4 @@
             max-height: initial !important;
         }
     }
-}
+}

+ 2 - 0
src/view/transfer-to-img/index.tsx

@@ -41,6 +41,8 @@ export default defineComponent({
 
 		onMounted(() => {
 			(window as any).appName = "colexiu";
+			state.isCreateImg = true;
+			state.isEvxml = true;
 			state.xmlUrl = decodeURIComponent(query.xmlUrl);
 			const specialXmls = ['https://oss.dayaedu.com/MECMP/1715332965751.xml','https://oss.dayaedu.com/MECMP/1715326622946.xml'];
 			if (specialXmls.includes(state.xmlUrl)) {

+ 1 - 1
vite.config.ts

@@ -77,7 +77,7 @@ export default defineConfig({
         // target: "https://test.lexiaoya.cn",
         // target: "https://kt.colexiu.com",
         // target: "https://dev.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
-        target: "https://dev.kt.colexiu.com",
+        target: "https://test.kt.colexiu.com",
         //target: "https://mec.colexiu.com",
         changeOrigin: true,
         rewrite: (path) => path.replace(/^\/instrument/, ""),

Some files were not shown because too many files changed in this diff