diff --git a/.gitignore b/.gitignore index dc49877..2012493 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ dist-ssr *.local public/video/* public/words/* -public/audio/* # Editor directories and files .vscode/* diff --git a/public/audio/BGM.mp3 b/public/audio/BGM.mp3 new file mode 100644 index 0000000..3393b04 Binary files /dev/null and b/public/audio/BGM.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_01.mp3 b/public/audio/chenyun/mp3_chenyun_01.mp3 new file mode 100644 index 0000000..cab1494 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_01.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_02.mp3 b/public/audio/chenyun/mp3_chenyun_02.mp3 new file mode 100644 index 0000000..4c45df5 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_02.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_03.mp3 b/public/audio/chenyun/mp3_chenyun_03.mp3 new file mode 100644 index 0000000..691138b Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_03.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_04.mp3 b/public/audio/chenyun/mp3_chenyun_04.mp3 new file mode 100644 index 0000000..0617d6a Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_04.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_05.mp3 b/public/audio/chenyun/mp3_chenyun_05.mp3 new file mode 100644 index 0000000..5675ebb Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_05.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_06.mp3 b/public/audio/chenyun/mp3_chenyun_06.mp3 new file mode 100644 index 0000000..eb53f6d Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_06.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_07.mp3 b/public/audio/chenyun/mp3_chenyun_07.mp3 new file mode 100644 index 0000000..4e9c8a8 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_07.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_08.mp3 b/public/audio/chenyun/mp3_chenyun_08.mp3 new file mode 100644 index 0000000..20cc769 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_08.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_09.mp3 b/public/audio/chenyun/mp3_chenyun_09.mp3 new file mode 100644 index 0000000..53528fc Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_09.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_10.mp3 b/public/audio/chenyun/mp3_chenyun_10.mp3 new file mode 100644 index 0000000..6ef1063 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_10.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_11.mp3 b/public/audio/chenyun/mp3_chenyun_11.mp3 new file mode 100644 index 0000000..8033933 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_11.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_12.mp3 b/public/audio/chenyun/mp3_chenyun_12.mp3 new file mode 100644 index 0000000..bdd51a0 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_12.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_13.mp3 b/public/audio/chenyun/mp3_chenyun_13.mp3 new file mode 100644 index 0000000..0465876 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_13.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_14.mp3 b/public/audio/chenyun/mp3_chenyun_14.mp3 new file mode 100644 index 0000000..c38d829 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_14.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_15.mp3 b/public/audio/chenyun/mp3_chenyun_15.mp3 new file mode 100644 index 0000000..652c572 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_15.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_16.mp3 b/public/audio/chenyun/mp3_chenyun_16.mp3 new file mode 100644 index 0000000..5328cc7 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_16.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_17.mp3 b/public/audio/chenyun/mp3_chenyun_17.mp3 new file mode 100644 index 0000000..a7138e0 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_17.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_18.mp3 b/public/audio/chenyun/mp3_chenyun_18.mp3 new file mode 100644 index 0000000..d8d11b4 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_18.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_19.mp3 b/public/audio/chenyun/mp3_chenyun_19.mp3 new file mode 100644 index 0000000..137e52c Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_19.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_20.mp3 b/public/audio/chenyun/mp3_chenyun_20.mp3 new file mode 100644 index 0000000..b27b971 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_20.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_21.mp3 b/public/audio/chenyun/mp3_chenyun_21.mp3 new file mode 100644 index 0000000..29e2cdc Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_21.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_22.mp3 b/public/audio/chenyun/mp3_chenyun_22.mp3 new file mode 100644 index 0000000..466cc7c Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_22.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_23.mp3 b/public/audio/chenyun/mp3_chenyun_23.mp3 new file mode 100644 index 0000000..7a8e22c Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_23.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_24.mp3 b/public/audio/chenyun/mp3_chenyun_24.mp3 new file mode 100644 index 0000000..a157951 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_24.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_25.mp3 b/public/audio/chenyun/mp3_chenyun_25.mp3 new file mode 100644 index 0000000..d888574 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_25.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_26.mp3 b/public/audio/chenyun/mp3_chenyun_26.mp3 new file mode 100644 index 0000000..76d9fb6 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_26.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_27.mp3 b/public/audio/chenyun/mp3_chenyun_27.mp3 new file mode 100644 index 0000000..1434d5f Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_27.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_28.mp3 b/public/audio/chenyun/mp3_chenyun_28.mp3 new file mode 100644 index 0000000..b28bb67 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_28.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_29.mp3 b/public/audio/chenyun/mp3_chenyun_29.mp3 new file mode 100644 index 0000000..731a1cc Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_29.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_30.mp3 b/public/audio/chenyun/mp3_chenyun_30.mp3 new file mode 100644 index 0000000..a47c88d Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_30.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_31.mp3 b/public/audio/chenyun/mp3_chenyun_31.mp3 new file mode 100644 index 0000000..d932e1c Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_31.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_32.mp3 b/public/audio/chenyun/mp3_chenyun_32.mp3 new file mode 100644 index 0000000..1bb0249 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_32.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_33.mp3 b/public/audio/chenyun/mp3_chenyun_33.mp3 new file mode 100644 index 0000000..c956452 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_33.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_34.mp3 b/public/audio/chenyun/mp3_chenyun_34.mp3 new file mode 100644 index 0000000..21ee93c Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_34.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_35.mp3 b/public/audio/chenyun/mp3_chenyun_35.mp3 new file mode 100644 index 0000000..ab4f756 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_35.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_36.mp3 b/public/audio/chenyun/mp3_chenyun_36.mp3 new file mode 100644 index 0000000..4fc0e09 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_36.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_37.mp3 b/public/audio/chenyun/mp3_chenyun_37.mp3 new file mode 100644 index 0000000..8a7f92d Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_37.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_38.mp3 b/public/audio/chenyun/mp3_chenyun_38.mp3 new file mode 100644 index 0000000..5dc8373 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_38.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_39.mp3 b/public/audio/chenyun/mp3_chenyun_39.mp3 new file mode 100644 index 0000000..c1d6c24 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_39.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_01.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_01.mp3 new file mode 100644 index 0000000..aa8c4cc Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_01.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_02.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_02.mp3 new file mode 100644 index 0000000..4c45df5 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_02.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_03.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_03.mp3 new file mode 100644 index 0000000..da9b642 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_03.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_04.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_04.mp3 new file mode 100644 index 0000000..f501afc Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_04.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_05.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_05.mp3 new file mode 100644 index 0000000..6170a63 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_05.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_06.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_06.mp3 new file mode 100644 index 0000000..bc0d1f4 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_06.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_07.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_07.mp3 new file mode 100644 index 0000000..5942d5f Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_07.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_08.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_08.mp3 new file mode 100644 index 0000000..9f8592b Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_08.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_09.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_09.mp3 new file mode 100644 index 0000000..39e2858 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_09.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_10.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_10.mp3 new file mode 100644 index 0000000..e2760f9 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_10.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_11.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_11.mp3 new file mode 100644 index 0000000..1ddf03d Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_11.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_12.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_12.mp3 new file mode 100644 index 0000000..d4b2522 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_12.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_13.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_13.mp3 new file mode 100644 index 0000000..bdc6d13 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_13.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_14.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_14.mp3 new file mode 100644 index 0000000..592a145 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_14.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_15.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_15.mp3 new file mode 100644 index 0000000..fd69e5e Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_15.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_16.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_16.mp3 new file mode 100644 index 0000000..f5c196d Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_16.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_17.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_17.mp3 new file mode 100644 index 0000000..b27ed06 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_17.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_18.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_18.mp3 new file mode 100644 index 0000000..691470a Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_18.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_19.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_19.mp3 new file mode 100644 index 0000000..6c7e18a Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_19.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_20.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_20.mp3 new file mode 100644 index 0000000..3ac39b4 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_20.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_21.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_21.mp3 new file mode 100644 index 0000000..908b08e Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_21.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_22.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_22.mp3 new file mode 100644 index 0000000..508f44a Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_22.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_23.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_23.mp3 new file mode 100644 index 0000000..26efbaf Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_23.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_24.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_24.mp3 new file mode 100644 index 0000000..e31b942 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_24.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_25.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_25.mp3 new file mode 100644 index 0000000..be05995 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_25.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_26.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_26.mp3 new file mode 100644 index 0000000..12ce040 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_26.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_27.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_27.mp3 new file mode 100644 index 0000000..fe885d5 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_27.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_28.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_28.mp3 new file mode 100644 index 0000000..e248869 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_28.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_29.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_29.mp3 new file mode 100644 index 0000000..3d6a494 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_29.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_30.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_30.mp3 new file mode 100644 index 0000000..d9053ba Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_30.mp3 differ diff --git a/public/audio/letter/1东方红.m4a b/public/audio/letter/1东方红.m4a new file mode 100644 index 0000000..38992a8 Binary files /dev/null and b/public/audio/letter/1东方红.m4a differ diff --git a/public/audio/letter/2理想.m4a b/public/audio/letter/2理想.m4a new file mode 100644 index 0000000..88a1e0a Binary files /dev/null and b/public/audio/letter/2理想.m4a differ diff --git a/public/audio/letter/3我和我的祖国.m4a b/public/audio/letter/3我和我的祖国.m4a new file mode 100644 index 0000000..65ec626 Binary files /dev/null and b/public/audio/letter/3我和我的祖国.m4a differ diff --git a/public/audio/letter/4不忘初心.m4a b/public/audio/letter/4不忘初心.m4a new file mode 100644 index 0000000..578b43c Binary files /dev/null and b/public/audio/letter/4不忘初心.m4a differ diff --git a/public/audio/letter/5我爱你中国.m4a b/public/audio/letter/5我爱你中国.m4a new file mode 100644 index 0000000..f8c71e9 Binary files /dev/null and b/public/audio/letter/5我爱你中国.m4a differ diff --git a/public/audio/letter/6灯火里的中国.m4a b/public/audio/letter/6灯火里的中国.m4a new file mode 100644 index 0000000..900b2a5 Binary files /dev/null and b/public/audio/letter/6灯火里的中国.m4a differ diff --git a/public/audio/letter/7走向复兴.m4a b/public/audio/letter/7走向复兴.m4a new file mode 100644 index 0000000..d054fec Binary files /dev/null and b/public/audio/letter/7走向复兴.m4a differ diff --git a/public/audio/letter/8如愿.m4a b/public/audio/letter/8如愿.m4a new file mode 100644 index 0000000..4bb01b9 Binary files /dev/null and b/public/audio/letter/8如愿.m4a differ diff --git a/public/audio/test.mp3 b/public/audio/test.mp3 new file mode 100644 index 0000000..cab1494 Binary files /dev/null and b/public/audio/test.mp3 differ diff --git a/src/assets/images/huangyanpei/reflection/components/喇叭1.png b/src/assets/images/huangyanpei/reflection/components/喇叭1.png new file mode 100644 index 0000000..15f91c2 Binary files /dev/null and b/src/assets/images/huangyanpei/reflection/components/喇叭1.png differ diff --git a/src/assets/images/huangyanpei/reflection/components/喇叭2.png b/src/assets/images/huangyanpei/reflection/components/喇叭2.png new file mode 100644 index 0000000..52eb95d Binary files /dev/null and b/src/assets/images/huangyanpei/reflection/components/喇叭2.png differ diff --git a/src/assets/images/huangyanpei/reflection/components/题目图.png b/src/assets/images/huangyanpei/reflection/components/题目图.png new file mode 100644 index 0000000..e8622b4 Binary files /dev/null and b/src/assets/images/huangyanpei/reflection/components/题目图.png differ diff --git a/src/components/guide/GuideSteps.tsx b/src/components/guide/GuideSteps.tsx index 99776bf..8376aa1 100644 --- a/src/components/guide/GuideSteps.tsx +++ b/src/components/guide/GuideSteps.tsx @@ -1,9 +1,24 @@ -import { ReactNode, useState, Children, isValidElement } from "react"; +import { + ReactNode, + useState, + Children, + isValidElement, + useEffect, +} from "react"; import { AnimatePresence, motion } from "framer-motion"; import { cn, getBackgroundImage } from "@/lib/utils"; import { Overlay } from "@/components/overlay"; import { useVideoContextSafe } from "@/contexts/VideoContext"; +import { useAudioManager } from "@/hooks/useAudioManager"; +/** + * 使用示例: + * + * + * 第一步内容... + * + * + */ interface GuideStepsProps { children: ReactNode; className?: string; @@ -17,11 +32,17 @@ interface GuideStepsProps { showMask?: boolean; /** 自定义 z-index,用于控制层级(默认:50) */ zIndex?: number; + /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ + audioChannelId?: string; } interface GuideStepsItemProps { children: ReactNode; speaker?: "guide" | "student"; + /** 当前步骤对应的音频地址 */ + audioSrc?: string; + /** 进入该步骤时是否自动播放音频(有 audioSrc 且未显式设置时,默认自动播放) */ + autoPlayOnEnter?: boolean; } const GuideSteps = ({ @@ -34,6 +55,7 @@ const GuideSteps = ({ highlightSelectors, showMask = true, zIndex = 50, + audioChannelId, }: GuideStepsProps) => { const [internalStep, setInternalStep] = useState(0); const [internalVisible, setInternalVisible] = useState(true); @@ -41,21 +63,37 @@ const GuideSteps = ({ // 安全地获取视频上下文(如果不在 VideoProvider 中,返回 null) const videoContext = useVideoContextSafe(); + // 音频管理:默认全局互斥,如果传了 audioChannelId 则按 channel 互斥 + const { play, stop } = useAudioManager( + audioChannelId + ? { mode: "channel", channelId: audioChannelId } + : { mode: "global" } + ); + // 收集每个步骤的内容和 speaker 信息 const stepsData = Children.toArray(children).map((child) => { if ( isValidElement(child) && child.type === GuideStepsItem ) { + const hasAudio = !!child.props.audioSrc; return { content: child.props.children, speaker: child.props.speaker || "guide", // 默认为 "guide" 保持向后兼容 + audioSrc: child.props.audioSrc, + // 如果没有显式设置 autoPlayOnEnter,则在有 audioSrc 时默认自动播放 + autoPlayOnEnter: + child.props.autoPlayOnEnter !== undefined + ? child.props.autoPlayOnEnter + : hasAudio, }; } // 如果不是 GuideStepsItem,保持原有行为 return { content: child, speaker: "guide" as const, + audioSrc: undefined, + autoPlayOnEnter: false, }; }); @@ -76,6 +114,17 @@ const GuideSteps = ({ // 获取当前步骤的 speaker const currentSpeaker = stepsData[currentStep]?.speaker || "guide"; + const currentStepData = stepsData[currentStep]; + const currentAudioSrc = currentStepData?.audioSrc; + const currentAutoPlayOnEnter = currentStepData?.autoPlayOnEnter ?? false; + + // 步骤变化时,按需自动播放音频(依赖稳定值,避免循环触发) + useEffect(() => { + if (!isVisible) return; + if (currentAutoPlayOnEnter && currentAudioSrc) { + play(currentAudioSrc); + } + }, [isVisible, currentStep, currentAutoPlayOnEnter, currentAudioSrc, play]); const updateStep = (newStep: number) => { if (!isControlled) { @@ -88,9 +137,20 @@ const GuideSteps = ({ if (visible === undefined) { setInternalVisible(newVisible); } + // 隐藏引导时,停止当前 channel 的音频 + if (!newVisible) { + stop(); + } onVisibleChange?.(newVisible); }; + // 受控 visible 场景:当外部把 visible 置为 false 时,也需要停止音频 + useEffect(() => { + if (!isVisible) { + stop(); + } + }, [isVisible, stop]); + const handleNext = (e: React.MouseEvent) => { e.stopPropagation(); // 阻止事件冒泡 if (currentStep < totalSteps - 1) { diff --git a/src/components/guide/StudyGuideModal.tsx b/src/components/guide/StudyGuideModal.tsx index db1e2c0..923c710 100644 --- a/src/components/guide/StudyGuideModal.tsx +++ b/src/components/guide/StudyGuideModal.tsx @@ -4,6 +4,8 @@ import { createPortal } from "react-dom"; import { Image } from "@/components"; import { cn, getStaticImage } from "@/lib/utils"; import { useScale } from "@/contexts/ScaleContext"; +import { useAudioManager } from "@/hooks/useAudioManager"; +import { useVideoContextSafe } from "@/contexts/VideoContext"; export interface StudyGuideModalProps { /** 是否可见 */ @@ -22,11 +24,27 @@ export interface StudyGuideModalProps { ailgn?: "center" | "left" | "right"; // 垂直对齐方式,默认 center verticalAlign?: "center" | "top" | "bottom"; + /** 引导语音地址(可选) */ + audioSrc?: string; + /** 弹窗出现时是否自动播放引导语音(默认 true) */ + autoPlayAudio?: boolean; + /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ + audioChannelId?: string; } /** * 通用学习引导弹窗 * 使用 images/components/guide 下的引导框 + 继续按钮资源 + * + * 使用示例: + * + * 文案内容... + * */ export const StudyGuideModal = ({ visible = false, @@ -37,15 +55,58 @@ export const StudyGuideModal = ({ zIndex = 120, ailgn = "center", verticalAlign = "top", + audioSrc, + autoPlayAudio = true, + audioChannelId, }: StudyGuideModalProps) => { const [mounted, setMounted] = useState(false); const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale(); + const videoContext = useVideoContextSafe(); + const { currentSrc, play, stop } = useAudioManager( + audioChannelId + ? { + mode: "channel", + channelId: audioChannelId, + } + : { mode: "global" } + ); // SSR 兼容:只在客户端挂载 useEffect(() => { setMounted(true); }, []); + // 当视频正在播放或内容未展示时,不应播放引导语音 + const canPlayAudio = + !videoContext || + (!videoContext.isVideoPlaying && videoContext.isContentVisible); + + // 弹窗显隐联动音频播放/停止 + useEffect(() => { + if (!mounted) return; + + if (visible && autoPlayAudio && audioSrc && canPlayAudio) { + play(audioSrc); + } + // 注意:这里不依赖 currentSrc,避免 play() 更新状态后重复触发自动播放 + }, [visible, mounted, autoPlayAudio, audioSrc, play, canPlayAudio]); + + // 关闭弹窗时停止(仅当当前播放的是该弹窗的 audioSrc) + useEffect(() => { + if (!mounted) return; + if (!visible && audioSrc && currentSrc === audioSrc) { + stop(); + } + }, [visible, mounted, audioSrc, currentSrc, stop]); + + // 当视频开始播放时,如果当前正在播放该弹窗音频,则停止,避免视频音频与弹窗音频重叠 + useEffect(() => { + if (!mounted) return; + if (!canPlayAudio && audioSrc && currentSrc === audioSrc) { + stop(); + } + }, [mounted, canPlayAudio, audioSrc, currentSrc, stop]); + if (!visible || !mounted) return null; const handleMaskClick = () => { diff --git a/src/components/index.ts b/src/components/index.ts index 11245ce..0ddafed 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -24,6 +24,9 @@ export type { ExhibitOverlayProps } from "./exhibit"; export { Overlay } from "./overlay"; export type { OverlayProps } from "./overlay"; +export { EndingOverlay } from "./overlay"; +export type { EndingOverlayProps } from "./overlay"; + // Spotlight component export { Spotlight } from "./spotlight"; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index e62ee80..ae615e2 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1,6 +1,6 @@ export { default as ResidenceLayout } from "./ResidenceLayout"; -export { default as ClueOverlay } from "./clue"; -export type { ClueOverlayProps } from "./clue"; +export { default as ClueOverlay } from "../../pages/chenyun/reflection/clue"; +export type { ClueOverlayProps } from "../../pages/chenyun/reflection/clue"; export { default as Alert } from "./alert/title-alert"; export type { AlertProps } from "./alert/title-alert"; export { default as SuccessAlert } from "./alert/success-alert"; @@ -9,12 +9,12 @@ export { default as ErrorAlert } from "./alert/error-alert"; export type { ErrorAlertProps } from "./alert/error-alert"; export { DragQuizPanel } from "./quiz"; export type { DragQuizPanelProps } from "./quiz"; -export type { - ClueData, - ClueType, - TextClueData, - MapClueData, - AudioClueData, - DialogueClueData, - CustomClueData, -} from "./clue/types"; +// export type { +// ClueData, +// ClueType, +// TextClueData, +// MapClueData, +// AudioClueData, +// DialogueClueData, +// CustomClueData, +// } from "../../pages/chenyun/reflection/clue/types"; diff --git a/src/components/layout/quiz/MultipleChoiceQuizPanel.tsx b/src/components/layout/quiz/MultipleChoiceQuizPanel.tsx index 0073f97..69edcd0 100644 --- a/src/components/layout/quiz/MultipleChoiceQuizPanel.tsx +++ b/src/components/layout/quiz/MultipleChoiceQuizPanel.tsx @@ -90,8 +90,12 @@ const MultipleChoiceQuizPanel = ({ onSubmit?.(submittedValues, true); } else { // 答题失败:清空状态,执行 onError,允许重新选择 + const submittedValues = [...selectedValues]; setSelectedValues([]); - const errorMsg = errorMessage || "很抱歉,你选错了!请你再思考一下吧!"; + const errorMsg = + typeof errorMessage === "function" + ? errorMessage(submittedValues) + : errorMessage || "很抱歉,你选错了!请你再思考一下吧!"; setError(errorMsg); onError?.(errorMsg); } diff --git a/src/components/layout/quiz/types.ts b/src/components/layout/quiz/types.ts index 32d22cc..e264315 100644 --- a/src/components/layout/quiz/types.ts +++ b/src/components/layout/quiz/types.ts @@ -65,8 +65,8 @@ export interface MultipleChoiceQuizPanelProps { options: MultipleChoiceOption[]; /** 正确答案数组(可选,用于验证) */ correctAnswer?: string[]; - /** 自定义错误提示信息(答案错误时显示) */ - errorMessage?: string; + /** 自定义错误提示信息(答案错误时显示),可以是字符串或函数(根据用户选择返回不同提示) */ + errorMessage?: string | ((selectedValues: string[]) => string); /** 自定义最小选择数量不足时的提示信息 */ minSelectionsErrorMessage?: string; /** 提交回调,返回用户选择和是否正确 */ diff --git a/src/components/letter/LetterToHuangyanpei.tsx b/src/components/letter/LetterToHuangyanpei.tsx index 3d83615..ec3b6d9 100644 --- a/src/components/letter/LetterToHuangyanpei.tsx +++ b/src/components/letter/LetterToHuangyanpei.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { getStaticImage } from "@/lib/utils"; import { Image } from "@/components"; @@ -23,6 +23,47 @@ const LetterToHuangyanpei = ({ }: LetterToHuangyanpeiProps) => { // 下划线可填写内容 const [blankValues, setBlankValues] = useState([]); + // 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字) + const [blankErrors, setBlankErrors] = useState([]); + + // 是否已提交:提交后隐藏编辑信件,展示只读信件 + const [submitted, setSubmitted] = useState(false); + + // 校验输入:不能为空、每个不超过 10 个字符 + const validateBlanks = () => { + // 计算占位总数,防止有些没被输入过而是 undefined + const placeholderCount = (letterContent.match(/____/g) || []).length; + + const nextErrors: boolean[] = Array(placeholderCount).fill(false); + const nextValues: string[] = [...blankValues]; + let hasError = false; + + for (let i = 0; i < placeholderCount; i++) { + const raw = blankValues[i] || ""; + const v = raw.trim(); + + // 为空 或 超过 10 个字,都视为不符合要求:清空并标红 + if (!v || v.length > 10) { + nextValues[i] = ""; + nextErrors[i] = true; + hasError = true; + } + } + + setBlankValues(nextValues); + setBlankErrors(nextErrors); + + return !hasError; + }; + + // 弹窗关闭后重置内容,方便下次重新填写 + useEffect(() => { + if (!visible) { + setSubmitted(false); + setBlankValues([]); + setBlankErrors([]); + } + }, [visible]); // 信件内容模板(根据设计稿) const letterContent = `敬爱的黄炎培先生: @@ -67,112 +108,239 @@ const LetterToHuangyanpei = ({
e.stopPropagation()} + style={{ perspective: "1200px" }} > - {/* 信纸背景 */} - 信纸 - - {/* 标题 - 垂直显示在左侧 */} -
+ {/* 编辑态 / 只读态:Y 轴翻转切换 */} + + {/* 前面:编辑态 */}
- 写给黄炎培先生的信 -
-
+ {/* 编辑态:信纸背景 */} + 信纸 - {/* 信件内容区域 */} -
-
- {(() => { - let placeholderIndex = 0; - let foundNiHao = false; - return letterContent.split("\n").map((line, lineIndex) => { - // 检测是否已经找到"您好"行 - if (line.includes("您好")) { - foundNiHao = true; - } + {/* 标题 - 垂直显示在左侧 */} +
+
+ 写给黄炎培先生的信 +
+
- // "您好"行之后的非空行需要首行缩进 - const needsIndent = - foundNiHao && - line.trim() !== "" && - !line.includes("您好"); - - const segments = line.split("____"); - return ( -
- {segments.map((part, partIndex) => { - const isLast = partIndex === segments.length - 1; - if (isLast) { - return ( - - {part} - - ); + {/* 信件内容区域(可编辑) */} +
+
+ {(() => { + let placeholderIndex = 0; + let foundNiHao = false; + return letterContent + .split("\n") + .map((line, lineIndex) => { + // 检测是否已经找到"您好"行 + if (line.includes("您好")) { + foundNiHao = true; } - const currentIndex = placeholderIndex++; + // "您好"行之后的非空行需要首行缩进 + const needsIndent = + foundNiHao && + line.trim() !== "" && + !line.includes("您好"); + + const segments = line.split("____"); return ( - - {part} - { - const next = [...blankValues]; - next[currentIndex] = e.target.value; - setBlankValues(next); - }} - /> - +
+ {segments.map((part, partIndex) => { + const isLast = + partIndex === segments.length - 1; + if (isLast) { + return ( + + {part} + + ); + } + + const currentIndex = placeholderIndex++; + return ( + + {part} + { + const next = [...blankValues]; + next[currentIndex] = e.target.value; + setBlankValues(next); + + // 用户再次输入时,清除该空的错误高亮 + if (blankErrors[currentIndex]) { + const nextErrs = [...blankErrors]; + nextErrs[currentIndex] = false; + setBlankErrors(nextErrs); + } + }} + /> + + ); + })} +
); - })} -
- ); - }); - })()} + }); + })()} +
+
+ + {/* 笔的图片 */} +
+ 笔 +
+ + {/* 提交按钮:切换到只读信件态 */} +
+ 可提交状态 { + if (!validateBlanks()) return; + setSubmitted(true); + }} + button + /> +
-
- {/* 笔的图片 */} -
- 笔 -
+ {/* 背面:只读态 */} +
+ {/* 只读态:新背景 */} + 信件 + 信纸 { + setSubmitted(false); + }} + /> - {/* 提交按钮 */} -
- 可提交状态 onSubmit?.(blankValues)} - button - /> -
+ {/* 信件内容区域(只读) */} +
+
+ {(() => { + let placeholderIndex = 0; + let foundNiHao = false; + return letterContent + .split("\n") + .map((line, lineIndex) => { + if (line.includes("您好")) { + foundNiHao = true; + } - {/* 寄信图标(可选,如果需要的话) */} -
- 寄信 -
+ const needsIndent = + foundNiHao && + line.trim() !== "" && + !line.includes("您好"); + + const segments = line.split("____"); + return ( +
+ {segments.map((part, partIndex) => { + const isLast = + partIndex === segments.length - 1; + if (isLast) { + return ( + + {part} + + ); + } + + const currentIndex = placeholderIndex++; + const value = blankValues[currentIndex] ?? ""; + + return ( + + {part} + {value || "  "} + + ); + })} +
+ ); + }); + })()} +
+
+ + {/* 寄出按钮:关闭窗口 */} +
+ 寄出 { + onSubmit?.(blankValues); + }} + button + /> +
+
+
diff --git a/src/components/narration/Narration.tsx b/src/components/narration/Narration.tsx index 7dca629..38ea500 100644 --- a/src/components/narration/Narration.tsx +++ b/src/components/narration/Narration.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; +import { useAudioManager } from "@/hooks/useAudioManager"; export interface NarrationProps { /** 旁白文本 */ @@ -16,6 +17,10 @@ export interface NarrationProps { enableAnimation?: boolean; /** 文字动画速度(毫秒/字),默认 100ms */ animationSpeed?: number; + /** 覆盖层出现时是否自动播放音频(默认 true) */ + autoPlayAudio?: boolean; + /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ + audioChannelId?: string; } /** @@ -34,18 +39,33 @@ const Narration = ({ className, enableAnimation = true, animationSpeed = 100, + autoPlayAudio = true, + audioChannelId, }: NarrationProps) => { const [displayedText, setDisplayedText] = useState(""); const [isAnimating, setIsAnimating] = useState(true); - const [audio, setAudio] = useState(null); const [isSkipped, setIsSkipped] = useState(false); + // 旁白音频:默认自动播放,支持按 channel 分组互斥 + const { currentSrc, play, stop } = useAudioManager( + audioChannelId + ? { + mode: "channel", + channelId: audioChannelId, + } + : { mode: "global" } + ); + // 文字动画效果 useEffect(() => { if (!visible) { setDisplayedText(""); setIsAnimating(true); setIsSkipped(false); + // 旁白关闭时,如果当前正在播放该旁白音频,则停止 + if (audioSrc && currentSrc === audioSrc) { + stop(); + } return; } @@ -76,7 +96,7 @@ const Narration = ({ }, animationSpeed); return () => clearInterval(timer); - }, [text, visible, enableAnimation, animationSpeed, isSkipped]); + }, [text, visible, enableAnimation, animationSpeed, isSkipped, audioSrc, currentSrc, stop]); // 跳过动画 const handleSkip = () => { @@ -85,27 +105,19 @@ const Narration = ({ setIsSkipped(true); }; - // 语音播报 + // 旁白出现时自动播放音频 useEffect(() => { - if (!visible || !audioSrc) return; + if (!visible) return; + if (!autoPlayAudio) return; + if (!audioSrc) return; - const audioElement = new Audio(audioSrc); - audioElement.play().catch((error) => { - console.warn("Audio playback failed:", error); - }); - setAudio(audioElement); - - return () => { - audioElement.pause(); - audioElement.currentTime = 0; - }; - }, [visible, audioSrc]); + play(audioSrc); + }, [visible, autoPlayAudio, audioSrc, play]); // 停止音频 const handleClose = () => { - if (audio) { - audio.pause(); - audio.currentTime = 0; + if (audioSrc && currentSrc === audioSrc) { + stop(); } onClose(); }; diff --git a/src/pages/chenyun/reflection/shared/EndingOverlay.tsx b/src/components/overlay/EndingOverlay.tsx similarity index 67% rename from src/pages/chenyun/reflection/shared/EndingOverlay.tsx rename to src/components/overlay/EndingOverlay.tsx index d20ecd9..240529b 100644 --- a/src/pages/chenyun/reflection/shared/EndingOverlay.tsx +++ b/src/components/overlay/EndingOverlay.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { getStaticImage } from "@/lib/utils"; +import { useAudioManager } from "@/hooks/useAudioManager"; export interface EndingOverlayProps { /** 是否可见 */ @@ -10,6 +11,12 @@ export interface EndingOverlayProps { onContinue: () => void; /** 文字动画速度(毫秒/字),默认 60ms */ animationSpeed?: number; + /** 结语对应的音频地址(可选) */ + audioSrc?: string; + /** 覆盖层出现时是否自动播放音频(默认 true) */ + autoPlayAudio?: boolean; + /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ + audioChannelId?: string; } /** @@ -21,15 +28,32 @@ export const EndingOverlay = ({ text, onContinue, animationSpeed = 60, + audioSrc, + autoPlayAudio = true, + audioChannelId, }: EndingOverlayProps) => { const [displayedText, setDisplayedText] = useState(""); const [isAnimating, setIsAnimating] = useState(false); + // 结语音频:默认自动播放,支持按 channel 分组互斥 + const { currentSrc, play, stop } = useAudioManager( + audioChannelId + ? { + mode: "channel", + channelId: audioChannelId, + } + : { mode: "global" } + ); + // 文字逐字动画 useEffect(() => { if (!visible) { setDisplayedText(""); setIsAnimating(false); + // 结语关闭时,如果当前正在播放该结语音频,则停止 + if (audioSrc && currentSrc === audioSrc) { + stop(); + } return; } @@ -48,7 +72,16 @@ export const EndingOverlay = ({ }, animationSpeed); return () => clearInterval(timer); - }, [visible, text, animationSpeed]); + }, [visible, text, animationSpeed, audioSrc, currentSrc, stop]); + + // 覆盖层显示时自动播放音频 + useEffect(() => { + if (!visible) return; + if (!autoPlayAudio) return; + if (!audioSrc) return; + + play(audioSrc); + }, [visible, autoPlayAudio, audioSrc, play]); // 全部文字出现后 1 秒自动关闭 useEffect(() => { diff --git a/src/components/overlay/index.ts b/src/components/overlay/index.ts index 74c8860..4b6f90b 100644 --- a/src/components/overlay/index.ts +++ b/src/components/overlay/index.ts @@ -1,3 +1,5 @@ export { default as Overlay } from "./Overlay"; export type { OverlayProps } from "./Overlay"; +export { EndingOverlay } from "./EndingOverlay"; +export type { EndingOverlayProps } from "./EndingOverlay"; diff --git a/src/hooks/useAudioManager.ts b/src/hooks/useAudioManager.ts new file mode 100644 index 0000000..c5db12a --- /dev/null +++ b/src/hooks/useAudioManager.ts @@ -0,0 +1,136 @@ +import { useCallback, useEffect, useState } from "react"; + +type AudioMode = "global" | "channel"; + +interface UseAudioManagerOptions { + mode?: AudioMode; + channelId?: string; +} + +interface AudioState { + key: string | null; + currentSrc: string | null; + isPlaying: boolean; +} + +// 全局音频池:每个 key(全局或某个 channel)对应一个 HTMLAudioElement +const audioPool = new Map(); + +// 订阅者列表,用于在任意地方播放/暂停时同步状态 +const listeners = new Set<(state: AudioState) => void>(); + +let globalState: AudioState = { + key: null, + currentSrc: null, + isPlaying: false, +}; + +function emit(state: AudioState) { + globalState = state; + listeners.forEach((listener) => listener(state)); +} + +function resolveKey(options?: UseAudioManagerOptions): string { + const mode = options?.mode ?? "global"; + + if (mode === "global") { + return "__GLOBAL__"; + } + + // mode === "channel" + return `channel:${options?.channelId ?? "__DEFAULT__"}`; +} + +function getOrCreateAudio(key: string): HTMLAudioElement { + let audio = audioPool.get(key); + if (!audio) { + audio = new Audio(); + audioPool.set(key, audio); + + audio.addEventListener("ended", () => { + // 某个 key 下的音频自然播放结束时,仅更新该 key 对应的播放状态 + if (globalState.key === key) { + emit({ + key, + currentSrc: globalState.currentSrc, + isPlaying: false, + }); + } + }); + } + return audio; +} + +/** + * 分组互斥音频管理: + * - mode = "global":全局单例,所有使用方互斥 + * - mode = "channel" + channelId:同一 channel 内互斥,channel 之间互不影响 + */ +export function useAudioManager(options?: UseAudioManagerOptions) { + const [state, setState] = useState(globalState); + + // 将 options 展开为基础类型,避免在 useCallback 中依赖对象引用导致每次 render 都重建函数 + const resolvedMode: AudioMode = options?.mode ?? "global"; + const resolvedChannelId: string | undefined = options?.channelId; + + useEffect(() => { + listeners.add(setState); + return () => { + listeners.delete(setState); + }; + }, []); + + const play = useCallback( + (src: string) => { + const key = resolveKey({ + mode: resolvedMode, + channelId: resolvedChannelId, + }); + const audio = getOrCreateAudio(key); + + if (audio.src !== src) { + audio.src = src; + } + + audio.currentTime = 0; + // 捕获浏览器因策略或中断导致的 play() 拒绝,避免控制台 Unhandled promise + audio.play().catch(() => { + // 可以在这里按需上报或忽略 + }); + + emit({ + key, + currentSrc: src, + isPlaying: true, + }); + }, + [resolvedMode, resolvedChannelId] + ); + + const stop = useCallback(() => { + const key = resolveKey({ + mode: resolvedMode, + channelId: resolvedChannelId, + }); + const audio = audioPool.get(key); + + if (!audio) return; + + audio.pause(); + audio.currentTime = 0; + + if (globalState.key === key) { + emit({ + key, + currentSrc: globalState.currentSrc, + isPlaying: false, + }); + } + }, [resolvedMode, resolvedChannelId]); + + return { + ...state, + play, + stop, + }; +} diff --git a/src/pages/chenyun/internalization.tsx b/src/pages/chenyun/internalization.tsx index 83e86fc..e73b511 100644 --- a/src/pages/chenyun/internalization.tsx +++ b/src/pages/chenyun/internalization.tsx @@ -355,13 +355,19 @@ const ChenyunInternalization = () => { }} showMask={false} > - + 小沪,跟着你参观陈云纪念馆真是收获满满!看到馆里展示的“米棉之战”史料、全国财经统筹的实物展品,我真切感受到陈云爷爷在建国初期平抑物价、稳定经济的魄力。 他用“全国一盘棋”思路化解危机的案例太震撼了,那你能再跟我讲讲,支撑这些成就的核心经济思想是什么吗? - + 针对你的这个问题,今天我们有幸邀请到了中国中共党史学会理事,全国陈云思想生平研究会常务理事兼副秘书长,中共一大纪念馆陈列研究部主任,研究馆员房中老师。关于你的问题,我想房老师会给你指点迷津! @@ -379,13 +385,19 @@ const ChenyunInternalization = () => { showMask={false} zIndex={60} > - + 感谢房老师的讲解!从陈云童年成长的水乡故居,到他主持"银元之战""米棉之战"的史料,再到那台称量粮食的天平、握不住的铅笔头,我真切感受到他"不唯上、不唯书、只唯实"的精神,也懂了他"为老百姓做事"的初心。 这些都让我特别受触动! - + 你的感悟很深刻!陈云同志用一生诠释了“务实”与“为民”——从破解边区经济困境,到擘画国家工业化蓝图,再到改革开放时力挺宝钢建设,每一步都扎根实际、心系百姓。 diff --git a/src/pages/chenyun/place/place1.tsx b/src/pages/chenyun/place/place1.tsx index e92332b..26d7fb0 100644 --- a/src/pages/chenyun/place/place1.tsx +++ b/src/pages/chenyun/place/place1.tsx @@ -8,7 +8,7 @@ import { StudyGuideModal, } from "@/components"; import { useNavigate, useParams } from "react-router-dom"; -import { getBackgroundImage, getStaticImage } from "@/lib/utils"; +import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useState, useMemo } from "react"; import Cursor from "@/components/cursor"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; @@ -176,6 +176,7 @@ const ChenyunPlace1 = () => { setShowGuideModal(false); setVisible(true); }} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_01.mp3")} > 同学你好,欢迎来到“走进故居·对话”学习模块!{"\n"} 此刻你正站在陈云同志的故居堂屋里,接下来请跟着时光的脚步, @@ -196,7 +197,7 @@ const ChenyunPlace1 = () => { )} - {/* 引导 */} + {/* 场景对话 */} { @@ -206,13 +207,18 @@ const ChenyunPlace1 = () => { setShowAfterDialogGuide(true); } }} - showMask={true} - highlightSelectors={[]} + showMask={false} > - + 小沪!这就是陈云小时候生活的房子吗?看起来和普通农家差不多,他童年过得艰苦吗? - + 这座房子就是陈云故居,位于上海市青浦区练塘镇下塘街95号,是一座坐南朝北、面向河畔的两层楼江南民居。 @@ -228,6 +234,7 @@ const ChenyunPlace1 = () => { visible={showAfterDialogGuide} onContinue={() => setShowAfterDialogGuide(false)} ailgn="left" + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_02.mp3")} > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {"\n"} diff --git a/src/pages/chenyun/place/place2.tsx b/src/pages/chenyun/place/place2.tsx index 9292027..7e36d32 100644 --- a/src/pages/chenyun/place/place2.tsx +++ b/src/pages/chenyun/place/place2.tsx @@ -7,7 +7,7 @@ import { GuideSteps, GuideStepsItem, } from "@/components"; -import { getBackgroundImage, getStaticImage } from "@/lib/utils"; +import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { useChenyunStore } from "@/store/chenyunStore"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; @@ -167,10 +167,16 @@ const ChenyunPlace2 = () => { highlightSelectors={[]} zIndex={10000} > - + 那陈云是怎么有机会到图中的青浦县乙种商业学校读书的呀? - + 因为家贫,初小读完的陈云便辍学在家。后在舅母亲戚的资助下,陈云才到青浦县乙种商业学校学习珠算和记账的,他在很短时间里就基本掌握了珠算,还初步学会写账,这为陈云日后从事经济工作打下了基础。 @@ -188,10 +194,16 @@ const ChenyunPlace2 = () => { highlightSelectors={[]} zIndex={10000} > - + 这里有一幅陈云提写的颜安小学,请问陈云在这所学校读过书吗? - + 是的,陈云在青浦县乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。 diff --git a/src/pages/chenyun/place/place3.tsx b/src/pages/chenyun/place/place3.tsx index c2ef741..327e8db 100644 --- a/src/pages/chenyun/place/place3.tsx +++ b/src/pages/chenyun/place/place3.tsx @@ -176,10 +176,16 @@ const ChenyunPlace3 = () => { highlightSelectors={[]} zIndex={10000} > - + 陈云14岁就去了上海商务印书馆当学徒,他主要负责哪些工作呢? - + 从颜安小学毕业后,陈云进入了商务印书馆当学徒,开始在柜台接待顾客,打珠算、写信封、练包装、理货物、修文具。 diff --git a/src/pages/chenyun/place/place4.tsx b/src/pages/chenyun/place/place4.tsx index f86de71..5f4741f 100644 --- a/src/pages/chenyun/place/place4.tsx +++ b/src/pages/chenyun/place/place4.tsx @@ -7,7 +7,7 @@ import { GuideSteps, GuideStepsItem, } from "@/components"; -import { getBackgroundImage, getStaticImage } from "@/lib/utils"; +import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { useChenyunStore } from "@/store/chenyunStore"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; @@ -165,10 +165,16 @@ const ChenyunPlace4 = () => { highlightSelectors={[]} zIndex={10000} > - + 这组史料看起来陈云前往中央苏区了,他在中央苏区主要负责哪些工作呢? - + 1932年3月,陈云担任中共临时中央政治局常委,兼任全国总工会党团书记。1933年1月,陈云离开上海前往中央苏区首府瑞金,继续参加中共中央和全国总工会的领导工作。 @@ -189,10 +195,16 @@ const ChenyunPlace4 = () => { highlightSelectors={[]} zIndex={10000} > - + 苏区的经济工作一定不简单,陈云是如何开展工作的呢? - + 中央苏区时期的经济斗争,是陈云经济工作能力的“淬炼场”。陈云分析了“左”倾经济政策的表现、危害以及原因。 diff --git a/src/pages/chenyun/place/place5.tsx b/src/pages/chenyun/place/place5.tsx index 2c4061c..5e4ef7d 100644 --- a/src/pages/chenyun/place/place5.tsx +++ b/src/pages/chenyun/place/place5.tsx @@ -155,10 +155,16 @@ const ChenyunPlace5 = () => { highlightSelectors={[]} zIndex={10000} > - + 小沪,延安时期因为国民党的封锁,陕甘宁边区经济困难。陈云转战陕甘宁边区主持财经工作,他一定遇到了很多经济难题吧。 - + 1944年新春伊始,陈云开始担任西北财经办事处副主任,工作转到财经方面,这是陈云以主要精力从事领导经济工作的开始。当时,陕甘宁边区在国民党严密封锁下,财经状况严峻,边区物价猛涨,老百姓日常生活必需的布匹和粮食十分缺乏。 diff --git a/src/pages/chenyun/place/place6.tsx b/src/pages/chenyun/place/place6.tsx index be0812b..87e85a0 100644 --- a/src/pages/chenyun/place/place6.tsx +++ b/src/pages/chenyun/place/place6.tsx @@ -7,7 +7,7 @@ import { GuideSteps, GuideStepsItem, } from "@/components"; -import { getBackgroundImage, getStaticImage } from "@/lib/utils"; +import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { useChenyunStore } from "@/store/chenyunStore"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; @@ -134,10 +134,16 @@ const ChenyunPlace6 = () => { highlightSelectors={[]} zIndex={10000} > - + 这是中央人民政府院财政经济委员会的印章,到这个时候陈云已经从地方到中央工作了吗? - + 是的,新中国刚成立时,中国共产党接管的是一个民穷财尽、物价飞涨的烂摊子。面对财政经济上的严重困难,陈云在1949年受命担任新成立的中央财政经济委员会(简称中财委)主任,主持领导全国财政经济工作。 diff --git a/src/pages/chenyun/place/place7.tsx b/src/pages/chenyun/place/place7.tsx index ceb382b..0477f71 100644 --- a/src/pages/chenyun/place/place7.tsx +++ b/src/pages/chenyun/place/place7.tsx @@ -261,10 +261,16 @@ const ChenyunPlace7 = () => { highlightSelectors={[]} zIndex={10001} > - + 那"银元之战"是如何取得胜利的呢? - + 由于通货膨胀,国民党发行的法币、金圆券变成了一堆废纸。当时坊间流传"大街过三道,物价跳三跳"。 @@ -332,10 +338,16 @@ const ChenyunPlace7 = () => { highlightSelectors={[]} zIndex={10011} > - + 那“两白一黑”战役是如何打赢的呢? - + “银元之战”的胜利并没有遏制住投机者的嚣张气焰,物价稳定仅十几天后,投机者就由银元投机转向“两白一黑”的投机,即:棉纱、大米和煤炭,引发了又一次全国性涨价狂潮。 diff --git a/src/pages/chenyun/place/place8.tsx b/src/pages/chenyun/place/place8.tsx index 4dc51dd..c6d726a 100644 --- a/src/pages/chenyun/place/place8.tsx +++ b/src/pages/chenyun/place/place8.tsx @@ -7,7 +7,7 @@ import { GuideSteps, GuideStepsItem, } from "@/components"; -import { getBackgroundImage, getStaticImage } from "@/lib/utils"; +import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { useChenyunStore } from "@/store/chenyunStore"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; @@ -170,10 +170,16 @@ const ChenyunPlace8 = () => { highlightSelectors={[]} zIndex={10001} > - + 您讲的"统购统销"是不是和这里我们看到的粮票有直接关系啊? - + 不错,随着大规模经济建设的展开,全国粮、棉、油等主要农产品严重紧缺,供求矛盾尖锐。陈云经过调查研究,反复比较,权衡利弊得失,终于下决心在农村征购,在城市配售的解决粮食问题的办法,也就是后来所说的"统购统销"。 @@ -192,10 +198,16 @@ const ChenyunPlace8 = () => { highlightSelectors={[]} zIndex={10001} > - + 打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗? - + 一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。 diff --git a/src/pages/chenyun/place/place9.tsx b/src/pages/chenyun/place/place9.tsx index e0a1d98..c22a3ae 100644 --- a/src/pages/chenyun/place/place9.tsx +++ b/src/pages/chenyun/place/place9.tsx @@ -157,7 +157,10 @@ const ChenyunPlace9 = () => { highlightSelectors={[]} zIndex={10001} > - + 我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他"适合老百姓的要求,做了一点事"的初心。 diff --git a/src/components/layout/clue/ClueContent.tsx b/src/pages/chenyun/reflection/clue/ClueContent.tsx similarity index 100% rename from src/components/layout/clue/ClueContent.tsx rename to src/pages/chenyun/reflection/clue/ClueContent.tsx diff --git a/src/components/layout/clue/DialogueContent.tsx b/src/pages/chenyun/reflection/clue/DialogueContent.tsx similarity index 100% rename from src/components/layout/clue/DialogueContent.tsx rename to src/pages/chenyun/reflection/clue/DialogueContent.tsx diff --git a/src/components/layout/clue/index.tsx b/src/pages/chenyun/reflection/clue/index.tsx similarity index 80% rename from src/components/layout/clue/index.tsx rename to src/pages/chenyun/reflection/clue/index.tsx index 6326b5f..7603667 100644 --- a/src/components/layout/clue/index.tsx +++ b/src/pages/chenyun/reflection/clue/index.tsx @@ -4,6 +4,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { cn, getStaticImage } from "@/lib/utils"; import { Image } from "@/components"; import { useScale } from "@/contexts/ScaleContext"; +import { useAudioManager } from "@/hooks/useAudioManager"; import ClueContent from "./ClueContent"; import { ClueData } from "./types"; @@ -18,12 +19,19 @@ export interface ClueOverlayProps { children?: ReactNode; /** 额外的自定义类 */ className?: string; + /** 线索对应的音频地址(可选) */ + audioSrc?: string; + /** 出现时是否自动播放音频(默认 true) */ + autoPlayAudio?: boolean; + /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ + audioChannelId?: string; } /** * 线索展示框组件 * - 中间内容区域可自定义(文本或其他内容) * - 使用背景资源:components/layout/纸(地图和音频类型除外) + * - 支持音频自动播放,与全局/分组音频互斥 */ const ClueOverlay = ({ visible = true, @@ -31,6 +39,9 @@ const ClueOverlay = ({ clue, children, className, + audioSrc, + autoPlayAudio = true, + audioChannelId, }: ClueOverlayProps) => { const maskSrc = getStaticImage("components/蒙版遮罩"); const backSrc = getStaticImage("components/关闭"); @@ -40,6 +51,13 @@ const ClueOverlay = ({ const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale(); const [mounted, setMounted] = useState(false); + // 音频管理:默认全局互斥,如果传了 audioChannelId 则按 channel 互斥 + const { currentSrc, play, stop } = useAudioManager( + audioChannelId + ? { mode: "channel", channelId: audioChannelId } + : { mode: "global" } + ); + // 判断是否需要显示纸张背景(地图、音频和对话类型不显示纸张背景) const shouldShowPaperBg = clue?.type !== "map" && clue?.type !== "audio" && clue?.type !== "dialogue"; @@ -49,8 +67,28 @@ const ClueOverlay = ({ setMounted(true); }, []); + // 线索显示时自动播放音频 + useEffect(() => { + if (!mounted) return; + if (visible && autoPlayAudio && audioSrc) { + play(audioSrc); + } + }, [visible, mounted, autoPlayAudio, audioSrc, play]); + + // 关闭线索时停止音频(仅当当前播放的是该线索的 audioSrc) + useEffect(() => { + if (!mounted) return; + if (!visible && audioSrc && currentSrc === audioSrc) { + stop(); + } + }, [visible, mounted, audioSrc, currentSrc, stop]); + const handleClose = (e: MouseEvent) => { e.stopPropagation(); + // 关闭时停止音频 + if (audioSrc && currentSrc === audioSrc) { + stop(); + } onClose?.(); }; diff --git a/src/components/layout/clue/types.ts b/src/pages/chenyun/reflection/clue/types.ts similarity index 86% rename from src/components/layout/clue/types.ts rename to src/pages/chenyun/reflection/clue/types.ts index 87df203..7c82d77 100644 --- a/src/components/layout/clue/types.ts +++ b/src/pages/chenyun/reflection/clue/types.ts @@ -26,6 +26,10 @@ export interface AudioClueData { text: string; /** 录音图片资源路径 */ audioImage: string; + /** 录音音频资源路径 */ + audioSrc?: string; + /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ + audioChannelId?: string; } /** 对话线索数据 */ diff --git a/src/pages/chenyun/reflection/constants.tsx b/src/pages/chenyun/reflection/constants.tsx deleted file mode 100644 index b60085d..0000000 --- a/src/pages/chenyun/reflection/constants.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { ReactNode } from "react"; -import { getPublicPath } from "@/lib/utils"; -import type { ClueData } from "@/components/layout"; - -// starName 固定为 chenyun -export const starName = "chenyun"; - -// 欢迎语文案(使用 StudyGuideModal 展示) -export const WELCOME_TEXT = `同学你好,欢迎来到“跨越时空·体悟”学习模块! -请登陆“时空回溯・亲历时代”体验仓,下一站:1949,上海。 -你将扎进“银元之战”的旋涡,踏过“米棉之战”的浪潮,以陈云同志特派员的身份体验陈云如何以智破局,稳稳托住这座城的经济天平。时空门已开,准备好入戏了吗?`; - -// 视频配置 -export const VIDEO_CONFIG = { - yinyuan: { - src: getPublicPath("video/chenyun/chenyun_15.mp4"), - narration: `1949年5月,上海解放,此时上海经济秩序混乱,一些资本家甚至说"共产党是军事一百分,政治八十分,财经打零分。"上海经济旧势力利用人们长期以来形成的担心钞票贬值的心理,大肆贩卖银元,掀起投机风潮。上海证券交易所是投机商的大本营,而大街小巷的银元贩子却更多。原来的一块银元值100人民币,到了6月4日已经涨到1100元,而涨势仍在继续。事实上,银元已经是上海市场的主要货币。有些商号只认银元,把人民币排斥到市场之外。上海,这个全国的经济心脏,能否在新政权手中恢复跳动?一场没有硝烟的经济战争即将打响。面对这硝烟弥漫的经济战线,陈云同志亲自参与指导了同旧经济势力间的"银元之战。你将化身陈云同志的特派员,直面这场'银元之战'。"`, - alert: { - title: '第一幕:"银元之战"--人民币的阵地争夺战', - subtitle: "上海目前出现大肆贩卖银元的情况,请你了解情况!", - }, - }, - mimian: { - src: getPublicPath("video/chenyun/chenyun_16.mp4"), - narration: "", // 没有旁白 - alert: { - title: '第二幕:"米棉之战"--全国一盘棋的战略调度', - subtitle: "上海目前出现米棉物资短缺的情况,请你先了解情况!", - }, - }, -} as const; - -// 线索数据配置 -export const CLUE_DATA: Record = { - // 银元之战 - 线索1 - "yinyuan-clue1": { - type: "text", - title: "线索一", - content: `沪市银元价格连日暴涨, - 5月27日一块银元值100人民币, - 6月3日涨到720元, - 6月4日涨到1100元。`, - }, - // 银元之战 - 线索2 - "yinyuan-clue2": { - type: "text", - title: "线索二", - content: ( -
-
- 南京路的先施、永安、大新、新新等四大公司率先拒收人民币,甚至用银元标价。 -
-
其他商户纷纷仿效,人民币在市场交易中地位下降。
-
- ), - }, - // 米棉之战 - 投机商情报 - "mimian-intelligence": { - type: "text", - title: "投机商情报", - content: `申新财团囤积棉纱3万件, - 扬言"控制纱布就能控制上海"。`, - }, - // 米棉之战 - 全国物资分布 - "mimian-distribution": { - type: "map", - mapImage: "chenyun/reflection/全国物资分布", - }, - // 米棉之战 - 对话线索 - "mimian-dialogue": { - type: "dialogue", - dialogues: [ - { - character: "群众1", - avatar: "chenyun/reflection/群众1", - text: "米价一天涨三次,家里快断粮了", - }, - { - character: "群众2", - avatar: "chenyun/reflection/群众2", - text: "钱庄都在囤媒,冬天没法取暖", - }, - ], - }, - // 银元之战 - 电话线索 - "yinyuan-phone": { - type: "audio", - text: `人民币进不了上海市场,问题很严重。必须让人民币站稳脚跟。`, - audioImage: "chenyun/reflection/录音", - }, -}; diff --git a/src/pages/chenyun/reflection/constants/mimian.ts b/src/pages/chenyun/reflection/constants/mimian.ts index d83952e..ff79a33 100644 --- a/src/pages/chenyun/reflection/constants/mimian.ts +++ b/src/pages/chenyun/reflection/constants/mimian.ts @@ -4,7 +4,7 @@ */ import type { ReactNode } from "react"; import { getPublicPath } from "@/lib/utils"; -import type { ClueData } from "@/components/layout"; +import type { ClueData } from "../clue/types"; // 米棉之战视频配置 export const MIMIAN_VIDEO_CONFIG = { @@ -19,7 +19,7 @@ export const MIMIAN_VIDEO_CONFIG = { // 米棉之战线索数据配置 export const MIMIAN_CLUE_DATA: Record< string, - ClueData & { content?: ReactNode } + ClueData & { content?: ReactNode; audioSrc?: string } > = { // 米棉之战 - 投机商情报 "mimian-intelligence": { diff --git a/src/pages/chenyun/reflection/constants/shared.ts b/src/pages/chenyun/reflection/constants/shared.ts index ab01f3f..80c7b46 100644 --- a/src/pages/chenyun/reflection/constants/shared.ts +++ b/src/pages/chenyun/reflection/constants/shared.ts @@ -7,7 +7,6 @@ export const starName = "chenyun"; // 欢迎语文案(使用 StudyGuideModal 展示) -export const WELCOME_TEXT = `同学你好,欢迎来到跨越时空-体悟学习模块! - -我们即将跨越时空,穿越到"银元之战"和"米棉之战"两个沉浸式场景,来感受陈云同志在1949年的上海是如何应对当时混乱的经济秩序的。`; - +export const WELCOME_TEXT = `同学你好,欢迎来到“跨越时空·体悟”学习模块! +请登陆“时空回溯・亲历时代”体验仓,下一站:1949年,上海。 +你将扎进“银元之战”的旋涡,踏过“米棉之战”的浪潮,以陈云同志特派员的身份体验陈云如何以智破局,稳稳托住这座城的经济天平。时空门已开,准备好入戏了吗?`; diff --git a/src/pages/chenyun/reflection/constants/yinyuan.tsx b/src/pages/chenyun/reflection/constants/yinyuan.tsx index 38ccdb3..fd54af2 100644 --- a/src/pages/chenyun/reflection/constants/yinyuan.tsx +++ b/src/pages/chenyun/reflection/constants/yinyuan.tsx @@ -4,7 +4,7 @@ */ import { type ReactNode } from "react"; import { getPublicPath } from "@/lib/utils"; -import type { ClueData } from "@/components/layout"; +import type { ClueData } from "../clue/types"; // 银元之战视频配置 export const YINYUAN_VIDEO_CONFIG = { @@ -19,7 +19,7 @@ export const YINYUAN_VIDEO_CONFIG = { // 银元之战线索数据配置 export const YINYUAN_CLUE_DATA: Record< string, - ClueData & { content?: ReactNode } + ClueData & { content?: ReactNode; audioSrc?: string } > = { // 银元之战 - 线索1 "yinyuan-clue1": { @@ -48,5 +48,6 @@ export const YINYUAN_CLUE_DATA: Record< type: "audio", text: `人民币进不了上海市场,问题很严重。必须让人民币站稳脚跟。`, audioImage: "chenyun/reflection/录音", + audioSrc: getPublicPath("audio/chenyun/mp3_chenyun_30.mp3"), }, }; diff --git a/src/pages/chenyun/reflection/index.tsx b/src/pages/chenyun/reflection/index.tsx index e0743ab..67147a2 100644 --- a/src/pages/chenyun/reflection/index.tsx +++ b/src/pages/chenyun/reflection/index.tsx @@ -5,19 +5,25 @@ import { Image, StudyGuideModal } from "@/components"; import ErrorAlert from "@/components/layout/alert/error-alert"; import TitleAlert from "@/components/layout/alert/title-alert"; -import ClueOverlay from "@/components/layout/clue"; -import type { ClueData } from "@/components/layout"; -import { getBackgroundImage, getStaticImage } from "@/lib/utils"; +import ClueOverlay from "@/pages/chenyun/reflection/clue"; +import type { ClueData } from "./clue/types"; +import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { WELCOME_TEXT, starName } from "./constants"; +import { + WELCOME_TEXT, + starName, + YINYUAN_VIDEO_CONFIG, + MIMIAN_VIDEO_CONFIG, + CLUE_DATA, +} from "./constants"; import { useClueStates } from "./shared/useClueStates"; import { VideoHandler } from "./shared/VideoHandler"; import { YinyuanBattle } from "./yinyuan/YinyuanBattle"; import { MimianBattle } from "./mimian/MimianBattle"; import type { FileType } from "./shared/types"; -import { YINYUAN_VIDEO_CONFIG } from "./constants/yinyuan"; -import { MIMIAN_VIDEO_CONFIG } from "./constants/mimian"; +// import { } from "./constants/yinyuan"; +// import { } from "./constants/mimian"; import { useChenyunStore } from "@/store/chenyunStore"; const ChenyunReflection = () => { @@ -81,6 +87,9 @@ const ChenyunReflection = () => { // ========== 银元之战和米棉之战状态(从 store 获取) ========== const { isYinyuanSolved, isMimianSolved } = useChenyunStore(); + //线索音频 + const [clueAudioSrc, setClueAudioSrc] = useState(""); + // ========== 事件处理函数 ========== /** @@ -173,8 +182,13 @@ const ChenyunReflection = () => { * @param clueKey 线索键名 */ const handleClueClick = (clue: ClueData, clueKey: string) => { + console.log(clueKey); setCurrentClue(clue); setCurrentClueKey(clueKey); + // 从合并后的 CLUE_DATA 中直接查找 audioSrc + const audioSrc = + (CLUE_DATA[clueKey] as { audioSrc?: string })?.audioSrc || ""; + setClueAudioSrc(audioSrc); setShowClueOverlay(true); }; @@ -259,6 +273,7 @@ const ChenyunReflection = () => { onContinue={() => { setShowWelcomeGuide(false); }} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_28.mp3")} > {WELCOME_TEXT} @@ -326,6 +341,7 @@ const ChenyunReflection = () => { visible={showClueOverlay} clue={currentClue} onClose={handleClueOverlayClose} + audioSrc={clueAudioSrc} /> )} diff --git a/src/pages/chenyun/reflection/mimian/MimianBattle.tsx b/src/pages/chenyun/reflection/mimian/MimianBattle.tsx index aa65040..0416fc5 100644 --- a/src/pages/chenyun/reflection/mimian/MimianBattle.tsx +++ b/src/pages/chenyun/reflection/mimian/MimianBattle.tsx @@ -11,7 +11,7 @@ import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; import { usePuzzleStore } from "@/store/puzzleStore"; import { starName } from "../constants"; import type { FileType } from "../shared/types"; -import type { ClueData } from "@/components/layout"; +import type { ClueData } from "../clue/types"; export interface MimianBattleProps { /** 当前展开的文件 */ @@ -85,7 +85,7 @@ export const MimianBattle = ({ state.setShowMimianQuiz2(false); state.setShowMimianSuccessAlert(true); state.setMimianSuccessAlertContent( - `恭喜你,选择正确!结合当时市场情况,陈云同志在会上做出了"全国一盘棋"的指示。全国统筹调运粮食、棉花、煤炭填补缺口,筑牢物资基础。关键节点集中抛售,让跟风囤货的投机商无力承接,因亏本和高利贷纷纷破产,快速平抑物价、稳固人民币地位。` + `结合当时市场情况,陈云同志在会上做出了"全国一盘棋"的指示。全国统筹调运粮食、棉花、煤炭填补缺口,筑牢物资基础。关键节点集中抛售,让跟风囤货的投机商无力承接,因亏本和高利贷纷纷破产,快速平抑物价、稳固人民币地位。` ); } // 选错时,错误信息已经在题目面板中通过 onWrongSelect 显示,不需要额外的 SuccessAlert @@ -125,15 +125,7 @@ export const MimianBattle = ({ if (isCorrect) { state.setShowMimianQuiz3(false); state.setShowMimianSuccessAlert(true); - state.setMimianSuccessAlertContent( - "恭喜你,选择正确!地方配合,调运按时推进。" - ); - } else { - state.setShowMimianQuiz3(false); - state.setShowMimianSuccessAlert(true); - state.setMimianSuccessAlertContent( - "很抱歉,你选错了!地方抵触,调运延误多天。请你再思考一下吧!" - ); + state.setMimianSuccessAlertContent("地方配合,调运按时推进。"); } }; @@ -155,15 +147,13 @@ export const MimianBattle = ({ if (isCorrect) { state.setShowMimianQuiz4(false); state.setShowMimianSuccessAlert(true); - state.setMimianSuccessAlertContent( - "恭喜你,选择正确!棉农支持,调运量足额完成。" - ); + state.setMimianSuccessAlertContent("棉农支持,调运量足额完成。"); } else { - state.setShowMimianQuiz4(false); - state.setShowMimianSuccessAlert(true); - state.setMimianSuccessAlertContent( - "很抱歉,你选错了!棉农抵触,调运量减少。请你再思考一下吧!" - ); + // state.setShowMimianQuiz4(false); + // state.setShowMimianSuccessAlert(true); + // state.setMimianSuccessAlertContent( + // "很抱歉,你选错了!棉农抵触,调运量减少。请你再思考一下吧!" + // ); } }; @@ -197,13 +187,7 @@ export const MimianBattle = ({ state.setShowMimianQuiz5(false); state.setShowMimianSuccessAlert(true); state.setMimianSuccessAlertContent( - "恭喜你,选择正确!这契合米棉之战史实,多地同步大量抛售,让借高利贷囤货的投机商措手不及。连续抛售使其无力承接,只得亏本抛售,最终纷纷破产,快速稳住物价。" - ); - } else { - state.setShowMimianQuiz5(false); - state.setShowMimianSuccessAlert(true); - state.setMimianSuccessAlertContent( - "很抱歉,你选错了!投机商此前曾吃下政府平价抛售物资获利。逐步降价加舆论引导难让其信服,他们会持续囤货,政府储备会被消耗,反而让投机商掌控节奏,无法遏制物价上涨。请你再思考一下吧!" + "这契合米棉之战史实,多地同步大量抛售,让借高利贷囤货的投机商措手不及。连续抛售使其无力承接,只得亏本抛售,最终纷纷破产,快速稳住物价。" ); } }; diff --git a/src/pages/chenyun/reflection/mimian/MimianClues.tsx b/src/pages/chenyun/reflection/mimian/MimianClues.tsx index 85a13af..ad930ea 100644 --- a/src/pages/chenyun/reflection/mimian/MimianClues.tsx +++ b/src/pages/chenyun/reflection/mimian/MimianClues.tsx @@ -6,7 +6,7 @@ import { Image } from "@/components"; import { cn, getStaticImage } from "@/lib/utils"; import { ClueItem } from "../shared/ClueItem"; import type { FileType } from "../shared/types"; -import type { ClueData } from "@/components/layout"; +import type { ClueData } from "../clue/types"; import { MIMIAN_CLUE_DATA } from "../constants/mimian"; export interface MimianCluesProps { @@ -127,4 +127,3 @@ export const MimianClues = ({ ); }; - diff --git a/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx b/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx index 31d494c..09351e2 100644 --- a/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx +++ b/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx @@ -2,9 +2,9 @@ * 米棉之战题目组件 * 包含单选题1-5、旁白1-3、成功提示框、结束语 */ -import { Narration, SingleChoiceQuizPanel } from "@/components"; +import { Narration, SingleChoiceQuizPanel, EndingOverlay } from "@/components"; import SuccessAlert from "@/components/layout/alert/success-alert"; -import { EndingOverlay } from "../shared/EndingOverlay"; +import { getPublicPath } from "@/lib/utils"; export interface MimianQuizzesProps { /** 是否显示单选题1 */ @@ -121,6 +121,7 @@ export const MimianQuizzes = ({ visible={showNarration1} onClose={onNarration1Close} enableAnimation={false} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_32.mp3")} /> {/* 米棉之战 - 单选题2(图3) */} @@ -152,7 +153,7 @@ export const MimianQuizzes = ({ /> {/* 米棉之战 - 错误提示框(单选题2答错后) */} - + /> */} {/* 米棉之战 - 旁白2(图4) */} {/* 米棉之战 - 单选题3(图5) */} @@ -183,7 +185,7 @@ export const MimianQuizzes = ({ }, ]} correctAnswer="B" - errorMessage="地方抵触,调运延误多天" + errorMessage="地方抵触,调运延误多天。请你再思考一下吧!" onSubmit={onQuiz3Submit} /> @@ -194,11 +196,7 @@ export const MimianQuizzes = ({ (successAlertContent.includes("地方配合") || successAlertContent.includes("地方抵触")) } - title={ - successAlertContent.includes("恭喜") - ? "恭喜你,选择正确!" - : "很抱歉,你选错了!" - } + title={"恭喜你,选择正确!"} content={successAlertContent} onContinue={() => { const isCorrect = successAlertContent.includes("地方配合"); @@ -218,7 +216,7 @@ export const MimianQuizzes = ({ }, ]} correctAnswer="B" - errorMessage="棉农抵触,调运量减少" + errorMessage="棉农抵触,调运量减少。请你再思考一下吧!" onSubmit={onQuiz4Submit} /> @@ -229,11 +227,7 @@ export const MimianQuizzes = ({ (successAlertContent.includes("棉农支持") || successAlertContent.includes("棉农抵触")) } - title={ - successAlertContent.includes("恭喜") - ? "恭喜你,选择正确!" - : "很抱歉,你选错了!" - } + title={"恭喜你,选择正确!"} content={successAlertContent} onContinue={() => { const isCorrect = successAlertContent.includes("棉农支持"); @@ -247,6 +241,7 @@ export const MimianQuizzes = ({ visible={showNarration3} onClose={onNarration3Close} enableAnimation={false} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_34.mp3")} /> {/* 米棉之战 - 单选题5(图8) */} @@ -276,11 +271,7 @@ export const MimianQuizzes = ({ (successAlertContent.includes("这契合米棉之战史实") || successAlertContent.includes("投机商此前曾吃下")) } - title={ - successAlertContent.includes("恭喜") - ? "恭喜你,选择正确!" - : "很抱歉,你选错了!" - } + title={"恭喜你,选择正确!"} content={successAlertContent} onContinue={() => { const isEndSuccess = @@ -296,6 +287,8 @@ export const MimianQuizzes = ({ text={ '从"银元之战"查封投机枢纽、稳固人民币根基,到"米棉之战"全国统筹调运、集中抛售击溃投机势力,这场经济战线的"淮海战役",不仅平定了物价、安定了民心,更印证了"全国一盘棋"的战略智慧与制度优势。当市场秩序重归稳定,新生政权在经济领域站稳脚跟,这份由陈云同志等老一辈革命家铸就的治理经验,也为新时代经济建设埋下了思想伏笔,等待我们在传承中探索新的实践路径。' } + animationSpeed={200} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_35.mp3")} /> ); diff --git a/src/pages/chenyun/reflection/shared/VideoHandler.tsx b/src/pages/chenyun/reflection/shared/VideoHandler.tsx index 5388ce4..150b8d8 100644 --- a/src/pages/chenyun/reflection/shared/VideoHandler.tsx +++ b/src/pages/chenyun/reflection/shared/VideoHandler.tsx @@ -7,6 +7,7 @@ import Alert from "@/components/layout/alert/title-alert"; import type { FileType } from "./types"; import { YINYUAN_VIDEO_CONFIG } from "../constants/yinyuan"; import { MIMIAN_VIDEO_CONFIG } from "../constants/mimian"; +import { getPublicPath } from "@/lib/utils"; export interface VideoHandlerProps { /** 当前播放的视频类型 */ @@ -70,7 +71,9 @@ export const VideoHandler = ({ text={narrationText} visible={showNarration} onClose={onNarrationClose} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_29.mp3")} enableAnimation={playingVideo === "yinyuan"} + animationSpeed={200} /> {/* 提示框 */} @@ -85,4 +88,3 @@ export const VideoHandler = ({ ); }; - diff --git a/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx b/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx index 19ce3b6..1e94f3e 100644 --- a/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx +++ b/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx @@ -12,7 +12,7 @@ import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; import { usePuzzleStore } from "@/store/puzzleStore"; import { starName } from "../constants"; import type { FileType } from "../shared/types"; -import type { ClueData } from "@/components/layout"; +import type { ClueData } from "../clue/types"; export interface YinyuanBattleProps { /** 当前展开的文件 */ diff --git a/src/pages/chenyun/reflection/yinyuan/YinyuanClues.tsx b/src/pages/chenyun/reflection/yinyuan/YinyuanClues.tsx index e1db459..e2e880f 100644 --- a/src/pages/chenyun/reflection/yinyuan/YinyuanClues.tsx +++ b/src/pages/chenyun/reflection/yinyuan/YinyuanClues.tsx @@ -6,7 +6,7 @@ import { Image } from "@/components"; import { cn, getStaticImage } from "@/lib/utils"; import { ClueItem } from "../shared/ClueItem"; import type { FileType } from "../shared/types"; -import type { ClueData } from "@/components/layout"; +import type { ClueData } from "../clue/types"; import { YINYUAN_CLUE_DATA } from "../constants/yinyuan"; export interface YinyuanCluesProps { @@ -120,4 +120,3 @@ export const YinyuanClues = ({ ); }; - diff --git a/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx b/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx index 03aeadd..1ce13b6 100644 --- a/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx +++ b/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx @@ -4,6 +4,7 @@ */ import { Narration, MultipleChoiceQuizPanel } from "@/components"; import SuccessAlert from "@/components/layout/alert/success-alert"; +import { getPublicPath } from "@/lib/utils"; export interface YinyuanQuizzesProps { /** 是否显示多选题1 */ @@ -70,6 +71,7 @@ export const YinyuanQuizzes = ({ visible={showNarration2} onClose={onNarration2Close} enableAnimation={false} + audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_31.mp3")} /> {/* 多选题2(图3)- 关闭旁白后显示 */} diff --git a/src/pages/huangyanpei/internalization.tsx b/src/pages/huangyanpei/internalization.tsx index 7fbf165..b45d04c 100644 --- a/src/pages/huangyanpei/internalization.tsx +++ b/src/pages/huangyanpei/internalization.tsx @@ -36,31 +36,31 @@ const starName = "huangyanpei"; // 锚点数据:根据视频时长调整 time const QUIZ_ANCHORS: QuizAnchor[] = [ - { - id: "quiz-1", - time: 96, // 1:36 - title: "题目1", - prompt: - "当年黄炎培先生创办中华职业学校,开设的专业都是当时中国最需要的实业领域,分别是( )。", - type: "multiple", - options: [ - { value: "A", label: "铁工", isCorrect: true }, - { - value: "B", - label: "自动控制", - }, - { value: "C", label: "木工", isCorrect: true }, - { value: "D", label: "纽扣科", isCorrect: true }, - ], - explanation: - "自动控制”这一工程理论框架成形于20世纪中叶,70年代实现集中控制,在民国时期,没有相关学科基础、设备与技术条件", - }, + // { + // id: "quiz-1", + // time: 96, // 1:36 + // title: "题目1", + // prompt: + // "当年黄炎培先生创办中华职业学校,开设的专业都是当时中国最需要的实业领域,分别是( )。", + // type: "multiple", + // options: [ + // { value: "A", label: "铁工", isCorrect: true }, + // { + // value: "B", + // label: "自动控制", + // }, + // { value: "C", label: "木工", isCorrect: true }, + // { value: "D", label: "纽扣科", isCorrect: true }, + // ], + // explanation: + // "自动控制”这一工程理论框架成形于20世纪中叶,70年代实现集中控制,在民国时期,没有相关学科基础、设备与技术条件", + // }, { id: "quiz-2", time: 221, // 3:41 title: "题目2", prompt: - "黄炎培先生主导设计的课程与现在中职学校强调的理实一体这一理念相通是( )。", + "黄炎培先生主导设计的课程与现在中职学校强调的理实一体这一理念相通是:", type: "multiple", options: [ { @@ -96,9 +96,9 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ { value: "特斯拉", label: "特斯拉" }, { value: "比亚迪", label: "比亚迪" }, { value: "华为", label: "华为" }, - { value: "上海电气", label: "上海电气" }, + { value: "江南造船", label: "江南造船" }, { value: "光明食品", label: "光明食品" }, - { value: "拼多多", label: "拼多多" }, + { value: "C919飞机", label: "C919飞机" }, ], blanks: [ { id: "blank-1", label: "国产企业1" }, @@ -108,29 +108,29 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ { id: "blank-5", label: "国产企业5" }, ], }, - { - id: "quiz-4", - time: 467, // 7:47 - title: "题目4", - prompt: - '当年现在的中职教育一直在"因时求变",许多专业的新增和关联正确的是( )。', - type: "multiple", - options: [ - { value: "A", label: "汽修专业→学新能源汽车检测与维修", isCorrect: true }, - { - value: "B", - label: "会计专业→要掌握大数据财务和智能记账", - isCorrect: true, - }, - { value: "C", label: "国货推销→跨境电商、直播带货", isCorrect: true }, - { - value: "D", - label: "美术设计与制作→影像与影视技术", - }, - ], - explanation: - "美术设计与制作专业侧重于平面设计,而影像与影视技术专业侧重于影视特效后期,两者的专业核心课程涉及的软硬件也有差异", - }, + // { + // id: "quiz-4", + // time: 467, // 7:47 + // title: "题目4", + // prompt: + // '当年现在的中职教育一直在"因时求变",许多专业的新增和关联正确的是( )。', + // type: "multiple", + // options: [ + // { value: "A", label: "汽修专业→学新能源汽车检测与维修", isCorrect: true }, + // { + // value: "B", + // label: "会计专业→要掌握大数据财务和智能记账", + // isCorrect: true, + // }, + // { value: "C", label: "国货推销→跨境电商、直播带货", isCorrect: true }, + // { + // value: "D", + // label: "美术设计与制作→影像与影视技术", + // }, + // ], + // explanation: + // "美术设计与制作专业侧重于平面设计,而影像与影视技术专业侧重于影视特效后期,两者的专业核心课程涉及的软硬件也有差异", + // }, ]; const HuangyanpeiInternalization = () => { @@ -203,7 +203,7 @@ const HuangyanpeiInternalization = () => { if (hasInvalidOption) { setDragQuizErrors((prev) => ({ ...prev, - [item.id]: "特斯拉不是中国国产企业哦~", + [item.id]: "特斯拉为外资企业哦~", })); return; } @@ -214,9 +214,9 @@ const HuangyanpeiInternalization = () => { const validAnswers = [ "比亚迪", "华为", - "上海电气", + "江南造船", "光明食品", - "拼多多", + "C919飞机", ]; const filledAnswers = val.map((v) => v.optionValue); const isCorrect = filledAnswers.every((answer) => @@ -240,9 +240,9 @@ const HuangyanpeiInternalization = () => { ({ value: opt.value, label: opt.label, @@ -292,7 +292,7 @@ const HuangyanpeiInternalization = () => { ({ value: opt.value, label: `${opt.value}. ${opt.label}`, @@ -381,11 +381,9 @@ const HuangyanpeiInternalization = () => { setPostGuideVisible(true); if (audioRef.current) { audioRef.current.currentTime = 0; - audioRef.current - .play() - .catch((error) => { - console.warn("背景音乐播放失败:", error); - }); + audioRef.current.play().catch((error) => { + console.warn("背景音乐播放失败:", error); + }); } }} /> @@ -404,13 +402,13 @@ const HuangyanpeiInternalization = () => { showMask={false} > - 小沪,刚刚结束的"任之成长记"太有意思了!我变成百年前的川沙少年选课程、做手工、设计国货推销方案,好像真的走进了黄炎培先生的职教世界。 + 小沪,刚刚结束的"小职成长记"太有意思了!我变成百年前的川沙少年选课程、做手工、设计国货推销方案,好像真的走进了黄炎培先生的职教世界。 但我还有一个疑问:先生当年倡导的"手脑并用、工学结合",放在我们现在的中职学习中,到底有什么样的意义呢? - 这个问题问到点子上啦!黄炎培先生的职教思想,从来不是尘封的历史,而是活在当下的行动指南。今天我们特别邀请到中华职业教育社第十三届理事会常务理事、中华职业学校党总支书记、校长黄玉璟,让她带我们解码先生精神的时代密码! + 这个问题问到点子上啦!黄炎培先生的职教思想,从来不是尘封的历史,而是活在当下的行动指南。今天我们特别邀请到中华职业教育社第十三届理事会常务理事、中华职业学校党总支副书记、校长黄玉璟,让她带我们解码先生精神的时代密码! @@ -452,8 +450,7 @@ const HuangyanpeiInternalization = () => { navigate(`/${moduleName}/${starName}/home`); }} > - 恭喜你已经顺利完成了黄炎培故居的学习,至此,你已经完成了本课程所有故居的学习, - 希望你能有所收获,学习八位名人的优秀品质和精神,在新时代为中华民族伟大复兴发光发热! + 恭喜你已经顺利完成了黄炎培故居的学习,请继续前往下一个故居吧! ); diff --git a/src/pages/huangyanpei/place/place1.tsx b/src/pages/huangyanpei/place/place1.tsx index a663469..30aa9f1 100644 --- a/src/pages/huangyanpei/place/place1.tsx +++ b/src/pages/huangyanpei/place/place1.tsx @@ -88,7 +88,7 @@ const HuangyanpeiPlace1 = () => { {completed && ( { navigate(`/${moduleName}/${starName}/place2`); @@ -154,11 +154,11 @@ const HuangyanpeiPlace1 = () => { onContinue={() => setShowAfterDialogGuide(false)} ailgn="left" > - 1、场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态; + 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {"\n"} - 2、在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点击查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习; + 2.在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点击查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习。 {"\n"} - 3、所有动态光点的展品全部学习后才能进入下一个场景学习; + 3.所有动态光点的展品全部学习后才能进入下一个场景学习。 ); diff --git a/src/pages/huangyanpei/place/place3.tsx b/src/pages/huangyanpei/place/place3.tsx index 0191552..7298db0 100644 --- a/src/pages/huangyanpei/place/place3.tsx +++ b/src/pages/huangyanpei/place/place3.tsx @@ -32,8 +32,8 @@ const HuangyanpeiPlace3 = () => { ); const [visited, setVisited] = useState>(new Set()); const { isEnabled } = useCursorController({ - initialEnabled: [1], - dependencies: { 2: [1], 3: [1, 2] }, + initialEnabled: [3], + dependencies: { 2: [3], 1: [2] }, visited, }); diff --git a/src/pages/huangyanpei/place/place4.tsx b/src/pages/huangyanpei/place/place4.tsx index 359c9b9..fd3b704 100644 --- a/src/pages/huangyanpei/place/place4.tsx +++ b/src/pages/huangyanpei/place/place4.tsx @@ -31,16 +31,15 @@ const HuangyanpeiPlace4 = () => { moduleName: string; }>(); - const [activeExhibitId, setActiveExhibitId] = useState< - 1 | 2 | 3 | 4 | 5 | null - >(null); + const [activeExhibitId, setActiveExhibitId] = useState<1 | 3 | 4 | 5 | null>( + null + ); const [visited, setVisited] = useState>(new Set()); const { isEnabled } = useCursorController({ - initialEnabled: [1], + initialEnabled: [3, 5], dependencies: { - 3: [1], - 4: [3], - 5: [4], + 1: [3, 5], + 4: [1], // 2 已注释,不计入顺序 }, visited, @@ -66,7 +65,7 @@ const HuangyanpeiPlace4 = () => { const [showSuccessAlert, setShowSuccessAlert] = useState(false); const videoRef = useRef(null); - const handleOpenExhibit = (index: 1 | 2 | 3 | 4 | 5) => { + const handleOpenExhibit = (index: 1 | 3 | 4 | 5) => { setActiveExhibitId(index); if (index === 1) { setGuide1Step(0); diff --git a/src/pages/huangyanpei/place/place6.tsx b/src/pages/huangyanpei/place/place6.tsx index 43178ff..72d9c4a 100644 --- a/src/pages/huangyanpei/place/place6.tsx +++ b/src/pages/huangyanpei/place/place6.tsx @@ -9,7 +9,7 @@ import { } from "@/components"; import { getBackgroundImage, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; -import { EndingOverlay } from "@/pages/chenyun/reflection/shared/EndingOverlay"; +import { EndingOverlay } from "@/components/overlay/EndingOverlay"; const starName = "huangyanpei"; diff --git a/src/pages/huangyanpei/reflection/components/dialog/index.tsx b/src/pages/huangyanpei/reflection/components/dialog/index.tsx new file mode 100644 index 0000000..d6ad251 --- /dev/null +++ b/src/pages/huangyanpei/reflection/components/dialog/index.tsx @@ -0,0 +1,225 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { cn, getStaticImage } from "@/lib/utils"; +import { Image } from "@/components"; +import { SuccessAlert } from "@/components/layout"; + +export interface QuizDialogProps { + /** 是否可见 */ + visible?: boolean; + /** 关闭回调 */ + onClose?: () => void; + /** 完成回调 */ + onComplete?: () => void; +} + +/** + * 喇叭考题弹窗 + */ +const QuizDialog = ({ + visible = true, + onClose, + onComplete, +}: QuizDialogProps) => { + const maskSrc = getStaticImage("components/蒙版遮罩"); + const backSrc = getStaticImage("components/关闭"); + + const [mounted, setMounted] = useState(false); + const [successVisible, setSuccessVisible] = useState(false); + + // SSR 兼容 + useEffect(() => { + setMounted(true); + }, []); + + const handleClose = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onClose?.(); + }, + [onClose] + ); + + // 关闭成功提示 + const handleSuccessContinue = useCallback(() => { + setSuccessVisible(false); + // 关闭提示后,完成并关闭弹窗 + onComplete?.(); + onClose?.(); + }, [onComplete, onClose]); + + // 可选话术选项 + const options = useMemo( + () => [ + { + value: "option1", + label: "加入义勇军(军事训练)", + }, + { + value: "option2", + label: "制作军需物资", + }, + { + value: "option3", + label: "宣传抗日募捐", + }, + ], + [] + ); + + // 样式 + const overlayStyle = useMemo( + () => ({ + position: "fixed", + inset: 0, + zIndex: 9999, + pointerEvents: "none", + }), + [] + ); + + if (!mounted) return null; + + const content = ( + + {visible && ( + +
+ {/* 背景蒙版 */} + {maskSrc && ( + + )} + + {/* 返回按钮 */} + {onClose && ( + 返回 + )} + + {/* 内容区域 */} +
+
+ {/* 顶部:题目文本和毛巾区域 */} +
+ {/* 题目文本 */} + +
+
+ 【单选】 +
+
+ 国难当头,黄先生说“职教救国”,中华职校的师生都行动起来了!你要加入哪支爱国队伍? +
+
+ + {/* 右侧:毛巾区域 */} +
+ +
+
+ + {/* 底部:可选话术区域 - 使用 DragFill */} +
+
答题区
+ +
+ {options.map((option, index) => ( +
{ + setSuccessVisible(true); + }} + > + {index + 1}. {option.label} +
+ ))} +
+ + {/* { + const index = options.findIndex( + (opt) => opt.value === option.value + ); + return ( +
+ {index + 1}. {option.label} +
+ ); + }} + renderDragging={(option) => { + return ( +
+ {option.label} +
+ ); + }} + renderBlank={() => { + // 返回隐藏元素,实际的空白区域在页面中通过 data-blank-id 定位 + return
; + }} + /> */} +
+
+
+ + {/* 成功提示 */} + +
+ + )} + + ); + + return content; +}; + +export default QuizDialog; diff --git a/src/pages/huangyanpei/reflection/components/index.ts b/src/pages/huangyanpei/reflection/components/index.ts index 48068ca..8735c2d 100644 --- a/src/pages/huangyanpei/reflection/components/index.ts +++ b/src/pages/huangyanpei/reflection/components/index.ts @@ -2,3 +2,5 @@ export { default as QuestionModal } from "./question-modal"; export type { QuestionModalProps } from "./question-modal"; export { BambooPuzzleGame } from "./game"; + +export { default as QuizDialog } from "./dialog"; diff --git a/src/pages/huangyanpei/reflection/scene1.tsx b/src/pages/huangyanpei/reflection/scene1.tsx index f341931..409baa6 100644 --- a/src/pages/huangyanpei/reflection/scene1.tsx +++ b/src/pages/huangyanpei/reflection/scene1.tsx @@ -77,9 +77,9 @@ const HuangyanpeiReflectionScene1 = () => { setShowGuideModal(false); }} > - 你好呀!现在是1903年,欢迎你进入川沙小学堂,请开启你的求学第一课吧。 - {"\n"} - 接下来你将在“任之”校长所在的学校学习,根据所知和所学,努力完成校长给你的考验,完成全部任务后,可进入到下一阶段的学习。 + 同学你好,欢迎来到“跨越时空·体悟”学习模块!{"\n"} + 请登陆“时空回溯・亲历时代”体验仓,下一站:1903,川沙小学堂。{"\n"} + 你将在“任之”校长所在的学校学习,根据所知和所学,努力完成校长给你的考验,完成全部任务后,可进入到下一阶段的学习。时空门已开,请做好准备迎接考验吧! - setShowErrorAlert(false)} - /> + /> */}
)} diff --git a/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx b/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx index 66efd60..fee8c76 100644 --- a/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx +++ b/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx @@ -115,7 +115,7 @@ const ChoiceDialog2 = ({ onContinue={() => setShowGuide(false)} verticalAlign="center" > - 在浦东中学,你可以选未来的方向啦!不管选哪个,黄先生都会支持,因为“三百六十行,行行出状元” + 在浦东中学,你可以选未来的方向啦!不管选哪个,黄先生都会支持,因为“三百六十行,行行出状元”! {/* 返回按钮 */} @@ -190,7 +190,8 @@ const ChoiceDialog2 = ({ diff --git a/src/pages/huangyanpei/reflection/scene3.tsx b/src/pages/huangyanpei/reflection/scene3.tsx index 2fba948..455548b 100644 --- a/src/pages/huangyanpei/reflection/scene3.tsx +++ b/src/pages/huangyanpei/reflection/scene3.tsx @@ -6,6 +6,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import ViseAssemblyDialog from "./scene3/ViseAssemblyDialog"; import type { VideoRef } from "@/components/video"; +import { QuizDialog } from "./components"; const starName = "huangyanpei"; @@ -15,6 +16,9 @@ const HuangyanpeiReflectionScene3 = () => { moduleName: string; }>(); + //喇叭考题弹窗 + const [showQuizDialog, setShowQuizDialog] = useState(false); + const [showGuide1, setShowGuide1] = useState(true); const [showDialog, setShowDialog] = useState(false); const [showQuiz, setShowQuiz] = useState(false); @@ -30,11 +34,11 @@ const HuangyanpeiReflectionScene3 = () => { } const progressCount = visited.size; - const total = 2; + const total = 3; const completed = progressCount >= total; const subtitle = completed - ? "探索中华职业学院·立业之本·铁工科(已完成)" - : `探索中华职业学院·立业之本·铁工科(${progressCount}/${total})`; + ? "探索中华职业学校·立业之本·铁工科(已完成)" + : `探索中华职业学校·立业之本·铁工科(${progressCount}/${total})`; const subtitleClassName = completed ? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]" : undefined; @@ -64,6 +68,15 @@ const HuangyanpeiReflectionScene3 = () => { }} /> + { + setShowQuizDialog(true); + }} + /> + {/* 第一段引导 */} { > 终于到中华职业学校啦!这里是中国近代职教的摇篮,"免学费、包分配",还能学真手艺,快来练就一技之长。 {"\n"} - 你选了学技术,那就来挑战制作中华职校的"明星产品"——台钳吧!当年这台钳还拿过巴拿马万国博览会金奖呢 + 你选了学技术,那就来挑战制作中华职校的"明星产品"——台钳吧!当年这台钳还拿过巴拿马万国博览会金奖呢! {/* 互动展品光标:组装台钳互动 */} @@ -120,7 +133,7 @@ const HuangyanpeiReflectionScene3 = () => { {/* 多选题答题框 */} { { value: "D", label: "D. 铁锤" }, ]} correctAnswer={["A", "C", "D"]} - errorMessage="毛笔是写字的哦! 台钳是铁制品,得用锯子、锉刀、铁锤这些工具,再选一次。" + errorMessage={(selectedValues) => { + // 如果选择了B,显示包含"毛笔是写字的哦"的提示 + if (selectedValues.includes("B")) { + return "毛笔是写字的哦!台钳是铁制品,得用锯子、锉刀、铁锤这些工具,再选一次吧!"; + } + // 其他情况(单选A或C或D,或者AC、CD、AD等组合) + return "台钳是铁制品,得用锯子、锉刀、铁锤这些工具,再选一次吧!"; + }} minSelections={1} onSubmit={(values, isCorrect) => { if (isCorrect) { @@ -194,6 +214,15 @@ const HuangyanpeiReflectionScene3 = () => { }} /> )} + + setShowQuizDialog(false)} + onComplete={() => { + setShowQuizDialog(false); + setVisited((prev) => new Set(prev).add(3)); + }} + /> ); }; diff --git a/src/pages/huangyanpei/reflection/scene4.tsx b/src/pages/huangyanpei/reflection/scene4.tsx index 757b6a0..61e15f8 100644 --- a/src/pages/huangyanpei/reflection/scene4.tsx +++ b/src/pages/huangyanpei/reflection/scene4.tsx @@ -6,6 +6,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import TowelSalesDialog from "./scene4/TowelSalesDialog"; import type { VideoRef } from "@/components/video"; +import { QuizDialog } from "./components"; const starName = "huangyanpei"; @@ -24,6 +25,9 @@ const HuangyanpeiReflectionScene4 = () => { const videoRef = useRef(null); const [showGuide1, setShowGuide1] = useState(true); + //喇叭考题弹窗 + const [showQuizDialog, setShowQuizDialog] = useState(false); + // 处理成功提示的继续和关闭 const handleSuccessAlertContinue = useCallback(() => { setShowSuccessAlert(false); @@ -50,7 +54,7 @@ const HuangyanpeiReflectionScene4 = () => { } const progressCount = visited.size; - const total = 2; + const total = 3; const completed = progressCount >= total; const subtitle = completed ? "探索中华职业学院·立业之本·商科(已完成)" @@ -75,6 +79,15 @@ const HuangyanpeiReflectionScene4 = () => { className="absolute top-10 left-1/2 -translate-x-1/2" /> + { + setShowQuizDialog(true); + }} + /> + { {/* 选择题1:中华职校商科的核心教学目标 */} { }, ]} correctAnswer={["B", "C"]} - errorMessage='不对哦! 黄先生倡导"商科要为民族工商业服务", 商科是要培养诚信经营、助力国货发展的人才, 可不能想着投机发财呀, 再选一次;' + errorMessage="不对哦!黄先生倡导“商科要为民族工商业服务”,商科是要培养诚信经营、助力国货发展的人才,可不能想着投机发财呀,再选一次吧!" minSelections={1} onSubmit={(values: string[], isCorrect: boolean) => { if (isCorrect) { @@ -130,9 +143,6 @@ const HuangyanpeiReflectionScene4 = () => { setShowSuccessAlert(true); } }} - onError={(_error: string) => { - // 答题错误时的处理(组件内部已显示错误信息) - }} /> {/* 答题成功提示 */} @@ -178,7 +188,7 @@ const HuangyanpeiReflectionScene4 = () => { > 终于到中华职业学校啦!这里是中国近代职教的摇篮,“免学费、包分配”,还能学真手艺,快来练就一技之长。 {"\n"} - 现在要完成你的第一个任务:推销中华职校生产的“国货精品”——手工纽扣和纯棉毛巾!当年学生们就是带着这些国货,走街串巷做推销呢 + 现在要完成你的第一个任务:推销中华职校生产的“国货精品”——手工纽扣和纯棉毛巾!当年学生们就是带着这些国货,走街串巷做推销呢! {showVideo && ( @@ -195,6 +205,15 @@ const HuangyanpeiReflectionScene4 = () => { }} /> )} + + setShowQuizDialog(false)} + onComplete={() => { + setShowQuizDialog(false); + setVisited((prev) => new Set(prev).add(3)); + }} + /> ); }; diff --git a/src/pages/huangyanpei/reflection/scene4/TowelSalesDialog.tsx b/src/pages/huangyanpei/reflection/scene4/TowelSalesDialog.tsx index 325a59f..e893579 100644 --- a/src/pages/huangyanpei/reflection/scene4/TowelSalesDialog.tsx +++ b/src/pages/huangyanpei/reflection/scene4/TowelSalesDialog.tsx @@ -209,7 +209,6 @@ const TowelSalesDialog = ({
{/* 题目文本 */}
-
题目文本
从以下话术里,挑选3句组成你的推销语 (拖拽至对话框)
diff --git a/src/pages/luxun/place/place1.tsx b/src/pages/luxun/place/place1.tsx index 0da7282..2857915 100644 --- a/src/pages/luxun/place/place1.tsx +++ b/src/pages/luxun/place/place1.tsx @@ -179,11 +179,11 @@ const LuxunPlace1 = () => { onContinue={() => setShowAfterDialogGuide(false)} ailgn="left" > - 1、场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态; + 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {"\n"} - 2、在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点开查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习; + 2.在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点击查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习。 {"\n"} - 3、所有动态光点的展品全部学习后才能进入下一个场景学习; + 3.所有动态光点的展品全部学习后才能进入下一个场景学习。 {/* 浏览完展品后的对话 */} diff --git a/src/pages/sunzhongshan/place/place1.tsx b/src/pages/sunzhongshan/place/place1.tsx index e0efa31..7dc8c94 100644 --- a/src/pages/sunzhongshan/place/place1.tsx +++ b/src/pages/sunzhongshan/place/place1.tsx @@ -15,6 +15,7 @@ import { useState, useRef } from "react"; import Cursor from "@/components/cursor"; import { SuccessAlert, DragQuizPanel } from "@/components/layout"; import { MultipleChoiceQuizPanel } from "@/components/layout/quiz"; +import { useCursorController } from "@/hooks/useCursorController"; // starName 固定为 sunzhongshan const starName = "sunzhongshan"; @@ -48,6 +49,22 @@ const SunzhongshanPlace1 = () => { // 已浏览展品的 ID 集合 const [visited, setVisited] = useState>(new Set()); + // 展品2窗口关闭的对话状态 + const [guide2CloseVisible, setGuide2CloseVisible] = useState(false); + const [guide2CloseStep, setGuide2CloseStep] = useState(0); + + // Cursor 顺序控制:1 -> 2 -> 3 -> 4 -> 5 + const { isEnabled } = useCursorController({ + initialEnabled: [2], + dependencies: { + 1: [3], + 3: [4], + 4: [5], + 5: [2], + }, + visited, + }); + // 光标2的对话状态 const [guide2Visible, setGuide2Visible] = useState(false); const [guide2Step, setGuide2Step] = useState(0); @@ -140,30 +157,35 @@ const SunzhongshanPlace1 = () => { className="absolute left-75 bottom-80" onClick={() => handleOpenExhibit(1)} read={visited.has(1)} + disabled={!isEnabled(1)} /> {/* 光标2 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 光标3 */} handleOpenExhibit(3)} read={visited.has(3)} + disabled={!isEnabled(3)} /> {/* 光标4 */} handleOpenExhibit(4)} read={visited.has(4)} + disabled={!isEnabled(4)} /> {/* 光标5 */} handleOpenExhibit(5)} read={visited.has(5)} + disabled={!isEnabled(5)} /> {/* 继续前进 - 仅在完成所有展品后显示 */} @@ -188,6 +210,9 @@ const SunzhongshanPlace1 = () => { if (currentId === 2) { setGuide2Step(0); setGuide2Visible(false); + + setGuide2CloseStep(0); + setGuide2CloseVisible(true); } else if (currentId === 5) { setGuide5Step(0); setGuide5Visible(false); @@ -201,6 +226,9 @@ const SunzhongshanPlace1 = () => { } else if (currentId === 3) { setGuide3Step(0); setGuide3Visible(false); + setActiveExhibitId(null); + setShowVideo4Second(true); + return; } else if (currentId === 1) { setGuide1Step(0); setGuide1Visible(false); @@ -228,8 +256,8 @@ const SunzhongshanPlace1 = () => { setVisible(true); }} > - {`同学你好,欢迎来到走进故居-对话学习模块! -进入孙中山故居纪念馆后,我们一起来了解孙中山同志经历的7个阶段:"立志救国 推翻帝制"、"讨袁护法 捍卫共和"、"与时俱进探索新路"、"抱病北上鞠躬尽瘁"、"革命先驱 永垂不朽"、"思想传承 影响深远"、"精神财富 激励后人";`} + {`同学你好,欢迎来到“走进故居·对话”学习模块! + 此刻你正置身于孙中山故居堂屋,接下来请随时光的脚步,一同走进他人生中四段激情燃烧的岁月:立志救国·推翻帝制、讨袁护法·捍卫共和、与时俱进·探索新路、抱病北上·鞠躬尽瘁。做好准备了吗?开始探索这段波澜壮阔的历史征程吧!`} {/* 引导 */} @@ -242,7 +270,7 @@ const SunzhongshanPlace1 = () => { setShowAfterDialogGuide(true); } }} - showMask={true} + showMask={false} highlightSelectors={[]} > @@ -259,11 +287,11 @@ const SunzhongshanPlace1 = () => { onContinue={() => setShowAfterDialogGuide(false)} ailgn="left" > - 1、场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,浏览展品后蓝色动态光点会变为已浏览状态; + 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {"\n"} - 2、在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点开查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习; + 2.在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点击查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习。 {"\n"} - 3、所有动态光点的展品全部学习后才能进入下一个场景学习; + 3.所有动态光点的展品全部学习后才能进入下一个场景学习。 {/* 光标2的对话 */} @@ -281,7 +309,7 @@ const SunzhongshanPlace1 = () => { }); } }} - showMask={true} + showMask={false} highlightSelectors={[]} zIndex={10000} > @@ -291,6 +319,25 @@ const SunzhongshanPlace1 = () => { + {/* 展品2窗口关闭的对话 */} + { + setGuide2CloseVisible(nextVisible); + }} + showMask={false} + zIndex={10000} + > + + 原来孙先生早在青年时期就把国家前途命运放在了心上。 + + + 不仅是关心,他还付诸了实际行动。看看这件展品。 + + + {/* 光标5的对话 */} { { visible={successAlert4Visible} onContinue={() => { setSuccessAlert4Visible(false); - setShowVideo4Second(true); + // setShowVideo4Second(true); + setVisited((prev) => { + const next = new Set(prev); + next.add(4); + return next; + }); }} onClose={() => { setSuccessAlert4Visible(false); - setShowVideo4Second(true); }} /> @@ -421,12 +472,8 @@ const SunzhongshanPlace1 = () => { showSkipButton onEnded={() => { setShowVideo4Second(false); - // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(4); - return next; - }); + + setVisited((prev) => new Set([...prev, 3])); }} /> )} @@ -438,16 +485,8 @@ const SunzhongshanPlace1 = () => { onStepChange={setGuide3Step} onVisibleChange={(nextVisible) => { setGuide3Visible(nextVisible); - if (!nextVisible) { - setVisited((prev) => { - const next = new Set(prev); - next.add(3); - return next; - }); - } }} showMask={false} - highlightSelectors={[]} zIndex={10000} > @@ -460,10 +499,10 @@ const SunzhongshanPlace1 = () => { 孙中山先生领导的辛亥革命推动了中国社会的变革,可是这张《民立报》让我有点不明白,孙先生刚当上临时大总统,为什么又很快宣布"解职"了呀? - 中华民国的建立有个重要背景——"南北议和"。辛亥革命中宣告独立的省份大多集中在中国南方,负隅顽抗的清政府将最后希望寄托在主导北洋军阀势力的袁世凯身上。在列强支持下,袁世凯率军南下... + 中华民国的建立有个重要背景——"南北议和"。辛亥革命中宣告独立的省份大多集中在中国南方,负隅顽抗的清政府将最后希望寄托在主导北洋军阀势力的袁世凯身上。在列强支持下,袁世凯率军南下。 - 一边以武力威胁革命势力,一边又迎合革命党人急于完成统一的愿望,诱使其开展和议。当时,革命党人力量还不够强。为避免国家陷入前途未卜的内战,孙中山先生发表声明:只要清帝退位,袁世凯赞成共和,就推举他做大总统。 + 他一边以武力威胁革命势力,一边又迎合革命党人急于完成统一的愿望,诱使其开展和议。当时,革命党人力量还不够强。为避免国家陷入前途未卜的内战,孙中山先生发表声明:只要清帝退位,袁世凯赞成共和,就推举他做大总统。 @@ -489,12 +528,12 @@ const SunzhongshanPlace1 = () => { { ); const [visited, setVisited] = useState>(new Set()); + // Cursor 顺序控制:1 -> 2 -> 3 -> 4 + const { isEnabled } = useCursorController({ + initialEnabled: [1], + dependencies: { + 2: [1], + 3: [2], + 4: [3], + }, + visited: visited, + }); + // 光标1的对话状态 const [guide1Visible, setGuide1Visible] = useState(false); const [guide1Step, setGuide1Step] = useState(0); @@ -98,24 +110,28 @@ const SunzhongshanPlace2 = () => { className="absolute left-160 top-114" onClick={() => handleOpenExhibit(1)} read={visited.has(1)} + disabled={!isEnabled(1)} /> {/* 光标2-陆军大元帅时佩带的指挥刀 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 光标3-917.9.10孙中山就任中华民国军政府海陆军大元帅留影 */} handleOpenExhibit(3)} read={visited.has(3)} + disabled={!isEnabled(3)} /> {/* 光标4-实业计划 */} handleOpenExhibit(4)} read={visited.has(4)} + disabled={!isEnabled(4)} /> {completed && ( @@ -225,7 +241,7 @@ const SunzhongshanPlace2 = () => { 真是一个宏伟的计划,可是在内忧外患的时局下,实现起来一定很艰难吧? - 是的,1913年3月"宋教仁案"将国民党和袁世凯方面的矛盾推向高峰。不愿坐视袁世凯窃取辛亥革命果实后独揽大权,革命党人发动了"二次革命"。可惜革命失败,孙中山先生被迫流亡日本。 + 是的,1913年3月"宋教仁案"将国民党和袁世凯方面的矛盾推向高峰。不愿坐视袁世凯窃取辛亥革命果实后独揽大权,革命党人发动了"二次革命"。可惜革命失败,孙中山先生被迫流亡海外。 袁世凯趁机撤销了他"筹办全国铁路全权"的职务,把铁路公司的事权交给了北京政府交通部,公司就这样解散了。 @@ -252,7 +268,6 @@ const SunzhongshanPlace2 = () => { }} /> )} -
); }; diff --git a/src/pages/sunzhongshan/place/place3.tsx b/src/pages/sunzhongshan/place/place3.tsx index 1b5c3e0..321ea5d 100644 --- a/src/pages/sunzhongshan/place/place3.tsx +++ b/src/pages/sunzhongshan/place/place3.tsx @@ -10,6 +10,7 @@ import { type VideoRef, type DragFillResult, } from "@/components"; +import { useCursorController } from "@/hooks/useCursorController"; import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { SuccessAlert, DragQuizPanel } from "@/components/layout"; @@ -34,6 +35,17 @@ const SunzhongshanPlace3 = () => { ); const [visited, setVisited] = useState>(new Set()); + // Cursor 顺序控制:1 -> 2 -> 3 -> 4 + const { isEnabled } = useCursorController({ + initialEnabled: [1], + dependencies: { + 2: [1], + 3: [2], + 4: [3], + }, + visited: visited, + }); + // 光标1的对话状态 const [guide1Visible, setGuide1Visible] = useState(false); const [guide1Step, setGuide1Step] = useState(0); @@ -112,22 +124,26 @@ const SunzhongshanPlace3 = () => { className="absolute left-198 top-118" onClick={() => handleOpenExhibit(1)} read={visited.has(1)} + disabled={!isEnabled(1)} /> handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> handleOpenExhibit(3)} read={visited.has(3)} + disabled={!isEnabled(3)} /> handleOpenExhibit(4)} read={visited.has(4)} + disabled={!isEnabled(4)} /> {completed && ( @@ -246,9 +262,9 @@ const SunzhongshanPlace3 = () => { = { }; // 光标1的展品描述内容(图2) -const exhibit1Description = - '1924年10月,冯玉祥将军发动"北京政变",推翻了曹锟政府,邀请孙中山先生北上共商国是,希望能实现国家和平统一。孙先生认为"这是一个好机会",于是11月13日从广州出发北上,途经上海、日本、天津等地,一路宣传反帝、反封建主张。孙先生抵达上海时,外滩码头上聚集了前来欢迎的各界代表、群众四千多人。大家关心国家前途命运,拥护孙先生,认同他在《北上宣言》中提出的"废除不平等条约""召开国民会议"等主张,真心希望结束军阀割据,国家实现统一,民族实现独立。'; +// const exhibit1Description = +// '1924年10月,冯玉祥将军发动"北京政变",推翻了曹锟政府,邀请孙中山先生北上共商国是,希望能实现国家和平统一。孙先生认为"这是一个好机会",于是11月13日从广州出发北上,途经上海、日本、天津等地,一路宣传反帝、反封建主张。孙先生抵达上海时,外滩码头上聚集了前来欢迎的各界代表、群众四千多人。大家关心国家前途命运,拥护孙先生,认同他在《北上宣言》中提出的"废除不平等条约""召开国民会议"等主张,真心希望结束军阀割据,国家实现统一,民族实现独立。'; const SunzhongshanPlace4 = () => { const navigate = useNavigate(); @@ -35,14 +36,32 @@ const SunzhongshanPlace4 = () => { const [initialDialogStep, setInitialDialogStep] = useState(0); const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null); const [visited, setVisited] = useState>(new Set()); + + // Cursor 顺序控制:1 -> 2 + const { isEnabled } = useCursorController({ + initialEnabled: [2], + dependencies: { + 1: [2], + }, + visited: visited, + }); // 光标1的视频状态 const [showVideo1, setShowVideo1] = useState(false); const video1Ref = useRef(null); // 光标1视频播放后的学习引导状态 const [showVideo1Guide, setShowVideo1Guide] = useState(false); + // 光标2的对话状态 + const [guide2Visible, setGuide2Visible] = useState(false); + const [guide2Step, setGuide2Step] = useState(0); + const handleOpenExhibit = (index: 1 | 2) => { setActiveExhibitId(index); + if (index === 2) { + console.log(index); + setGuide2Step(0); + setGuide2Visible(true); + } }; const progressCount = visited.size; @@ -84,11 +103,13 @@ const SunzhongshanPlace4 = () => { className="absolute right-209 top-128" onClick={() => handleOpenExhibit(1)} read={visited.has(1)} + disabled={!isEnabled(1)} /> handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {completed && ( @@ -105,7 +126,7 @@ const SunzhongshanPlace4 = () => { { const currentId = activeExhibitId; @@ -144,9 +165,10 @@ const SunzhongshanPlace4 = () => { /> )} - {/* 光标1视频播放后的学习引导 */} - { setShowVideo1Guide(false); // 学习引导关闭后,修改cursor状态 @@ -156,10 +178,7 @@ const SunzhongshanPlace4 = () => { return next; }); }} - ailgn="left" - > - 通过了解这些展品背后的故事,我们回顾了孙中山先生这位民主革命先驱伟大的一生,不仅看到了孙先生万难不辞、奔波海上的革命足迹,更感受到了他愈挫愈奋、攀登不已的革命精神。下面,让我们跨越时空回到黎明的前夜,与孙先生一起,探寻国家的出路。 - + /> {/* 关闭学习引导后显示的对话(图1) */} { 是的。然而长期操劳国事使得孙中山先生身体抱恙。在生命最后的时间里,他仍在为国奔走,始终未曾停歇。看看这张拍摄于1924年11月17日的照片吧。 + + {/* 光标2的对话 */} + { + setGuide2Visible(nextVisible); + }} + showMask={false} + zIndex={10000} + > + 这是……外滩? + + 没错。1924年10月,冯玉祥将军发动“北京政变”,推翻了曹锟政府,邀请孙中山先生北上共商国是,希望能实现国家和平统一。 + + + 孙先生认为“这是一个好机会”,于是11月13日从广州出发北上,途经上海、日本、天津等地,一路宣传反帝、反封建主张。孙先生抵达上海时,外滩码头上聚集了前来欢迎的各界代表、群众四千多人。 + + + 大家关心国家前途命运,拥护孙先生,认同他在《北上宣言》中提出的“废除不平等条约”“召开国民会议”等主张,真心希望结束军阀割据,国家实现统一,民族实现独立。 + +
); }; diff --git a/src/pages/sunzhongshan/reflection/index.tsx b/src/pages/sunzhongshan/reflection/index.tsx index 5aefce4..dc77c22 100644 --- a/src/pages/sunzhongshan/reflection/index.tsx +++ b/src/pages/sunzhongshan/reflection/index.tsx @@ -130,9 +130,10 @@ const SunzhongshanReflection = () => { {/* 学习引导弹窗 */} - 同学你好,欢迎来到跨越时空 - 体悟学习模块! - {"\n\n"} - 本模块包含三个推理小游戏,场景学习顺序不分先后,三个场景全部学习完后方能完成本模块的学习,那么现在就请你开始体验吧。 + 同学你好,欢迎来到“跨越时空·体悟”学习模块!{"\n"} + 请登陆“时空回溯・亲历时代”体验仓,下一站:二十世纪初,上海。{"\n"} + 辛亥革命之后,民主共和的观念深入人心。面对中外反动势力,社会各界更广泛地行动起来,以各自的方式争取着民主自由和国家尊严。 + 请探索周围环境,拨开时局迷雾,与奋进求索的仁人志士们一同探寻中国革命的前路……本模块包含三个推理小游戏,完成三个场景的探索后,可进入到下一阶段的学习。时空门已开,请做好准备迎接考验吧! ); diff --git a/src/pages/sunzhongshan/reflection/scene1.tsx b/src/pages/sunzhongshan/reflection/scene1.tsx index 31d3df8..1a54f06 100644 --- a/src/pages/sunzhongshan/reflection/scene1.tsx +++ b/src/pages/sunzhongshan/reflection/scene1.tsx @@ -433,10 +433,10 @@ const SunzhongshanReflectionScene1 = () => { onContinue={handleGuideClose} zIndex={10000} > - 同学你好,欢迎来到二十世纪初的上海。辛亥革命之后,民主共和的观念深入人心。 - 面对中外反动势力,社会各界更广泛地行动起来,以各自的方式争取着民主自由和国家尊严。 + 同学你好,欢迎来到二十世纪初的上海。{"\n"} + 辛亥革命之后,民主共和的观念深入人心。面对中外反动势力,社会各界更广泛地行动起来,以各自的方式争取着民主自由和国家尊严。 {"\n"} - 请探索周围环境,拨开时局迷雾,与奋进求索的仁人志士们一同探寻中国革命的前路。本游戏需要在场景中找出4条线索,并回答正确所有问题后才能完成本游戏探索。 + 你需要在本场景中找出4条线索,并正确回答所有问题后,才能拨开时局迷雾,进入下一个场景。 {/* 题目1:今天的日期是? */} diff --git a/src/pages/sunzhongshan/reflection/scene2.tsx b/src/pages/sunzhongshan/reflection/scene2.tsx index 1fe0712..99b1ba1 100644 --- a/src/pages/sunzhongshan/reflection/scene2.tsx +++ b/src/pages/sunzhongshan/reflection/scene2.tsx @@ -474,12 +474,12 @@ const SunzhongshanReflectionScene2 = () => { { value: "A", label: - "A: 回想一年多来,全国已经组织起了大小罢工上百次。虽然取得过一些胜利,但现在工人们的力量毕竟弱小,我们愈是坚定,便愈将面对敌人疯狂的反扑。若为工人们找到其他方面的朋友,或许方可与荷枪实弹的军阀势力相抗衡。", + "A.回想一年多来,全国已经组织起了大小罢工上百次。虽然取得过一些胜利,但现在工人们的力量毕竟弱小,我们愈是坚定,便愈将面对敌人疯狂的反扑。若为工人们找到其他方面的朋友,或许方可与荷枪实弹的军阀势力相抗衡。", }, { value: "B", label: - "B: 回想一年多来,全国已经组织起了大小罢工上百次,多有捷报。此时虽面对一些挫折,但只要工人们始终坚定地团结在一起,敢于牺牲,就一定能战胜内外敌人!", + "B.回想一年多来,全国已经组织起了大小罢工上百次,多有捷报。此时虽面对一些挫折,但只要工人们始终坚定地团结在一起,敢于牺牲,就一定能战胜内外敌人!", }, ]} correctAnswer="A" @@ -495,11 +495,11 @@ const SunzhongshanReflectionScene2 = () => { options={[ { value: "A", - label: "A: 青帮、洪门等颇有势力的会党", + label: "A.青帮、洪门等颇有势力的会党", }, { value: "B", - label: "B: 孙中山先生领导的中国国民党", + label: "B.孙中山先生领导的中国国民党", }, ]} correctAnswer="B"