diff --git a/package-lock.json b/package-lock.json index 3af4cca..ce84b96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "shanghai-residences", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "axios": "^1.12.2", "clsx": "^2.1.1", "framer-motion": "^12.23.26", @@ -339,6 +342,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", diff --git a/public/audio/chenyun/mp3_chenyun_22.mp3 b/public/audio/chenyun/mp3_chenyun_22.mp3 index 466cc7c..7c200db 100644 Binary files a/public/audio/chenyun/mp3_chenyun_22.mp3 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 index 26cd67a..0f201d1 100644 Binary files a/public/audio/chenyun/mp3_chenyun_23.mp3 and b/public/audio/chenyun/mp3_chenyun_23.mp3 differ diff --git a/public/audio/chenyun/mp3_chenyun_29.mp3 b/public/audio/chenyun/mp3_chenyun_29.mp3 index 731a1cc..6aca543 100644 Binary files a/public/audio/chenyun/mp3_chenyun_29.mp3 and b/public/audio/chenyun/mp3_chenyun_29.mp3 differ diff --git a/src/assets/images/chenyun/place2/场景2展品1.png b/src/assets/images/chenyun/place2/场景2展品1.png index e4ecba4..d892323 100644 Binary files a/src/assets/images/chenyun/place2/场景2展品1.png and b/src/assets/images/chenyun/place2/场景2展品1.png differ diff --git a/src/assets/images/chenyun/place2/场景2展品2.png b/src/assets/images/chenyun/place2/场景2展品2.png index 085945b..f930261 100644 Binary files a/src/assets/images/chenyun/place2/场景2展品2.png and b/src/assets/images/chenyun/place2/场景2展品2.png differ diff --git a/src/assets/images/chenyun/place3/场景3展品1.png b/src/assets/images/chenyun/place3/场景3展品1.png index 2c91d97..780e1ef 100644 Binary files a/src/assets/images/chenyun/place3/场景3展品1.png and b/src/assets/images/chenyun/place3/场景3展品1.png differ diff --git a/src/assets/images/chenyun/place3/场景3展品2.png b/src/assets/images/chenyun/place3/场景3展品2.png index 831e60b..a640203 100644 Binary files a/src/assets/images/chenyun/place3/场景3展品2.png and b/src/assets/images/chenyun/place3/场景3展品2.png differ diff --git a/src/assets/images/chenyun/place4/场景4展品1.png b/src/assets/images/chenyun/place4/场景4展品1.png index 8b98445..f824266 100644 Binary files a/src/assets/images/chenyun/place4/场景4展品1.png and b/src/assets/images/chenyun/place4/场景4展品1.png differ diff --git a/src/assets/images/chenyun/place4/场景4展品2.png b/src/assets/images/chenyun/place4/场景4展品2.png index f6bf8a7..56e9d51 100644 Binary files a/src/assets/images/chenyun/place4/场景4展品2.png and b/src/assets/images/chenyun/place4/场景4展品2.png differ diff --git a/src/assets/images/chenyun/place5/场景5展品1.png b/src/assets/images/chenyun/place5/场景5展品1.png index 76b4005..032a7c5 100644 Binary files a/src/assets/images/chenyun/place5/场景5展品1.png and b/src/assets/images/chenyun/place5/场景5展品1.png differ diff --git a/src/assets/images/chenyun/place5/场景5展品2.png b/src/assets/images/chenyun/place5/场景5展品2.png index 408f2be..6de56bd 100644 Binary files a/src/assets/images/chenyun/place5/场景5展品2.png and b/src/assets/images/chenyun/place5/场景5展品2.png differ diff --git a/src/assets/images/chenyun/place6/场景6展品1.png b/src/assets/images/chenyun/place6/场景6展品1.png index 21190a8..47416bc 100644 Binary files a/src/assets/images/chenyun/place6/场景6展品1.png and b/src/assets/images/chenyun/place6/场景6展品1.png differ diff --git a/src/assets/images/chenyun/place8/场景8展品1.png b/src/assets/images/chenyun/place8/场景8展品1.png index a861763..d37cbbb 100644 Binary files a/src/assets/images/chenyun/place8/场景8展品1.png and b/src/assets/images/chenyun/place8/场景8展品1.png differ diff --git a/src/assets/images/chenyun/place8/场景8展品2.png b/src/assets/images/chenyun/place8/场景8展品2.png index fdf65f1..ba3b3b0 100644 Binary files a/src/assets/images/chenyun/place8/场景8展品2.png and b/src/assets/images/chenyun/place8/场景8展品2.png differ diff --git a/src/assets/images/chenyun/reflection/投机商情报桌上.png b/src/assets/images/chenyun/reflection/投机商情报桌上.png index 6b710bd..7ec44a9 100644 Binary files a/src/assets/images/chenyun/reflection/投机商情报桌上.png and b/src/assets/images/chenyun/reflection/投机商情报桌上.png differ diff --git a/src/assets/images/chenyun/reflection/线索1.png b/src/assets/images/chenyun/reflection/线索1.png index 7982e93..a5648c6 100644 Binary files a/src/assets/images/chenyun/reflection/线索1.png and b/src/assets/images/chenyun/reflection/线索1.png differ diff --git a/src/assets/images/chenyun/reflection/线索2.png b/src/assets/images/chenyun/reflection/线索2.png index d6ec00b..bcf7c97 100644 Binary files a/src/assets/images/chenyun/reflection/线索2.png and b/src/assets/images/chenyun/reflection/线索2.png differ diff --git a/src/assets/images/huangyanpei/place3/5_006.png b/src/assets/images/huangyanpei/place3/5_006.png index ddb2609..4846137 100644 Binary files a/src/assets/images/huangyanpei/place3/5_006.png and b/src/assets/images/huangyanpei/place3/5_006.png differ diff --git a/src/assets/images/learning-map/spotlight/陈云详情.png b/src/assets/images/learning-map/spotlight/陈云详情.png index cb6ab32..dc3979e 100644 Binary files a/src/assets/images/learning-map/spotlight/陈云详情.png and b/src/assets/images/learning-map/spotlight/陈云详情.png differ diff --git a/src/assets/images/learning-map/底图.png b/src/assets/images/learning-map/底图.png index d5a4425..130e91c 100644 Binary files a/src/assets/images/learning-map/底图.png and b/src/assets/images/learning-map/底图.png differ diff --git a/src/assets/images/luxun/place1/展品1.png b/src/assets/images/luxun/place1/展品1.png index c96b061..79f1a35 100644 Binary files a/src/assets/images/luxun/place1/展品1.png and b/src/assets/images/luxun/place1/展品1.png differ diff --git a/src/assets/images/luxun/place1/展品2.png b/src/assets/images/luxun/place1/展品2.png index 8926dd9..3abbce6 100644 Binary files a/src/assets/images/luxun/place1/展品2.png and b/src/assets/images/luxun/place1/展品2.png differ diff --git a/src/assets/images/luxun/place2/展品5.png b/src/assets/images/luxun/place2/展品5.png index fc86939..f1c5315 100644 Binary files a/src/assets/images/luxun/place2/展品5.png and b/src/assets/images/luxun/place2/展品5.png differ diff --git a/src/assets/images/luxun/place3/10.png b/src/assets/images/luxun/place3/10.png index 9d899fa..b64e946 100644 Binary files a/src/assets/images/luxun/place3/10.png and b/src/assets/images/luxun/place3/10.png differ diff --git a/src/assets/images/luxun/place3/7.png b/src/assets/images/luxun/place3/7.png index d1cb973..e046bcc 100644 Binary files a/src/assets/images/luxun/place3/7.png and b/src/assets/images/luxun/place3/7.png differ diff --git a/src/assets/images/luxun/place3/8.png b/src/assets/images/luxun/place3/8.png index 0cea5a1..1f01893 100644 Binary files a/src/assets/images/luxun/place3/8.png and b/src/assets/images/luxun/place3/8.png differ diff --git a/src/assets/images/luxun/place3/9.png b/src/assets/images/luxun/place3/9.png index 74febe1..80ec63c 100644 Binary files a/src/assets/images/luxun/place3/9.png and b/src/assets/images/luxun/place3/9.png differ diff --git a/src/assets/images/luxun/place4/展品13.png b/src/assets/images/luxun/place4/展品13.png index a714a95..e98845c 100644 Binary files a/src/assets/images/luxun/place4/展品13.png and b/src/assets/images/luxun/place4/展品13.png differ diff --git a/src/assets/images/luxun/reflection/scene1/效果图1.jpg b/src/assets/images/luxun/reflection/scene1/效果图1.jpg deleted file mode 100644 index 8093c01..0000000 Binary files a/src/assets/images/luxun/reflection/scene1/效果图1.jpg and /dev/null differ diff --git a/src/assets/images/luxun/reflection/scene1/效果图2.jpg b/src/assets/images/luxun/reflection/scene1/效果图2.jpg deleted file mode 100644 index f3c9bff..0000000 Binary files a/src/assets/images/luxun/reflection/scene1/效果图2.jpg and /dev/null differ diff --git a/src/assets/images/luxun/reflection/scene2/底图.png b/src/assets/images/luxun/reflection/scene2/底图.png index 28bc46b..f3f39b5 100644 Binary files a/src/assets/images/luxun/reflection/scene2/底图.png and b/src/assets/images/luxun/reflection/scene2/底图.png differ diff --git a/src/assets/images/luxun/reflection/scene2/效果图1.jpg b/src/assets/images/luxun/reflection/scene2/效果图1.jpg deleted file mode 100644 index 1ecf48d..0000000 Binary files a/src/assets/images/luxun/reflection/scene2/效果图1.jpg and /dev/null differ diff --git a/src/assets/images/luxun/reflection/scene2/效果图2.jpg b/src/assets/images/luxun/reflection/scene2/效果图2.jpg deleted file mode 100644 index 256f5bf..0000000 Binary files a/src/assets/images/luxun/reflection/scene2/效果图2.jpg and /dev/null differ diff --git a/src/assets/images/luxun/reflection/scene3/效果图1.jpg b/src/assets/images/luxun/reflection/scene3/效果图1.jpg deleted file mode 100644 index 8c67b05..0000000 Binary files a/src/assets/images/luxun/reflection/scene3/效果图1.jpg and /dev/null differ diff --git a/src/assets/images/luxun/reflection/scene3/效果图2.jpg b/src/assets/images/luxun/reflection/scene3/效果图2.jpg deleted file mode 100644 index a656879..0000000 Binary files a/src/assets/images/luxun/reflection/scene3/效果图2.jpg and /dev/null differ diff --git a/src/assets/images/nieer/place1/场景1展品1.png b/src/assets/images/nieer/place1/场景1展品1.png index 8bee2ae..a20b561 100644 Binary files a/src/assets/images/nieer/place1/场景1展品1.png and b/src/assets/images/nieer/place1/场景1展品1.png differ diff --git a/src/assets/images/nieer/place1/场景1展品2.png b/src/assets/images/nieer/place1/场景1展品2.png index e570438..fc820b5 100644 Binary files a/src/assets/images/nieer/place1/场景1展品2.png and b/src/assets/images/nieer/place1/场景1展品2.png differ diff --git a/src/assets/images/nieer/place1/场景1展品3.png b/src/assets/images/nieer/place1/场景1展品3.png index 1d992ef..3cb4d63 100644 Binary files a/src/assets/images/nieer/place1/场景1展品3.png and b/src/assets/images/nieer/place1/场景1展品3.png differ diff --git a/src/assets/images/nieer/place2/场景2展品1.png b/src/assets/images/nieer/place2/场景2展品1.png index c6cb2f0..2c4ef13 100644 Binary files a/src/assets/images/nieer/place2/场景2展品1.png and b/src/assets/images/nieer/place2/场景2展品1.png differ diff --git a/src/assets/images/nieer/place2/场景2展品2.png b/src/assets/images/nieer/place2/场景2展品2.png index bc22989..3117bc1 100644 Binary files a/src/assets/images/nieer/place2/场景2展品2.png and b/src/assets/images/nieer/place2/场景2展品2.png differ diff --git a/src/assets/images/nieer/place3/场景3展品1.png b/src/assets/images/nieer/place3/场景3展品1.png index 922d160..c723e0a 100644 Binary files a/src/assets/images/nieer/place3/场景3展品1.png and b/src/assets/images/nieer/place3/场景3展品1.png differ diff --git a/src/assets/images/nieer/place3/场景3展品2.png b/src/assets/images/nieer/place3/场景3展品2.png index 09c5bec..69320ae 100644 Binary files a/src/assets/images/nieer/place3/场景3展品2.png and b/src/assets/images/nieer/place3/场景3展品2.png differ diff --git a/src/assets/images/nieer/place3/场景3展品3.png b/src/assets/images/nieer/place3/场景3展品3.png index bb589df..52a6cb9 100644 Binary files a/src/assets/images/nieer/place3/场景3展品3.png and b/src/assets/images/nieer/place3/场景3展品3.png differ diff --git a/src/assets/images/nieer/place4/场景4展品1.png b/src/assets/images/nieer/place4/场景4展品1.png index d54c85b..a4193d1 100644 Binary files a/src/assets/images/nieer/place4/场景4展品1.png and b/src/assets/images/nieer/place4/场景4展品1.png differ diff --git a/src/assets/images/nieer/place4/场景4展品2.png b/src/assets/images/nieer/place4/场景4展品2.png index 86d9ff2..dc1efa5 100644 Binary files a/src/assets/images/nieer/place4/场景4展品2.png and b/src/assets/images/nieer/place4/场景4展品2.png differ diff --git a/src/assets/images/nieer/place5/场景5展品1.png b/src/assets/images/nieer/place5/场景5展品1.png index 1a18e18..1e50181 100644 Binary files a/src/assets/images/nieer/place5/场景5展品1.png and b/src/assets/images/nieer/place5/场景5展品1.png differ diff --git a/src/assets/images/nieer/place5/场景5展品2.png b/src/assets/images/nieer/place5/场景5展品2.png index 4bdefd8..ddfc15c 100644 Binary files a/src/assets/images/nieer/place5/场景5展品2.png and b/src/assets/images/nieer/place5/场景5展品2.png differ diff --git a/src/assets/images/nieer/place5/场景5展品3.png b/src/assets/images/nieer/place5/场景5展品3.png index f14410e..425b251 100644 Binary files a/src/assets/images/nieer/place5/场景5展品3.png and b/src/assets/images/nieer/place5/场景5展品3.png differ diff --git a/src/assets/images/nieer/place6/场景6展品1.png b/src/assets/images/nieer/place6/场景6展品1.png index 7990999..7c43205 100644 Binary files a/src/assets/images/nieer/place6/场景6展品1.png and b/src/assets/images/nieer/place6/场景6展品1.png differ diff --git a/src/assets/images/nieer/place6/场景6展品2.png b/src/assets/images/nieer/place6/场景6展品2.png index 92622ad..a0d63f0 100644 Binary files a/src/assets/images/nieer/place6/场景6展品2.png and b/src/assets/images/nieer/place6/场景6展品2.png differ diff --git a/src/assets/images/puzzle/chenyun/details/6.png b/src/assets/images/puzzle/chenyun/details/6.png index 4f8f5ec..00e3176 100644 Binary files a/src/assets/images/puzzle/chenyun/details/6.png and b/src/assets/images/puzzle/chenyun/details/6.png differ diff --git a/src/assets/images/puzzle/luxun/details/13.png b/src/assets/images/puzzle/luxun/details/13.png index c364db5..81b8be6 100644 Binary files a/src/assets/images/puzzle/luxun/details/13.png and b/src/assets/images/puzzle/luxun/details/13.png differ diff --git a/src/assets/images/puzzle/nieer/details/1.png b/src/assets/images/puzzle/nieer/details/1.png index 218822c..fe3e58b 100644 Binary files a/src/assets/images/puzzle/nieer/details/1.png and b/src/assets/images/puzzle/nieer/details/1.png differ diff --git a/src/assets/images/puzzle/nieer/details/10.png b/src/assets/images/puzzle/nieer/details/10.png index 5881483..a17ba40 100644 Binary files a/src/assets/images/puzzle/nieer/details/10.png and b/src/assets/images/puzzle/nieer/details/10.png differ diff --git a/src/assets/images/puzzle/nieer/details/11.png b/src/assets/images/puzzle/nieer/details/11.png index 05f20fa..bd00fd3 100644 Binary files a/src/assets/images/puzzle/nieer/details/11.png and b/src/assets/images/puzzle/nieer/details/11.png differ diff --git a/src/assets/images/puzzle/nieer/details/12.png b/src/assets/images/puzzle/nieer/details/12.png index 3b80847..ca5c45c 100644 Binary files a/src/assets/images/puzzle/nieer/details/12.png and b/src/assets/images/puzzle/nieer/details/12.png differ diff --git a/src/assets/images/puzzle/nieer/details/13.png b/src/assets/images/puzzle/nieer/details/13.png index 55190fa..97fb5df 100644 Binary files a/src/assets/images/puzzle/nieer/details/13.png and b/src/assets/images/puzzle/nieer/details/13.png differ diff --git a/src/assets/images/puzzle/nieer/details/14.png b/src/assets/images/puzzle/nieer/details/14.png index 71c7a78..2849cd4 100644 Binary files a/src/assets/images/puzzle/nieer/details/14.png and b/src/assets/images/puzzle/nieer/details/14.png differ diff --git a/src/assets/images/puzzle/nieer/details/3.png b/src/assets/images/puzzle/nieer/details/3.png index 997eafa..1634e7b 100644 Binary files a/src/assets/images/puzzle/nieer/details/3.png and b/src/assets/images/puzzle/nieer/details/3.png differ diff --git a/src/assets/images/puzzle/nieer/details/5.png b/src/assets/images/puzzle/nieer/details/5.png index 15ad3fe..34a572d 100644 Binary files a/src/assets/images/puzzle/nieer/details/5.png and b/src/assets/images/puzzle/nieer/details/5.png differ diff --git a/src/assets/images/puzzle/nieer/details/8.png b/src/assets/images/puzzle/nieer/details/8.png index 30412bd..fea13dc 100644 Binary files a/src/assets/images/puzzle/nieer/details/8.png and b/src/assets/images/puzzle/nieer/details/8.png differ diff --git a/src/assets/images/puzzle/nieer/details/9.png b/src/assets/images/puzzle/nieer/details/9.png index 59e7a6a..693a2ee 100644 Binary files a/src/assets/images/puzzle/nieer/details/9.png and b/src/assets/images/puzzle/nieer/details/9.png differ diff --git a/src/assets/images/sunzhongshan/place1/“南北议和”纪念章.png b/src/assets/images/sunzhongshan/place1/“南北议和”纪念章.png index 9f94957..05bd3a6 100644 Binary files a/src/assets/images/sunzhongshan/place1/“南北议和”纪念章.png and b/src/assets/images/sunzhongshan/place1/“南北议和”纪念章.png differ diff --git a/src/assets/images/sunzhongshan/place1/“四大寇”合影.png b/src/assets/images/sunzhongshan/place1/“四大寇”合影.png index 70a8ad1..c0b8133 100644 Binary files a/src/assets/images/sunzhongshan/place1/“四大寇”合影.png and b/src/assets/images/sunzhongshan/place1/“四大寇”合影.png differ diff --git a/src/assets/images/sunzhongshan/place1/民立报.png b/src/assets/images/sunzhongshan/place1/民立报.png index 4ed19c2..4306108 100644 Binary files a/src/assets/images/sunzhongshan/place1/民立报.png and b/src/assets/images/sunzhongshan/place1/民立报.png differ diff --git a/src/assets/images/sunzhongshan/place1/申报.png b/src/assets/images/sunzhongshan/place1/申报.png index 8cffc8d..3e38893 100644 Binary files a/src/assets/images/sunzhongshan/place1/申报.png and b/src/assets/images/sunzhongshan/place1/申报.png differ diff --git a/src/assets/images/sunzhongshan/place1/郑观应的推荐信.png b/src/assets/images/sunzhongshan/place1/郑观应的推荐信.png index bfdb610..f790400 100644 Binary files a/src/assets/images/sunzhongshan/place1/郑观应的推荐信.png and b/src/assets/images/sunzhongshan/place1/郑观应的推荐信.png differ diff --git a/src/assets/images/sunzhongshan/place2/917.9.10孙中山就任中华民国军政府海陆军大元帅留影.png b/src/assets/images/sunzhongshan/place2/917.9.10孙中山就任中华民国军政府海陆军大元帅留影.png index b7a6a87..686a778 100644 Binary files a/src/assets/images/sunzhongshan/place2/917.9.10孙中山就任中华民国军政府海陆军大元帅留影.png and b/src/assets/images/sunzhongshan/place2/917.9.10孙中山就任中华民国军政府海陆军大元帅留影.png differ diff --git a/src/assets/images/sunzhongshan/place2/实业计划.png b/src/assets/images/sunzhongshan/place2/实业计划.png index 72e51f3..5ce73c6 100644 Binary files a/src/assets/images/sunzhongshan/place2/实业计划.png and b/src/assets/images/sunzhongshan/place2/实业计划.png differ diff --git a/src/assets/images/sunzhongshan/place2/铁路计划图.png b/src/assets/images/sunzhongshan/place2/铁路计划图.png index b4cf981..c6becd3 100644 Binary files a/src/assets/images/sunzhongshan/place2/铁路计划图.png and b/src/assets/images/sunzhongshan/place2/铁路计划图.png differ diff --git a/src/assets/images/sunzhongshan/place2/陆军大元帅时佩带的指挥刀.png b/src/assets/images/sunzhongshan/place2/陆军大元帅时佩带的指挥刀.png index a583e75..f1a71b4 100644 Binary files a/src/assets/images/sunzhongshan/place2/陆军大元帅时佩带的指挥刀.png and b/src/assets/images/sunzhongshan/place2/陆军大元帅时佩带的指挥刀.png differ diff --git a/src/assets/images/sunzhongshan/place3/中国国民党第一次全国代表大会宣言.png b/src/assets/images/sunzhongshan/place3/中国国民党第一次全国代表大会宣言.png index 97eae26..ddbd065 100644 Binary files a/src/assets/images/sunzhongshan/place3/中国国民党第一次全国代表大会宣言.png and b/src/assets/images/sunzhongshan/place3/中国国民党第一次全国代表大会宣言.png differ diff --git a/src/assets/images/sunzhongshan/place3/星期评论.png b/src/assets/images/sunzhongshan/place3/星期评论.png index 31dbfef..cfd03c4 100644 Binary files a/src/assets/images/sunzhongshan/place3/星期评论.png and b/src/assets/images/sunzhongshan/place3/星期评论.png differ diff --git a/src/assets/images/sunzhongshan/place3/李大钊给胡适的信.png b/src/assets/images/sunzhongshan/place3/李大钊给胡适的信.png index 079eb8e..4d57739 100644 Binary files a/src/assets/images/sunzhongshan/place3/李大钊给胡适的信.png and b/src/assets/images/sunzhongshan/place3/李大钊给胡适的信.png differ diff --git a/src/assets/images/sunzhongshan/place3/社会主义的精义.png b/src/assets/images/sunzhongshan/place3/社会主义的精义.png index d500b3d..015b522 100644 Binary files a/src/assets/images/sunzhongshan/place3/社会主义的精义.png and b/src/assets/images/sunzhongshan/place3/社会主义的精义.png differ diff --git a/src/assets/images/sunzhongshan/reflection/scene2/线索2-详情.png b/src/assets/images/sunzhongshan/reflection/scene2/线索2-详情.png index 11a0d5c..7ab8f42 100644 Binary files a/src/assets/images/sunzhongshan/reflection/scene2/线索2-详情.png and b/src/assets/images/sunzhongshan/reflection/scene2/线索2-详情.png differ diff --git a/src/components/layout/alert/continue-learning-modal.tsx b/src/components/layout/alert/continue-learning-modal.tsx index ba28a54..0ef2eaf 100644 --- a/src/components/layout/alert/continue-learning-modal.tsx +++ b/src/components/layout/alert/continue-learning-modal.tsx @@ -62,7 +62,7 @@ const ContinueLearningModal = ({ {/* 正文 */} -

+

{message}

diff --git a/src/components/layout/alert/error-alert.tsx b/src/components/layout/alert/error-alert.tsx index defb08c..b4b8c7f 100644 --- a/src/components/layout/alert/error-alert.tsx +++ b/src/components/layout/alert/error-alert.tsx @@ -42,7 +42,9 @@ const ErrorAlert = ({ {/* 标题和副标题容器 - 带深色背景条 */}
{/* 副标题 */} -
{text}
+
+ {text} +
{/* 继续按钮 */} diff --git a/src/components/layout/quiz/BasketQuizPanel.tsx b/src/components/layout/quiz/BasketQuizPanel.tsx index 31aafdd..14e89fc 100644 --- a/src/components/layout/quiz/BasketQuizPanel.tsx +++ b/src/components/layout/quiz/BasketQuizPanel.tsx @@ -136,7 +136,7 @@ const BasketQuizPanel = ({ renderOption={(option, isUsed) => (
)} - + {option.label}
@@ -155,7 +155,7 @@ const BasketQuizPanel = ({ renderDragging={(option) => (
- + {option.label}
diff --git a/src/components/layout/quiz/DragQuizPanel.tsx b/src/components/layout/quiz/DragQuizPanel.tsx index cfff795..4205abe 100644 --- a/src/components/layout/quiz/DragQuizPanel.tsx +++ b/src/components/layout/quiz/DragQuizPanel.tsx @@ -16,8 +16,15 @@ const DragQuizPanel = ({ blankSeparators, value, error, + feedbackType = 'error', onChange, + onSubmit, + submitText = '提交', + disabled = false, }: DragQuizPanelProps) => { + const isComplete = blanks.every((blank) => + value.some((result) => result.blankId === blank.id && result.optionValue !== '') + ) // 跟踪之前的 visible 状态 const prevVisibleRef = useRef(visible) @@ -62,7 +69,7 @@ const DragQuizPanel = ({ {filledValue ? ( {filledValue} - + */} ) : ( ____ @@ -87,48 +94,63 @@ const DragQuizPanel = ({ })} {questionSuffix && {questionSuffix}} {/* 答案反馈 */} - {error &&
{error}
} + {error && ( +
+ {error} +
+ )}
- {/* 使用 DragFill 组件 */} -
+ {/* 选项区域 */} +
(
- {!isUsed && ( -
- )} - - {option.label} - + {option.label}
)} renderBlank={(_blank) => { - // 返回一个隐藏元素,实际的空白区域在题干中 - // 但需要通过 data-blank-id 让 DragFill 能找到它 return
}} />
+ + {/* 提交按钮 */} +
) diff --git a/src/components/layout/quiz/ErrorCorrectionQuizPanel.tsx b/src/components/layout/quiz/ErrorCorrectionQuizPanel.tsx new file mode 100644 index 0000000..dfefdf3 --- /dev/null +++ b/src/components/layout/quiz/ErrorCorrectionQuizPanel.tsx @@ -0,0 +1,208 @@ +import { useEffect, useRef, useState } from 'react' +import { cn } from '@/lib/utils' +import SuccessAlert from '@/components/layout/alert/success-alert' +import type { ErrorCorrectionQuizPanelProps } from './types' + +/** + * ErrorCorrectionQuizPanel - 纠错题面板 + * 横向展示多个文本区块,用户点击找出其中的错误 + */ +const ErrorCorrectionQuizPanel = ({ + visible, + zIndex = 10000, + title, + introText, + blocks, + errorCount, + onSubmit, + disabled = false, +}: ErrorCorrectionQuizPanelProps) => { + // 记录用户点击过的区块 + const [clickedBlocks, setClickedBlocks] = useState>(new Set()) + // 记录找到的错误数量 + const [foundErrors, setFoundErrors] = useState(0) + // 当前显示在题干区域的反馈 + const [feedbackBlockId, setFeedbackBlockId] = useState(null) + // 临时高亮的错误选项 + const [wrongFlashBlockId, setWrongFlashBlockId] = useState(null) + const wrongFlashTimerRef = useRef | null>(null) + // 是否已完成 + const [isCompleted, setIsCompleted] = useState(false) + + // 重置状态 + useEffect(() => { + if (visible) { + if (wrongFlashTimerRef.current) { + clearTimeout(wrongFlashTimerRef.current) + wrongFlashTimerRef.current = null + } + setClickedBlocks(new Set()) + setFoundErrors(0) + setFeedbackBlockId(null) + setWrongFlashBlockId(null) + setIsCompleted(false) + } + + return () => { + if (wrongFlashTimerRef.current) { + clearTimeout(wrongFlashTimerRef.current) + wrongFlashTimerRef.current = null + } + } + }, [visible]) + + // 处理区块点击 + const handleBlockClick = (blockId: number) => { + if (disabled || isCompleted || clickedBlocks.has(blockId)) return + + const block = blocks.find((b) => b.id === blockId) + if (!block) return + + const newClickedBlocks = new Set(clickedBlocks) + setFeedbackBlockId(blockId) + if (wrongFlashTimerRef.current) { + clearTimeout(wrongFlashTimerRef.current) + wrongFlashTimerRef.current = null + } + setWrongFlashBlockId(null) + + // 如果点击的是错误区块,增加找到的错误数 + if (block.isError) { + newClickedBlocks.add(blockId) + setClickedBlocks(newClickedBlocks) + const newFoundErrors = foundErrors + 1 + setFoundErrors(newFoundErrors) + + // 检查是否找全了所有错误 + if (newFoundErrors === errorCount) { + setIsCompleted(true) + } + } else { + setWrongFlashBlockId(blockId) + wrongFlashTimerRef.current = setTimeout(() => { + setWrongFlashBlockId(null) + wrongFlashTimerRef.current = null + }, 500) + } + } + + // 获取区块的显示状态 + const getBlockStatus = (blockId: number) => { + if (!clickedBlocks.has(blockId)) return 'unclicked' + + const block = blocks.find((b) => b.id === blockId) + if (!block) return 'unclicked' + + return block.isError ? 'found-error' : 'wrong-selection' + } + + if (!visible) return null + + const feedbackBlock = blocks.find((block) => block.id === feedbackBlockId) + const feedbackStatus = feedbackBlock?.isError ? 'found-error' : 'wrong-selection' + + return ( +
+
+ {/* 题目区域 */} +
+
+ {/*
+ 纠错 +
*/} +
+
+ {title} + + 已找到 {foundErrors} / {errorCount} 处错误 + +
+ {introText && ( +
+ {introText} +
+ )} +
+
+ + {feedbackBlock && ( +
+ {/* + {feedbackStatus === 'found-error' ? '找到疑点:' : '不是疑点:'} + */} + {feedbackBlock.feedback} +
+ )} +
+ + {/* 文本区块(横向排列) */} +
+ {blocks.map((block) => { + const status = getBlockStatus(block.id) + const isClicked = clickedBlocks.has(block.id) + const isWrongFlashing = wrongFlashBlockId === block.id + + return ( +
+ {/* 区块本身 */} +
handleBlockClick(block.id)} + > + {isWrongFlashing && ( +
+ 再看看 +
+ )} + {status === 'found-error' && ( +
+ 已标记 +
+ )} + {status === 'wrong-selection' && ( +
+ 再看看 +
+ )} + +
{block.text}
+
+
+ ) + })} +
+ + {/* 完成提示 */} + onSubmit?.(true)} + /> +
+
+ ) +} + +export default ErrorCorrectionQuizPanel diff --git a/src/components/layout/quiz/SortQuizPanel.tsx b/src/components/layout/quiz/SortQuizPanel.tsx new file mode 100644 index 0000000..f3f02eb --- /dev/null +++ b/src/components/layout/quiz/SortQuizPanel.tsx @@ -0,0 +1,276 @@ +import { useEffect, useState, useRef, useCallback } from 'react' +import { cn } from '@/lib/utils' +import type { SortQuizPanelProps } from './types' + +/** + * SortQuizPanel - 排序题面板 + * 支持拖拽排序,用于让用户按正确顺序排列选项 + */ +const SortQuizPanel = ({ + visible, + zIndex = 10000, + title, + items, + correctOrder, + errorMessage, + successMessage, + onSubmit, + onError, + disabled = false, + submitText = '提交', +}: SortQuizPanelProps) => { + // 当前排序状态 + const [currentOrder, setCurrentOrder] = useState([]) + const [error, setError] = useState('') + const [isSubmitted, setIsSubmitted] = useState(false) + + // 拖拽状态 + const [draggingIndex, setDraggingIndex] = useState(null) + const [dragOverIndex, setDragOverIndex] = useState(null) + const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null) + const dragOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) + const itemRefs = useRef<(HTMLDivElement | null)[]>([]) + const panelRef = useRef(null) + + // 初始化排序(使用items的初始顺序) + useEffect(() => { + if (visible && items.length > 0) { + setCurrentOrder(items.map((item) => item.id)) + setError('') + setIsSubmitted(false) + } + }, [visible, items]) + + const getLocalPoint = useCallback((clientX: number, clientY: number) => { + const panelElement = panelRef.current + if (!panelElement) return { x: clientX, y: clientY } + + const rect = panelElement.getBoundingClientRect() + const scaleX = rect.width / panelElement.offsetWidth || 1 + const scaleY = rect.height / panelElement.offsetHeight || 1 + + return { + x: (clientX - rect.left) / scaleX, + y: (clientY - rect.top) / scaleY, + } + }, []) + + const getLocalRect = useCallback((element: HTMLElement) => { + const panelElement = panelRef.current + if (!panelElement) return element.getBoundingClientRect() + + const panelRect = panelElement.getBoundingClientRect() + const rect = element.getBoundingClientRect() + const scaleX = panelRect.width / panelElement.offsetWidth || 1 + const scaleY = panelRect.height / panelElement.offsetHeight || 1 + + return { + left: (rect.left - panelRect.left) / scaleX, + top: (rect.top - panelRect.top) / scaleY, + width: rect.width / scaleX, + height: rect.height / scaleY, + right: (rect.right - panelRect.left) / scaleX, + bottom: (rect.bottom - panelRect.top) / scaleY, + } + }, []) + + const updateDragOverIndex = useCallback((clientX: number, clientY: number) => { + for (let i = 0; i < itemRefs.current.length; i++) { + const element = itemRefs.current[i] + if (!element) continue + + const rect = element.getBoundingClientRect() + if ( + clientX >= rect.left && + clientX <= rect.right && + clientY >= rect.top && + clientY <= rect.bottom + ) { + setDragOverIndex(i) + return + } + } + setDragOverIndex(null) + }, []) + + // 处理拖拽开始(Pointer Events 同时兼容鼠标、触摸和手写笔) + const handlePointerDown = (e: React.PointerEvent, index: number) => { + if (disabled || isSubmitted || e.button !== 0) return + + const itemElement = itemRefs.current[index] + if (!itemElement) return + + const localPoint = getLocalPoint(e.clientX, e.clientY) + const localRect = getLocalRect(itemElement) + dragOffsetRef.current = { + x: localPoint.x - localRect.left, + y: localPoint.y - localRect.top, + } + + setDraggingIndex(index) + setDragPosition(localPoint) + updateDragOverIndex(e.clientX, e.clientY) + + e.currentTarget.setPointerCapture(e.pointerId) + e.preventDefault() + } + + // 处理放下 + const handleDrop = useCallback(() => { + if (draggingIndex === null) return + + if (dragOverIndex !== null && dragOverIndex !== draggingIndex) { + const newOrder = [...currentOrder] + const removed = newOrder[draggingIndex] + if (removed === undefined) return + newOrder.splice(draggingIndex, 1) + newOrder.splice(dragOverIndex, 0, removed) + setCurrentOrder(newOrder) + } + + setDraggingIndex(null) + setDragOverIndex(null) + setDragPosition(null) + }, [currentOrder, dragOverIndex, draggingIndex]) + + // 处理拖拽移动 + useEffect(() => { + if (draggingIndex === null) return + + const handlePointerMove = (e: PointerEvent) => { + setDragPosition(getLocalPoint(e.clientX, e.clientY)) + updateDragOverIndex(e.clientX, e.clientY) + e.preventDefault() + } + + const handlePointerUp = () => { + handleDrop() + } + + document.addEventListener('pointermove', handlePointerMove, { passive: false }) + document.addEventListener('pointerup', handlePointerUp) + document.addEventListener('pointercancel', handlePointerUp) + + return () => { + document.removeEventListener('pointermove', handlePointerMove) + document.removeEventListener('pointerup', handlePointerUp) + document.removeEventListener('pointercancel', handlePointerUp) + } + }, [draggingIndex, getLocalPoint, handleDrop, updateDragOverIndex]) + + // 提交验证 + const handleSubmit = () => { + if (disabled || isSubmitted) return + + const isCorrect = JSON.stringify(currentOrder) === JSON.stringify(correctOrder) + + if (isCorrect) { + setError('') + setIsSubmitted(true) + onSubmit?.(currentOrder, true) + } else { + setError(errorMessage || '排序不正确,请重新调整顺序') + onError?.(errorMessage || '排序不正确,请重新调整顺序') + onSubmit?.(currentOrder, false) + } + } + + if (!visible) return null + + return ( +
+
+ {/* 题目标题 */} +
+
{title}
+ {error &&
{error}
} + {isSubmitted && successMessage && ( +
{successMessage}
+ )} +
+ + {/* 排序选项区域 */} +
+ {currentOrder.map((itemId, index) => { + const item = items.find((i) => i.id === itemId) + if (!item) return null + + const isDragging = draggingIndex === index + const isDragOver = dragOverIndex === index && dragOverIndex !== draggingIndex + + return ( +
{ + itemRefs.current[index] = el + }} + className={cn( + 'relative px-8 py-6 rounded-2xl border-3 transition-all duration-200 touch-none', + 'text-[28px] leading-relaxed select-none', + disabled || isSubmitted + ? 'cursor-not-allowed opacity-60' + : 'cursor-grab active:cursor-grabbing', + isDragging + ? 'opacity-50 scale-95' + : isDragOver + ? 'border-[#f0be75] bg-[#f0be75]/20 scale-105' + : 'border-[#d4c5a9] bg-[#d7b888] text-[#5c3e2e] hover:border-[#f0be75] hover:shadow-lg' + )} + onPointerDown={(e) => handlePointerDown(e, index)} + > +
+ {/* 拖拽手柄图标 */} +
+
+
+
+
+
{item.label}
+
+
+ ) + })} +
+ + {/* 提交按钮 */} + {!isSubmitted && ( + + )} + + {/* 拖拽预览 */} + {draggingIndex !== null && dragPosition && ( +
+
+ {items.find((i) => i.id === currentOrder[draggingIndex])?.label} +
+
+ )} +
+
+ ) +} + +export default SortQuizPanel diff --git a/src/components/layout/quiz/index.ts b/src/components/layout/quiz/index.ts index 6eb139f..5fd1e56 100644 --- a/src/components/layout/quiz/index.ts +++ b/src/components/layout/quiz/index.ts @@ -5,6 +5,8 @@ export { default as DragQuizPanel } from './DragQuizPanel' export { default as BasketQuizPanel } from './BasketQuizPanel' export { default as MatchQuizPanel } from './MatchQuizPanel' export { default as SelectFillQuizPanel } from './SelectFillQuizPanel' +export { default as SortQuizPanel } from './SortQuizPanel' +export { default as ErrorCorrectionQuizPanel } from './ErrorCorrectionQuizPanel' export type { SingleChoiceQuizPanelProps, MultipleChoiceQuizPanelProps, @@ -15,6 +17,10 @@ export type { SelectFillQuizPanelProps, SelectFillBlank, SelectFillResult, + SortQuizPanelProps, + SortQuizItem, + ErrorCorrectionQuizPanelProps, + ErrorCorrectionBlock, } from './types' // 保留旧组件以保持向后兼容 diff --git a/src/components/layout/quiz/types.ts b/src/components/layout/quiz/types.ts index cad1433..4d1289b 100644 --- a/src/components/layout/quiz/types.ts +++ b/src/components/layout/quiz/types.ts @@ -37,6 +37,12 @@ export interface DragQuizPanelProps { feedbackType?: 'success' | 'error' /** 值变化回调 */ onChange: (value: DragFillResult[]) => void + /** 所有空白填满后点击提交的回调 */ + onSubmit?: (value: DragFillResult[]) => void + /** 提交按钮文本 */ + submitText?: string + /** 是否禁用拖拽和提交 */ + disabled?: boolean } /** 单选题面板配置 */ @@ -192,6 +198,72 @@ export interface SelectFillQuizPanelProps { submitText?: string } +/** 排序题项 */ +export interface SortQuizItem { + /** 唯一标识 */ + id: string + /** 显示文本 */ + label: string +} + +/** 排序题面板配置 */ +export interface SortQuizPanelProps { + /** 是否可见 */ + visible: boolean + /** z-index 层级 */ + zIndex?: number + /** 题目 */ + title: string + /** 待排序的选项列表(初始顺序) */ + items: SortQuizItem[] + /** 正确的排序顺序(id数组) */ + correctOrder: string[] + /** 错误提示信息 */ + errorMessage?: string + /** 成功提示信息(可选) */ + successMessage?: string + /** 提交回调,返回用户排序和是否正确 */ + onSubmit?: (order: string[], isCorrect: boolean) => void + /** 错误回调(可选) */ + onError?: (error: string) => void + /** 是否禁用 */ + disabled?: boolean + /** 提交按钮文本 */ + submitText?: string +} + +/** 纠错题区块 */ +export interface ErrorCorrectionBlock { + /** 区块ID */ + id: number + /** 区块文本内容 */ + text: string + /** 是否是错误 */ + isError: boolean + /** 点击后的反馈文本 */ + feedback: string +} + +/** 纠错题面板配置 */ +export interface ErrorCorrectionQuizPanelProps { + /** 是否可见 */ + visible: boolean + /** z-index 层级 */ + zIndex?: number + /** 题目 */ + title: string + /** 情景介绍文本(可选) */ + introText?: string + /** 文本区块列表 */ + blocks: ErrorCorrectionBlock[] + /** 需要找出的错误数量 */ + errorCount: number + /** 提交回调(找到所有错误时自动调用) */ + onSubmit?: (isCorrect: boolean) => void + /** 是否禁用 */ + disabled?: boolean +} + /** @deprecated 使用 SingleChoiceQuizPanelProps 替代 */ export interface QuizPanelProps { visible: boolean diff --git a/src/components/letter/LetterTemplate.tsx b/src/components/letter/LetterTemplate.tsx index 412a8d3..acb4717 100644 --- a/src/components/letter/LetterTemplate.tsx +++ b/src/components/letter/LetterTemplate.tsx @@ -237,7 +237,7 @@ const LetterTemplate = ({ } else if (/\d/.test(value)) { showHint('不能包含数字') } else if (/[\p{P}\p{S}]/u.test(value)) { - showHint('不能包含标点符号') + showHint('不能包含特殊符号') } } @@ -331,7 +331,7 @@ const LetterTemplate = ({ } else if (/\d/.test(value)) { showHint('不能包含数字') } else if (/[\p{P}\p{S}]/u.test(value)) { - showHint('不能包含标点符号') + showHint('不能包含特殊符号') } } @@ -549,14 +549,10 @@ const LetterTemplate = ({ />
-
- {title} +
+ {Array.from(title).map((char, index) => ( + {char} + ))}
diff --git a/src/components/letter/LetterToChenyun.tsx b/src/components/letter/LetterToChenyun.tsx index d7e9d83..845a1d2 100644 --- a/src/components/letter/LetterToChenyun.tsx +++ b/src/components/letter/LetterToChenyun.tsx @@ -47,16 +47,21 @@ const fields: Record = { 3: { type: 'input', maxLength: 12, - widthClassName: 'w-[180px]', - placeholder: '结合所学专业', + widthClassName: 'w-[120px]', readOnlyFallback: '专业', }, 4: { type: 'input', maxLength: 12, - widthClassName: 'w-[120px]', + widthClassName: 'w-[180px]', + placeholder: '结合所学专业', readOnlyFallback: '实干报国', }, + 5: { + type: 'input', + maxLength: 12, + widthClassName: 'w-[120px]', + }, } const LetterToChenyun = ({ diff --git a/src/components/video/video.tsx b/src/components/video/video.tsx index b6329a6..70351d8 100644 --- a/src/components/video/video.tsx +++ b/src/components/video/video.tsx @@ -575,8 +575,14 @@ const Video = forwardRef( // 计算进度百分比 const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0 - const disableFirstWatchVideoGating = + + // 视频首次观看限制:env 默认值可被调试面板运行时覆盖 + const envDisableGating = import.meta.env.VITE_DISABLE_FIRST_WATCH_VIDEO_GATING === 'true' + const sessionOverride = window.sessionStorage.getItem('debug:video-gating') + const disableFirstWatchVideoGating = + sessionOverride !== null ? sessionOverride === 'disabled' : envDisableGating + const canShowSkipButton = !skipKey || disableFirstWatchVideoGating || isVideoCompleted const canShowControls = !controlsOnReplay || disableFirstWatchVideoGating || isVideoCompleted const shouldShowReadonlyProgress = @@ -642,6 +648,43 @@ const Video = forwardRef( style={{ transform: `scaleX(${progressPercent / 100})` }} />
+ {/* 锚点标记:纵向竖标 + 圆点 */} + {visibleAnchors.map((anchor) => { + const anchorPercent = duration > 0 ? (anchor.time / duration) * 100 : 0 + const isPassed = currentTime >= anchor.time + return ( +
+ {/* 竖线 */} +
+ {/* 圆点 */} +
+
+ ) + })}
)} {/* 跳过按钮 */} diff --git a/src/config/puzzleData.ts b/src/config/puzzleData.ts index af1860d..96d0550 100644 --- a/src/config/puzzleData.ts +++ b/src/config/puzzleData.ts @@ -357,7 +357,7 @@ export const puzzleDataConfig: Record> = route: 'place4', }, 13: { - title: '冯雪峰与鲁迅谈话的资料图', + title: '鲁迅全家与冯雪峰全家合影', detailImage: 'luxun/details/13', route: 'place4', }, @@ -419,7 +419,7 @@ export const puzzleDataConfig: Record> = route: 'place4', }, 9: { - title: '中共四大立碑处', + title: '中共四大遗址立碑处', detailImage: 'maozedong/details/9', route: 'place4', }, @@ -476,7 +476,7 @@ export const puzzleDataConfig: Record> = route: 'place1', }, 4: { - title: '英雄儿女胶片', + title: '风云儿女胶片', detailImage: 'nieer/details/4', route: 'place2', }, @@ -486,7 +486,7 @@ export const puzzleDataConfig: Record> = route: 'place2', }, 6: { - title: '聂耳手写《义勇军进行曲》', + title: '《义勇军进行曲》\n曲谱的手稿', detailImage: 'nieer/details/6', route: 'place3', }, @@ -526,7 +526,7 @@ export const puzzleDataConfig: Record> = route: 'place6', }, 14: { - title: '唱国歌一定要站起来', + title: '定格阅兵瞬间', detailImage: 'nieer/details/14', route: 'reflection', }, diff --git a/src/debug/progressionDebug.ts b/src/debug/progressionDebug.ts index ebae40b..65735cc 100644 --- a/src/debug/progressionDebug.ts +++ b/src/debug/progressionDebug.ts @@ -1,6 +1,7 @@ import { puzzleDataConfig } from '@/config/puzzleData' import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences' import { usePuzzleStore } from '@/store/puzzleStore' +import { savePuzzleStore } from '@/store/puzzleStore' const MODE_KEY = 'debug:progression-mode' const BACKUP_KEY = 'debug:progression-real-puzzles-backup' @@ -20,6 +21,21 @@ type BadgePosition = { offset: number } +/** 故居碎片按 route 前缀分三阶段 */ +type StageKey = 'dialogue' | 'reflection' | 'internalization' + +interface StageConfig { + key: StageKey + label: string + routePrefix: string +} + +const STAGES: StageConfig[] = [ + { key: 'dialogue', label: '对话', routePrefix: 'place' }, + { key: 'reflection', label: '体悟', routePrefix: 'reflection' }, + { key: 'internalization', label: '内化', routePrefix: 'internalization' }, +] + const serializePuzzles = (puzzles: Record>) => { return Object.fromEntries( Object.entries(puzzles).map(([residence, pieces]) => [residence, [...pieces]]) @@ -62,6 +78,91 @@ const buildAllCompletedPuzzles = () => { return puzzles } +/** + * 获取指定故居各阶段的碎片 ID 集合 + */ +const getStagePieceIds = (residence: string) => { + const residencePieces = puzzleDataConfig[residence] + const result: Record = { + dialogue: [], + reflection: [], + internalization: [], + } + if (!residencePieces) return result + + for (const [pieceId, data] of Object.entries(residencePieces)) { + for (const stage of STAGES) { + if (data.route.startsWith(stage.routePrefix)) { + result[stage.key].push(Number(pieceId)) + break + } + } + } + return result +} + +/** + * 获取某个故居在某阶段已收集的碎片数 / 总数 + */ +const getStageProgress = (residence: string, stageKey: StageKey): { collected: number; total: number } => { + const stagePieceIds = getStagePieceIds(residence)[stageKey] + const currentPieces = usePuzzleStore.getState().puzzles[residence] ?? new Set() + const collected = stagePieceIds.filter((id) => currentPieces.has(id)).length + return { collected, total: stagePieceIds.length } +} + +/** + * 确保目标故居的前序故居碎片完整(保证解锁关系),并合并当前已有碎片。 + */ +const buildPuzzlesWithAncestors = (residence: string): Record> => { + const targetIndex = PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence) + + const puzzles: Record> = {} + for (let i = 0; i < targetIndex; i++) { + const prev = PUZZLE_RESIDENCES[i]?.residence + if (!prev) continue + puzzles[prev] = new Set(getAllPieceIds(prev)) + } + + const currentPuzzles = usePuzzleStore.getState().puzzles + for (const [res, pieces] of Object.entries(currentPuzzles)) { + if (res === residence) continue + if (!puzzles[res]) { + puzzles[res] = pieces + } else { + puzzles[res] = new Set([...puzzles[res], ...pieces]) + } + } + return puzzles +} + +/** + * 直接切换一个故居某个阶段的碎片收集状态(无确认、无刷新) + */ +const toggleResidenceStage = (residence: string, stageKey: StageKey) => { + const stagePieceIds = getStagePieceIds(residence)[stageKey] + if (stagePieceIds.length === 0) return + + const currentPieces = usePuzzleStore.getState().puzzles[residence] ?? new Set() + const allCollected = stagePieceIds.every((id) => currentPieces.has(id)) + const puzzles = buildPuzzlesWithAncestors(residence) + + if (allCollected) { + const remaining = new Set() + for (const id of currentPieces) { + if (!stagePieceIds.includes(id)) { + remaining.add(id) + } + } + puzzles[residence] = remaining + } else { + puzzles[residence] = new Set([...currentPieces, ...stagePieceIds]) + } + + usePuzzleStore.setState({ puzzles }) + savePuzzleStore(puzzles) +} + const getStoredMode = (): DebugMode => { const raw = window.sessionStorage.getItem(MODE_KEY) if (!raw) return { type: 'real' } @@ -91,6 +192,10 @@ const getModeLabel = (mode = getStoredMode()) => { return '真实进度' } +const getResidenceLabel = (residence: string) => { + return PUZZLE_RESIDENCES.find((r) => r.residence === residence)?.displayName ?? residence +} + const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max) const getStoredBadgePosition = (): BadgePosition => { @@ -224,6 +329,63 @@ const removePanel = () => { document.getElementById(PANEL_ID)?.remove() } +// ========== 面板渲染 ========== + +const renderStageButton = ( + residence: string, + displayName: string, + stage: StageConfig, +): HTMLButtonElement => { + const { collected, total } = getStageProgress(residence, stage.key) + const isFull = total > 0 && collected >= total + + const btn = document.createElement('button') + btn.type = 'button' + btn.textContent = `${stage.label} ${collected}/${total}` + btn.style.flex = '1' + btn.style.border = '1px solid' + btn.style.borderRadius = '4px' + btn.style.fontSize = '10px' + btn.style.padding = '3px 4px' + btn.style.cursor = total > 0 ? 'pointer' : 'default' + btn.style.flexShrink = '0' + btn.style.opacity = total > 0 ? '1' : '0.3' + + if (total === 0) { + btn.style.borderColor = 'rgba(255,255,255,0.08)' + btn.style.background = 'rgba(255,255,255,0.04)' + btn.style.color = 'rgba(255,255,255,0.3)' + btn.title = `${displayName} 没有「${stage.label}」阶段` + } else if (isFull) { + btn.style.borderColor = 'rgba(100,220,120,0.5)' + btn.style.background = 'rgba(80,200,100,0.18)' + btn.style.color = '#99e8aa' + btn.title = `「${stage.label}」已完成(${total} 块)。点击移除。` + } else if (collected > 0) { + btn.style.borderColor = 'rgba(255,200,80,0.5)' + btn.style.background = 'rgba(255,180,60,0.15)' + btn.style.color = '#ffd080' + btn.title = `「${stage.label}」部分完成(${collected}/${total})。点击补全。` + } else { + btn.style.borderColor = 'rgba(100,180,255,0.4)' + btn.style.background = 'rgba(80,150,255,0.12)' + btn.style.color = '#99ccff' + btn.title = `「${stage.label}」未开始。点击补全(${total} 块)。` + } + + if (total > 0) { + btn.addEventListener('click', (e) => { + e.stopPropagation() + toggleResidenceStage(residence, stage.key) + // 即时刷新面板按钮状态 + removePanel() + togglePanel() + }) + } + + return btn +} + const togglePanel = () => { const existingPanel = document.getElementById(PANEL_ID) if (existingPanel) { @@ -237,7 +399,7 @@ const togglePanel = () => { panel.style.right = '12px' panel.style.top = '72px' panel.style.zIndex = '99999' - panel.style.width = '260px' + panel.style.width = '320px' panel.style.maxHeight = '70vh' panel.style.overflowY = 'auto' panel.style.padding = '10px' @@ -254,11 +416,86 @@ const togglePanel = () => { panel.appendChild(title) const description = document.createElement('div') - description.textContent = '只写本地 puzzleStore,不调用易班保存。' + description.textContent = '只写本地 store,不调用易班保存。点击阶段按钮即时生效。' description.style.opacity = '0.75' - description.style.marginBottom = '8px' + description.style.marginBottom = '6px' panel.appendChild(description) + // ===== 视频首次观看限制开关 ===== + const gatingSection = document.createElement('div') + gatingSection.style.display = 'flex' + gatingSection.style.alignItems = 'center' + gatingSection.style.gap = '6px' + gatingSection.style.marginBottom = '8px' + gatingSection.style.padding = '6px 8px' + gatingSection.style.borderRadius = '6px' + gatingSection.style.background = 'rgba(255,255,255,0.06)' + + const gatingLabel = document.createElement('span') + gatingLabel.textContent = '视频首次限制:' + gatingLabel.style.fontSize = '11px' + gatingLabel.style.opacity = '0.8' + gatingSection.appendChild(gatingLabel) + + const sessionOverride = window.sessionStorage.getItem('debug:video-gating') + const envDisableGating = import.meta.env.VITE_DISABLE_FIRST_WATCH_VIDEO_GATING === 'true' + // sessionOverride: null=跟随env, 'enabled'=强制首次(无进度条/跳过), 'disabled'=强制关闭(有进度条/跳过) + const isGatingOn = sessionOverride !== null + ? sessionOverride === 'enabled' + : !envDisableGating + + const gatingBtn = document.createElement('button') + gatingBtn.type = 'button' + gatingBtn.textContent = isGatingOn ? '开启(无进度条)' : '关闭(有进度条)' + gatingBtn.title = isGatingOn + ? '当前:模拟首次学习体验(视频不可跳过、无进度条)。点击切换。' + : '当前:跳过首次学习限制(视频可跳过、有进度条)。点击切换。' + gatingBtn.style.border = isGatingOn + ? '1px solid rgba(255,200,80,0.5)' + : '1px solid rgba(100,200,120,0.5)' + gatingBtn.style.borderRadius = '4px' + gatingBtn.style.background = isGatingOn + ? 'rgba(255,180,60,0.2)' + : 'rgba(80,200,100,0.18)' + gatingBtn.style.color = isGatingOn ? '#ffd080' : '#99e8aa' + gatingBtn.style.fontSize = '11px' + gatingBtn.style.padding = '4px 8px' + gatingBtn.style.cursor = 'pointer' + gatingBtn.style.flex = '1' + gatingBtn.addEventListener('click', (e) => { + e.stopPropagation() + if (isGatingOn) { + window.sessionStorage.setItem('debug:video-gating', 'disabled') + } else { + window.sessionStorage.setItem('debug:video-gating', 'enabled') + } + removePanel() + togglePanel() + }) + gatingSection.appendChild(gatingBtn) + + const resetGatingBtn = document.createElement('button') + resetGatingBtn.type = 'button' + resetGatingBtn.textContent = '重置' + resetGatingBtn.title = `恢复跟随环境变量(VITE_DISABLE_FIRST_WATCH_VIDEO_GATING=${envDisableGating ? 'true' : 'false'})` + resetGatingBtn.style.border = '1px solid rgba(255,255,255,0.2)' + resetGatingBtn.style.borderRadius = '4px' + resetGatingBtn.style.background = 'rgba(255,255,255,0.08)' + resetGatingBtn.style.color = '#aaa' + resetGatingBtn.style.fontSize = '10px' + resetGatingBtn.style.padding = '4px 6px' + resetGatingBtn.style.cursor = 'pointer' + resetGatingBtn.addEventListener('click', (e) => { + e.stopPropagation() + window.sessionStorage.removeItem('debug:video-gating') + removePanel() + togglePanel() + }) + gatingSection.appendChild(resetGatingBtn) + + panel.appendChild(gatingSection) + + // ===== 全局模式按钮 ===== const list = document.createElement('div') list.style.display = 'flex' list.style.flexDirection = 'column' @@ -266,14 +503,73 @@ const togglePanel = () => { list.appendChild(createButton('真实进度', () => applyMode({ type: 'real' }))) + // ===== 按故居分阶段控制 ===== + const residenceSection = document.createElement('div') + residenceSection.style.display = 'flex' + residenceSection.style.flexDirection = 'column' + residenceSection.style.gap = '4px' + + const sectionTitle = document.createElement('div') + sectionTitle.textContent = '— 各故居阶段控制 —' + sectionTitle.style.fontSize = '11px' + sectionTitle.style.opacity = '0.6' + sectionTitle.style.marginTop = '4px' + sectionTitle.style.marginBottom = '2px' + residenceSection.appendChild(sectionTitle) + for (const residence of PUZZLE_RESIDENCES) { - list.appendChild( - createButton(`到达:${residence.displayName}`, () => - applyMode({ type: 'arrived', residence: residence.residence, label: residence.displayName }) - ) - ) + // 名称行 + 到达按钮 + const row1 = document.createElement('div') + row1.style.display = 'flex' + row1.style.gap = '4px' + row1.style.alignItems = 'center' + + const label = document.createElement('span') + label.textContent = residence.displayName + label.style.flex = '1' + label.style.fontSize = '11px' + label.style.fontWeight = 'bold' + label.style.opacity = '0.9' + label.style.overflow = 'hidden' + label.style.textOverflow = 'ellipsis' + label.style.whiteSpace = 'nowrap' + row1.appendChild(label) + + const arrivedBtn = document.createElement('button') + arrivedBtn.type = 'button' + arrivedBtn.textContent = '到达' + arrivedBtn.title = '模拟到达该故居(前序全部完成,该故居未开始)' + arrivedBtn.style.border = '1px solid rgba(255,255,255,0.25)' + arrivedBtn.style.borderRadius = '4px' + arrivedBtn.style.background = 'rgba(255,255,255,0.1)' + arrivedBtn.style.color = '#ddd' + arrivedBtn.style.fontSize = '10px' + arrivedBtn.style.padding = '2px 5px' + arrivedBtn.style.cursor = 'pointer' + arrivedBtn.style.flexShrink = '0' + arrivedBtn.addEventListener('click', (e) => { + e.stopPropagation() + applyMode({ type: 'arrived', residence: residence.residence, label: residence.displayName }) + }) + row1.appendChild(arrivedBtn) + + residenceSection.appendChild(row1) + + // 阶段按钮行 + const row2 = document.createElement('div') + row2.style.display = 'flex' + row2.style.gap = '4px' + row2.style.marginBottom = '6px' + + for (const stage of STAGES) { + row2.appendChild(renderStageButton(residence.residence, residence.displayName, stage)) + } + + residenceSection.appendChild(row2) } + list.appendChild(residenceSection) + list.appendChild(createButton('全流程完成', () => applyMode({ type: 'allCompleted' }))) panel.appendChild(list) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c4addc8..e34cead 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,11 +3,8 @@ export { usePageScale } from './usePageScale' export { useTouch } from './useTouch' export { useZoomAndDrag } from './useZoomAndDrag' export { useCursorController } from './useCursorController' -export { useResidenceContinue } from './useResidenceContinue' +export { useResidenceContinue, setPreviousPath } from './useResidenceContinue' export type { UseTouchOptions, UseTouchReturn, TouchEventData, SwipeDirection } from './useTouch' export type { UseZoomAndDragOptions, UseZoomAndDragReturn } from './useZoomAndDrag' export type { CursorControllerConfig } from './useCursorController' -export type { - UseResidenceContinueParams, - UseResidenceContinueResult, -} from './useResidenceContinue' +export type { UseResidenceContinueParams, UseResidenceContinueResult } from './useResidenceContinue' diff --git a/src/hooks/useResidenceContinue.ts b/src/hooks/useResidenceContinue.ts index cfda116..e7cf6ca 100644 --- a/src/hooks/useResidenceContinue.ts +++ b/src/hooks/useResidenceContinue.ts @@ -16,11 +16,16 @@ import { useVideoContextSafe } from '@/contexts/VideoContext' const promptedResidences = new Set() /** - * 记录上一次访问的路由,用于判断是否刚从场景离开。 - * 模块级变量,跨 hook 实例共享。 + * 记录上次 ResidenceLayout 访问的路径,由 ResidenceLayout 在路由变化时更新。 + * 用于判断 Home 页面之前是否来自本故居的 place/reflection/internalization/draw 场景。 */ let previousPath = '' +/** 由 ResidenceLayout 调用,记录每次路由变化的路径 */ +export const setPreviousPath = (path: string) => { + previousPath = path +} + export interface UseResidenceContinueParams { /** 故居标识(同时也是路由里的 starName),如 'maozedong' */ residence: string @@ -50,7 +55,7 @@ export interface UseResidenceContinueResult { * 3. 本会话内该故居尚未自动弹过 * 4. 故居已解锁 && 已开始 && 未全部完成 * 5. 能解析出有效的续学目标 - * 6. 不是刚从本故居场景页面离开的(避免干扰正常流程) + * 6. 上一页不是本故居的 place/reflection/internalization/draw(避免从场景返回时误弹) * * 弹一次后即标记,之后再回到 home 不再自动弹(由首页的三个阶段按钮继续)。 */ @@ -79,21 +84,15 @@ export function useResidenceContinue({ // 没开始过的新故居不打扰,让学生自己点「一」正常开始 if (!isResidenceStarted(residence, puzzles)) return - // 检查上一个路由:如果是从本故居的 place/reflection/internalization 来的,不弹 - // 例如从 /revolutionary/maozedong/place1 回到 /revolutionary/maozedong/home - const currentPath = location.pathname - const isFromResidenceScene = + // 如果上一页是本故居的 place/reflection/internalization/draw,不弹 + // 例如从 /revolutionary/maozedong/place4 回到 /revolutionary/maozedong/home + if ( previousPath && (previousPath.includes(`/${residence}/place`) || previousPath.includes(`/${residence}/reflection`) || previousPath.includes(`/${residence}/internalization`) || previousPath.includes(`/${residence}/draw`)) - - // 更新 previousPath 供下次判断 - previousPath = currentPath - - if (isFromResidenceScene) { - // 刚从场景离开,不弹窗 + ) { return } diff --git a/src/lib/utils/api-common.ts b/src/lib/utils/api-common.ts index 5e7f6e6..c056b85 100644 --- a/src/lib/utils/api-common.ts +++ b/src/lib/utils/api-common.ts @@ -184,6 +184,17 @@ export async function getUserDefineResult( return getUserDefineResultAsyncInternal(lessonId, key, options) } +export function parseUserDefineContent(content: string): T { + const raw = JSON.parse(content) + if (useUserStore.getState().yibanConnectionStatus === 'connected') { + return raw as T + } + if (raw && typeof raw === 'object' && 'state' in raw) { + return (raw as { state: T }).state + } + return raw as T +} + // 根据课时顺序号(所处课程的第几课时)更新对应易班平台上的每个环节进度(异步) export async function setCourseTimeProgress(order: number): Promise { if (useUserStore.getState().yibanConnectionStatus !== 'connected') { diff --git a/src/lib/utils/sensitiveWordList.ts b/src/lib/utils/sensitiveWordList.ts new file mode 100644 index 0000000..a5eaa10 --- /dev/null +++ b/src/lib/utils/sensitiveWordList.ts @@ -0,0 +1,2191 @@ +/** + * 敏感词屏蔽字库 + * 部分词条参考 Konsheng/Sensitive-lexicon,MIT License + */ + +// 基础不文明用语 +const BASIC_WORDS = [ + '垃圾', + '脏话', + '骂人', + '辱骂', + '仇恨', + '傻逼', + '煞笔', + '傻b', + '傻比', + 'sb', + 'SB', + '草泥马', + '操', + '妈的', + '他妈', + '你妈', + '去死', + '找死', + '该死', + '白痴', + '智障', + '弱智', + '脑残', + '废物', + '贱', + '滚', +] + +// 暴力与自伤风险 +const VIOLENCE_WORDS = [ + '暴力', + '恐怖', + '杀人', + '杀害', + '血腥', + '虐待', + '城管暴力执法', + '杀毙', + '恐怖分子', + '恐怖份子', + '集体自杀', + 'zi杀', + '电话追杀系统', + '自杀', + '畏罪自杀。', + '恐怖分子傻瓜手册', + '爆破和杀伤燃烧弹', + '恐怖分子自己制造炸弹', + '完全自杀手册', + '红色恐怖', +] + +// 色情低俗 +const PORNOGRAPHY_WORDS = [ + '色情', + '涉黄', + '黄色内容', + '黄色网站', + '淫秽', + '成人视频', + '卖淫', + '嫖娼', + '约炮', + '爱液', + '按摩棒', + '拔出来', + '爆草', + '包二奶', + '暴干', + '暴奸', + '暴乳', + '爆乳', + '暴淫', + '被操', + '被插', + '被干', + '逼奸', + '仓井空', + '插暴', + '操逼', + '操黑', + '操烂', + '肏你', + '肏死', + '操死', + '操我', + '厕奴', + '插比', + '插b', + '插逼', + '插进', + '插你', + '插我', + '插阴', + '潮吹', + '潮喷', + '成人电影', + '成人论坛', + '成人色情', + '成人网站', + '成人文学', + '成人小说', + '艳情小说', + '成人游戏', + '吃精', + '抽插', + '春药', + '大波', + '大力抽送', + '大乳', + '荡妇', + '荡女', + '盗撮', + '发浪', + '放尿', + '肥逼', + '粉穴', + '风月大陆', + '干死你', + '干穴', + '肛交', + '肛门', + '龟头', + '裹本', + '国产av', + '好嫩', + '豪乳', + '黑逼', + '后庭', + '后穴', + '虎骑', + '换妻俱乐部', + '黄片', + '几吧', + '鸡吧', + '鸡巴', + '鸡奸', + '妓女', + '奸情', + '叫床', + '脚交', + '精液', + '就去日', + '巨屌', + '菊花洞', + '菊门', + '巨奶', + '巨乳', + '菊穴', + '开苞', + '口爆', + '口活', + '口交', + '口射', + '口淫', + '裤袜', + '狂操', + '狂插', + '浪逼', + '浪妇', + '浪叫', + '浪女', + '狼友', + '聊性', + '凌辱', + '漏乳', + '露b', + '乱交', + '乱伦', + '轮暴', + '轮操', + '轮奸', + '裸陪', + '买春', + '美逼', + '美少妇', + '美乳', + '美腿', + '美穴', + '美幼', + '秘唇', + '迷奸', + '密穴', + '蜜穴', + '蜜液', + '摸奶', + '摸胸', + '母奸', + '奈美', + '奶子', + '男奴', + '内射', + '嫩逼', + '嫩女', + '嫩穴', + '捏弄', + '女优', + '炮友', + '砲友', + '喷精', + '屁眼', + '前凸后翘', + '强jian', + '强暴', + '强奸处女', + '情趣用品', + '情色', + '拳交', + '全裸', + '群交', + '人妻', + '人兽', + '日逼', + '日烂', + '肉棒', + '肉逼', + '肉唇', + '肉洞', + '肉缝', + '肉棍', + '肉茎', + '肉具', + '揉乳', + '肉穴', + '肉欲', + '乳爆', + '乳房', + '乳沟', + '乳交', + '乳头', + '骚逼', + '骚比', + '骚女', + '骚水', + '骚穴', + '色逼', + '色界', + '色猫', + '色盟', + '色情网站', + '色区', + '色色', + '色诱', + '色欲', + '色b', + '少年阿宾', + '射爽', + '射颜', + '食精', + '释欲', + '兽奸', + '兽交', + '手淫', + '兽欲', + '熟妇', + '熟母', + '熟女', + '爽片', + '双臀', + '死逼', + '丝袜', + '丝诱', + '松岛枫', + '酥痒', + '汤加丽', + '套弄', + '体奸', + '体位', + '舔脚', + '舔阴', + '调教', + '偷欢', + '推油', + '脱内裤', + '文做', + '舞女', + '无修正', + '吸精', + '夏川纯', + '相奸', + '小逼', + '校鸡', + '小穴', + '小xue', + '性感妖娆', + '性感诱惑', + '性虎', + '性饥渴', + '性技巧', + '性交', + '性奴', + '性虐', + '性息', + '性欲', + '胸推', + '穴口', + '穴图', + '亚情', + '颜射', + '阳具', + '杨思敏', + '要射了', + '夜勤病栋', + '一本道', + '一夜欢', + '一夜情', + '一ye情', + '阴部', + '淫虫', + '阴唇', + '淫荡', + '阴道', + '淫电影', + '阴阜', + '淫妇', + '淫河', + '阴核', + '阴户', + '淫贱', + '淫叫', + '淫教师', + '阴茎', + '阴精', + '淫浪', + '淫媚', + '淫糜', + '淫魔', + '淫母', + '淫女', + '淫虐', + '淫妻', + '淫情', + '淫色', + '淫声浪语', + '淫兽学园', + '淫书', + '淫术炼金士', + '淫水', + '淫娃', + '淫威', + '淫亵', + '淫样', + '淫液', + '淫照', + '阴b', + '应召', + '幼交', + '欲火', + '欲女', + '玉乳', + '玉穴', + '援交', + '原味内衣', + '援助交际', + '招鸡', + '招妓', + '抓胸', + '自慰', + '作爱', + 'a片', + 'fuck', + 'gay片', + 'g点', + 'h动画', + 'h动漫', + '失身粉', + '淫荡自慰器', + '小姐', + '包夜', + '3P', + '楼凤', + '足交', + 'SM', + '桑拿', + '吞精', + '咪咪', + '乳方', + '性伴侣', +] + +// 赌博博彩 +const GAMBLING_WORDS = [ + '赌博', + '博彩', + '赌场', + '下注', + '赌球', + '六合彩', + '黄赌毒', + '戈博军刀', + '足球投注', + '博彩娱', + '赌球网', + '外围赌球', +] + +// 毒品相关 +const DRUG_WORDS = [ + '毒品', + '吸毒', + '贩毒', + '制毒', + '冰毒', + '海洛因', + '大麻', + '摇头丸', + '毒豺', + '台毒', + '藏毒', + '江毒', + '中共邪毒素', + '新型毒品', + '麻古', + 'k粉', + '毒龙', +] + +// 违法广告与诈骗 +const ILLEGAL_ADS_AND_SCAMS_WORDS = [ + '诈骗', + '邪教', + '反动', + '传销', + '洗钱', + '走私', + '私人侦探', + '私家侦探', + '针孔摄象', + '无抵押贷款', + '代孕妈妈', + '代开发票', + '地下钱庄', + '替考试', + '出售答案', + '考中答案', + '质押贷款', + '小额贷款', + '办理文凭', + '办理证书', + '办证', + '保过答案', + '出售发票', + '达毕业证', + '答案包', + '答案提供', + '代办发票', + '代考', + '代理发票', + '代孕', + '发票出', + '发票代', + '发票销', + '级答案', + '究生答案', + '考答案', + '考前答案', + '考试答案', + '卖发票', + '实毕业证', + '手答案', + '替考', + '务员答案', + '种学历证', + '作弊器', + '发票', +] + +// 政治敏感 +const POLITICAL_WORDS = [ + '法轮功', + '习近平', + '习主席', + '习大大', + '国家主席习近平', + '总书记习近平', + '李强', + '赵乐际', + '王沪宁', + '蔡奇', + '丁薛祥', + '李希', + '韩正', + '胡锦涛', + '江泽民', + '邓小平', + '中共', + '共产党', + '中国共产党', + '党中央', + '政治局', + '中央军委', + '人大常委会', + '国务院', + '政协', + '国家领导人', + '领导人', + '总书记', + '国家主席', + '主席连任', + '宪法修正', + '修宪', + '一党专政', + '独裁', + '专制', + '民主运动', + '民运', + '六四', + '天安门事件', + '天安门运动', + '八九学运', + '占中', + '港独', + '台独', + '藏独', + '疆独', + '香港独立', + '台湾独立', + '西藏独立', + '新疆独立', + '达赖', + '达赖喇嘛', + '热比娅', + '民进党', + '国民党', + '一国两制', + '平近习', + 'xjp', + '习太子', + '习明泽', + '老习', + '温家宝', + '温加宝', + '温x', + '温jia宝', + '温宝宝', + '温加饱', + '温加保', + '张培莉', + '温云松', + '温如春', + '温jb', + '胡温', + '胡x', + '胡jt', + '胡boss', + '胡总', + '胡王八', + 'hujintao', + '胡jintao', + '胡j涛', + '胡惊涛', + '胡景涛', + '胡紧掏', + '湖紧掏', + '胡紧套', + '锦涛', + 'hjt', + '胡派', + '胡主席', + '刘永清', + '胡海峰', + '胡海清', + '民泽江', + '江胡', + '江哥', + '江主席', + '江书记', + '江浙闽', + '江沢民', + '江浙民', + '择民', + '则民', + '茳泽民', + 'zemin', + 'ze民', + '老江', + '老j', + '江core', + '江x', + '江派', + '江zm', + 'jzm', + '江戏子', + '江蛤蟆', + '江某某', + '江贼', + '江猪', + '江氏集团', + '江绵恒', + '江绵康', + '王冶坪', + '江泽慧', + '平小邓', + 'xiao平', + '邓xp', + '邓晓平', + '邓朴方', + '邓榕', + '邓质方', + '毛泽东', + '猫泽东', + '猫则东', + '猫贼洞', + '毛zd', + '毛zx', + 'z东', + 'ze东', + '泽d', + 'zedong', + '毛太祖', + '毛相', + '主席画像', + '改革历程', + '朱镕基', + '朱容基', + '朱镕鸡', + '朱容鸡', + '朱云来', + '李鹏', + '李peng', + '里鹏', + '李月月鸟', + '李小鹏', + '李小琳', + '华主席', + '华国', + '国锋', + '国峰', + '锋同志', + '白春礼', + '薄熙来', + '薄一波', + '蔡赴朝', + '蔡武', + '曹刚川', + '常万全', + '陈炳德', + '陈德铭', + '陈建国', + '陈良宇', + '陈绍基', + '陈同海', + '陈至立', + '戴秉国', + '丁一平', + '董建华', + '杜德印', + '杜世成', + '傅锐', + '郭伯雄', + '郭金龙', + '贺国强', + '胡春华', + '耀邦', + '华建敏', + '黄华华', + '黄丽满', + '黄兴国', + '回良玉', + '贾庆林', + '贾廷安', + '靖志远', + '李长春', + '李春城', + '李建国', + '李克强', + '李岚清', + '李沛瑶', + '李荣融', + '李瑞环', + '李铁映', + '李先念', + '李学举', + '李源潮', + '栗智', + '梁光烈', + '廖锡龙', + '林树森', + '林炎志', + '林左鸣', + '令计划', + '柳斌杰', + '刘奇葆', + '刘少奇', + '刘延东', + '刘云山', + '刘志军', + '龙新民', + '路甬祥', + '罗箭', + '吕祖善', + '马飚', + '马恺', + '孟建柱', + '欧广源', + '强卫', + '沈跃跃', + '宋平顺', + '粟戎生', + '苏树林', + '孙家正', + '铁凝', + '屠光绍', + '王东明', + '汪东兴', + '王鸿举', + '王乐泉', + '王洛林', + '王岐山', + '王胜俊', + '王太华', + '王学军', + '王兆国', + '王振华', + '吴邦国', + '吴定富', + '吴官正', + '无官正', + '吴胜利', + '吴仪', + '奚国华', + '习仲勋', + '徐才厚', + '许其亮', + '徐绍史', + '杨洁篪', + '叶剑英', + '由喜贵', + '于幼军', + '俞正声', + '袁纯清', + '曾培炎', + '曾庆红', + '曾宪梓', + '曾荫权', + '张德江', + '张定发', + '张高丽', + '张立昌', + '张荣坤', + '张志国', + '赵洪祝', + '紫阳', + '周生贤', + '周永康', + '朱海仑', + '中南海', + '大陆当局', + '中国当局', + '北京当局', + '党产共', + '共贪党', + '阿共', + '产党共', + '公产党', + '工产党', + '共c党', + '共x党', + '共铲', + '供产', + '共惨', + '供铲党', + '供铲谠', + '供铲裆', + '共残党', + '共残主义', + '共产主义的幽灵', + '拱铲', + '老共', + '中珙', + '中gong', + 'gc党', + '贡挡', + 'gong党', + 'g产', + '狗产蛋', + '共残裆', + '恶党', + '邪党', + '共产专制', + '共产王朝', + '裆中央', + '土共', + '土g', + '共狗', + 'g匪', + '共匪', + '仇共', + '政府', + '症腐', + '政腐', + '政付', + '正府', + '政俯', + '政f', + 'zhengfu', + '政zhi', + '挡中央', + '档中央', + '中央领导', + '中国zf', + '中央zf', + '国wu院', + '中华帝国', + 'gong和', + '大陆官方', + '北京政权', + '贺子珍', + '李德生', + '姚依林', + '李干成', + '黄镇', + '刘瑞龙', + '黄敬', + '薄熙', + '周小川', + '周建南', + '徐明', + '李洪志', + '新疆骚乱', + '打倒共产党', + '打倒中共', + '抵制共产党', + '抵制中共', + '请愿', + '上访', + '集会', + '游行', + '罢工', + '八九年', + '民主女神', + '藏独立', + '天灭中共', + '中共帮凶', + '中共保命', + '中共裁', + '中共党文化', + '中共腐败', + '中共的血旗', + '中共的罪恶', + '中共帝国', + '中共独裁', + '中共封锁', + '中共封网', + '中共黑', + '中共黑帮', + '中共解体', + '中共近期权力斗争', + '中共恐惧', + '中共权力斗争', + '中共任用', + '中共退党', + '中共洗脑', + '中共邪教', + '中共政治游戏', + '共产党腐败', + '共产党专制', + '共产党的报应', + '共产党的末日', + '八九民', + '八九学', + '八九政治', + '罢工门', + '六四事', + '请集会', + '人游行', + '田罢工', +] + +// 反动相关 +const REACTIONARY_WORDS = [ + '腐败中国', + '三个呆婊', + '你办事我放心', + '社会主义灭亡', + '打倒中国', + '打倒共产主义', + '打倒胡锦涛', + '打倒江泽民', + '打倒江主席', + '打倒李鹏', + '打倒罗干', + '打倒温家宝', + '打倒朱镕', + '抵制共产主义', + '抵制胡锦涛', + '抵制江泽民', + '抵制江主席', + '抵制李鹏', + '抵制罗干', + '抵制温家宝', + '抵制朱镕基', + '灭亡中国', + '亡党亡国', + '粉碎四人帮', + '激流中国', + '特供', + '特贡', + '特共', + 'zf大楼', + '殃视', + '贪污腐败', + '强制拆除', + '形式主义', + '政治风波', + '太子党', + '上海帮', + '北京帮', + '清华帮', + '红色贵族', + '权贵集团', + '河蟹社会', + '喝血社会', + '九风', + '9风', + '十七大', + '十7大', + '17da', + '九学', + '9学', + '四风', + '4风', + '双规', + '南街村', + '最淫官员', + '警匪', + '官匪', + '独夫民贼', + '官商勾结', + '强制捐款', + '一党执政', + '一党专制', + '专制政权', + '宪法法院', + '胡平', + '苏晓康', + '贺卫方', + '谭作人', + '焦国标', + '万润南', + '张志新', + '辛灝年', + '高勤荣', + '王炳章', + '高智晟', + '司马璐', + '刘晓竹', + '刘宾雁', + '魏京生', + '寻找林昭的灵魂', + '别梦成灰', + '谁是新中国', + '讨伐中宣部', + '异议人士', + '民运人士', + '启蒙派', + '选国家主席', + '民一主', + 'min主', + '民竹', + '民珠', + '民猪', + 'chinesedemocracy', + '大赦国际', + '国际特赦', + 'da选', + '投公', + '公头', + '宪政', + '平反', + '党章', + '维权', + '昝爱宗', + '宪章', + '08宪', + '08xz', + '抿主', + '敏主', + '人拳', + '人木又', + '人quan', + 'renquan', + '中国人权', + '中国新民党', + '群体事件', + '群体性事件', + '上中央', + '去中央', + '讨说法', + '请命', + '公开信', + '联名上书', + '万人大签名', + '万人骚动', + '截访', + 'shangfang', + '信访', + '访民', + '集合', + '组织集体', + '静坐', + '静zuo', + 'jing坐', + '示威', + '示wei', + 'you行', + '油行', + '游xing', + 'youxing', + '官逼民反', + '反party', + '反共', + '抗议', + '亢议', + '抵制', + '低制', + '底制', + 'di制', + '抵zhi', + 'dizhi', + 'boycott', + '血书', + '焚烧中国国旗', + 'baoluan', + '流血冲突', + '出现暴动', + '发生暴动', + '引起暴动', + 'baodong', + '灭共', + '霸工', + '罢考', + '罢餐', + '霸餐', + '罢参', + '罢饭', + '罢吃', + '罢食', + '罢课', + '罢ke', + '霸课', + 'ba课', + '罢教', + '罢学', + '罢运', + '网特', + '网评员', + '网络评论员', + '五毛党', + '五毛们', + '5毛党', + '戒严', + 'jieyan', + 'jie严', + '戒yan', + '8的平方事件', + '知道64', + '贰拾年', + '2o年', + '20和谐年', + '贰拾周年', + '六河蟹四', + '六百度四', + '六和谐四', + '陆四', + '陆肆', + '198964', + '5月35', + '89年春夏之交', + '64惨案', + '64时期', + '64运动', + '4事件', + '四事件', + '北京风波', + '学潮', + '学chao', + 'xuechao', + '学百度潮', + '门安天', + '天按门', + '坦克压大学生', + '历史的伤口', + '高自联', + '北高联', + '血洗京城', + '四二六社论', + '王丹', + '柴玲', + '沈彤', + '封从德', + '王超华', + '王维林', + '吾尔开希', + '吾尔开西', + '侯德健', + '阎明复', + '方励之', + '蒋捷连', + '丁子霖', + '辛灏年', + '蒋彦永', + '严家其', + '陈一咨', + '中华局域网', + '党的喉舌', + '互联网审查', + '当局严密封锁', + '新闻封锁', + '封锁消息', + '爱国者同盟', + '关闭所有论坛', + '网络封锁', + '金盾工程', + 'gfw', + '无界浏览', + '无界网络', + '自由门', + '何清涟', + '中国的陷阱', + '汪兆钧', + '记者无疆界', + '境外媒体', + '维基百科', + '纽约时报', + 'bbc中文网', + '华盛顿邮报', + '世界日报', + '东森新闻网', + '东森电视', + '星岛日报', + 'wikipedia', + 'youtube', + 'googleblogger', + '美国广播公司', + '英国金融时报', + '自由亚洲', + '自由时报', + '中国时报', + '反分裂', + '威胁论', + '左翼联盟', + '钓鱼岛', + '保钓组织', + '主权', + '弓单', + '火乍', + '木仓', + '石肖', + '核蛋', + '步qiang', + 'bao炸', + '爆zha', + 'baozha', + 'zha药', + 'zha弹', + '炸dan', + '炸yao', + 'zhadan', + 'zhayao', + 'hmtd', + '三硝基甲苯', + '六氟化铀', + '炸药配方', + '弹药配方', + '炸弹配方', + '皮箱炸弹', + '火药配方', + '人体炸弹', + '人肉炸弹', + '解放军', + '兵力部署', + '军转', + '军事社', + '8341部队', + '第21集团军', + '七大军区', + '7大军区', + '北京军区', + '沈阳军区', + '济南军区', + '成都军区', + '广州军区', + '南京军区', + '兰州军区', + '颜色革命', + '规模冲突', + '塔利班', + '基地组织', + '三股势力', + '印尼屠华', + '印尼事件', + '蒋公纪念歌', + '马英九', + 'mayingjiu', + '李天羽', + '苏贞昌', + '林文漪', + '陈水扁', + '陈s扁', + '陈随便', + '阿扁', + 'a扁', + '告全国同胞书', + '台百度湾', + '台完', + '台wan', + 'taiwan', + '台弯', + '湾台', + '台湾国', + '台湾共和国', + '台军', + '台du', + 'taidu', + 'twdl', + '一中一台', + '打台湾', + '两岸战争', + '攻占台湾', + '支持台湾', + '进攻台湾', + '占领台湾', + '统一台湾', + '收复台湾', + '登陆台湾', + '解放台湾', + '解放tw', + '解决台湾', + '光复民国', + '台湾问题', + '台海问题', + '台海危机', + '台海统一', + '台海大战', + '台海战争', + '台海局势', + '入联', + '入耳关', + '中华联邦', + 'x民党', + '青天白日', + '闹独立', + 'duli', + 'fenlie', + '日本万岁', + '小泽一郎', + '劣等民族', + '汉人', + '汉维', + '维汉', + '维吾', + '吾尔', + '伊力哈木', + '东突厥斯坦解放组织', + '东突解放组织', + '蒙古分裂分子', + '列确', + '阿旺晋美', + '藏人', + '臧人', + 'zang人', + '藏民', + '藏m', + '赖达', + 'dalai', + '哒赖', + 'dl喇嘛', + '丹增嘉措', + '打砸抢', + '西独', + '葬独', + '臧独', + '藏du', + 'zangdu', + '支持zd', + '藏暴乱', + '藏青会', + '雪山狮子旗', + '拉萨', + '啦萨', + '啦沙', + '啦撒', + '拉sa', + 'lasa', + 'la萨', + '西藏', + '藏西', + '藏春阁', + '藏獨', + '藏妇会', + '藏字石', + 'xizang', + 'xi藏', + 'x藏', + '西z', + 'tibet', + '希葬', + '希藏', + '硒藏', + '稀藏', + '西脏', + '西奘', + '西葬', + '西臧', + '援藏', + 'bjork', + '王千源', + '安拉', + '回教', + '回族', + '回回', + '回民', + '穆斯林', + '穆罕穆德', + '穆罕默德', + '默罕默德', + '伊斯兰', + '圣战组织', + '清真', + '清zhen', + 'qingzhen', + '真主', + '阿拉伯', + '高丽棒子', + '韩国狗', + '满洲第三帝国', + '满狗', + '鞑子', + '江丑闻', + '江嫡系', + '江独裁', + '江核心', + '江黑心', + '江胡内斗', + '江祸心', + '江家帮', + '江派和胡派', + '江派人马', + '江泉集团', + '江人马', + '江三条腿', + '江氏家族', + '江氏政治局', + '江氏政治委员', + '江梳头', + '江太上', + '江系人', + '江系人马', + '江宰民', + '江贼民', + '麻果丸', + '麻将透', + '麻醉弹', + '麻醉狗', + '麻醉枪', + '麻醉槍', + '麻醉药', + '麻醉藥', + '台湾版假币', + '台湾应该独立', + '台湾有权独立', +] + +// 邪教与暴恐 +const CULT_AND_TERROR_WORDS = [ + '福音会', + '中国教徒', + '统一教', + '观音法门', + '清海无上师', + '盘古', + '志洪李', + '李宏志', + '轮功', + '法轮', + '轮法功', + '三去车仑', + '氵去车仑', + '发论工', + '法x功', + '法o功', + '法0功', + '法一轮一功', + '轮子功', + '车仑工力', + '法lun', + 'fa轮', + '法lg', + 'flg', + 'fl功', + 'falungong', + '大法弟子', + '大纪元', + 'dajiyuan', + '明慧网', + '明慧周报', + '正见网', + '新唐人', + '伪火', + '退党', + 'tuidang', + '退dang', + '超越红墙', + '自fen', + '真善忍', + '九评', + '9评', + '9ping', + '九ping', + 'jiuping', + '自sha', + 'suicide', + 'titor', + '逢8必灾', + '逢八必灾', + '逢9必乱', + '逢九必乱', + '朱瑟里诺', + '根达亚文明', + '诺查丹玛斯', + '人类灭亡进程表', + '按照马雅历法', + '推背图', + '推bei图', + '济世灵文', + '诸世纪', + '电狗', + '电话定位器', + '电话拦截器', + '电话窃听', + '电话监', + '电话交友', + '电击枪', + '电鸡', + '电警棒', + '枪出售', + '枪的制', + '枪货到', + '枪决女犯', + '枪模', + '枪手', + '枪销售', + '枪械制', + '枪子弹', + '售步枪', + '售纯度', + '售单管', + '售弹簧刀', + '售防身', + '售狗子', + '售虎头', + '售火药', + '售假币', + '售健卫', + '售军用', + '售猎枪', + '售氯胺', + '售麻醉', + '售枪支', + '售热武', + '售三棱', + '售手枪', + '售五四', + '售一元硬', + '售子弹', + '售左轮', + '亚砷(酸)酐', + '亚砷酸钾', + '亚砷酸钠', + '亚硒酸', + '亚硒酸二钠', + '亚硒酸镁', + '亚硒酸钠', + '亚硒酸氢钠', + '亚硝酸乙酯', + '亚硝酰乙氧', + '氧化二丁基锡', + '氧化汞', + '氧化铊', + '氧化亚铊', + '氧氯化磷', + '原装弹', + '原子弹方法', + '原子弹清单', + '安眠酮', + '代血浆', + '普萘洛尔', + '呋塞米', + '西布曲明', + 'testosterone', + '胰岛素样生长因子', + '促红细胞生成素', + '地西泮', + '尼可刹米', + '甲睾酮', + 'adrenaline', + 'erythropoietin', + '地奈德', + '莫达非尼', + '氯噻嗪', + '苯巴比妥', + '促性腺激素', + '泼尼松', + '麻黄草', + '雄烯二醇', + '地塞米松', + 'tamoxifen', + 'strychnine', + 'androst', + '杜冷丁', + '兴奋剂', + 'mdma', + '海luo因', + 'heroin', + 'diamorphine', + 'diacetylmorphine', + '鸦片', + '阿芙蓉', + '咖啡因', + 'cocain', + '三唑仑', + '美沙酮', + '凯他敏', + 'ketamine', + '苯丙胺', + 'cannabis', + '爱他死', + '氯胺酮', + 'benzodiazepines', + '甲基安非他明', + '吗啡', + '安非他命', +] + +// 涉枪涉爆 +const GUN_EXPLOSIVE_WORDS = [ + '出售雷管', + '炸药', + '出售炸药', + '出售炸药 电话', + '制作火药配方', + '炸药出售', + '出售雷管炸药', + '在家里做原子弹', + '硝酸甘油炸弹制作', + 'TNT 炸弹的制作', + '硝铵炸药配方', + '硝酸甘油制作', + '简易炸药', + '火药制作简易炸弹', + '炸弹制作', + '硝酸甘油', + '硝酸甘油炸弹制作方法', + '手把手教你做炸弹', + '氢弹手工制作方法', + '起爆器', + '自制燃烧弹', + '燃烧弹', + '弹药制造方法', + '液体炸药配方', + '炸药配方与制作', + '自制炸药配方', + 'tnt炸药成分', + '炸药的制作', + 'tnt炸药配方', + '各种炸药配方大全', + '炸药的成分', + '燃烧弹的制作方', + '黑火药的配方', + '黑火药的制作', + '黑火药比例', + '燃烧弹 制作', + '黑火药配方比例', + '黑火药的成分', + '原子弹配方', + '原子弹DIY教程', + 'C4炸药', + '雷管出售', + '制作硝化甘油', + 'DIY核弹', + '核弹制作方法', + '原子弹制作手册', + '简易原子弹教程', + '黑火药配方', + '自制炸药方法', + '教你怎么用土办法做武器', + '莫洛托夫鸡尾酒的配方', + 'zz炸弹的制作方法', + 'C4炸药的成分', + '炸药制造生产工艺', + '原子弹制造完全手册', + '「红烧兔子」大餐', + '汽车炸弹制作', + '人体炸弹制作流程', + '炸弹制作从入门', + '鸡尾酒炸弹制作', + '集束炸弹制作', + '硝酸铵的简便制取方法', + '莫洛托夫燃烧弹', + '塑胶炸弹', + '用火药混合塑料', + '高性能爆炸物质混合', + '原子弹 制作方法', + '手教你制作原子弹', + '轻型巡航导弹的制作', + '黑索金的制造过程', + 'Zha药制造进程', + '购买自爆材料', + '可塑炸弹制作方法', + 'C4塑胶炸药', + '水浴法制TNT', + '简易核弹头的制造', + '高爆炸药的基本配方', + '如何制硝化甘油', + '燃烧剂的简易制备', + '简易大威力炸弹', + '炸弹制作入门', + '硝酸甘油炸药制作', + '基本配方是硝酸甘', + '燃烧剂的简易制作', + '制作性能优良的燃烧弹', + '酸甘油炸药', + '简易导弹制作', + '核弹头的制造', + '可塑炸弹制作', + '炸药硝酸甘油制法', + '原子弹制作方法', + '火药的配方', + '手教你做原子弹', + '怎么用土办法做武器', + '手把手教你制作原子弹', + '燃烧弹的制作方法', + '怎么自制燃烧弹', + 'TNT炸弹制作', + '高爆炸药基本配方', + '炸弹制作从入门到精', + '写的炸弹制作教程', + '原子弹制作 方法', + '教你制作原子弹', + '下硝化甘油的制作方法', + 'DIY原子弹', + '原子弹制作', + '炸药及其制造方法', + 'ATOM弹制造', + '三分钟教你原子弹', + '火药原理', + '简易炸弹', + '简易燃烧弹', + '土炸药成份', + '你怎么用土办法做武器', + '燃烧弹制作', + '燃烧瓶的制作方法', + '炸弹制作手册 硫酸', + '原子弹制作方法DIY', + '汽油三分之一白糖混合', + '炸药的制作配方', + '炸药的配方', + '如何自制燃烧弹', + '标准炸弹教程', + '黑火药和硝酸钾农药混合', + '手柄制作炸弹引爆器', + '制作炸弹首先要', + '燃烧瓶制作', + '出售炸药QQ', + '出售气枪', + '出售气枪 QQ', + '出售气枪 电话', + '出售手枪', + '出售手枪 QQ', + '出售手枪 电话', + '出售猎枪', + '出售猎枪 QQ', + '出售猎枪 电话', + '出售汽枪', + '出售步枪', + '出售狙击枪', + '出售手狗', + '出售气狗', + '出售猎狗', + '【手枪出售】联系电话', + '64式手枪出售', + '92式手枪出售', + '出售:高仿真枪', + '出售54式手枪', + '出售M1911', + '出售各类猎狗', + '出售国产军用54手枪', + '出售火枪 –游戏', + '出售进口金属枪模', + '出售仿真枪', + '出售麻醉枪免收任何定金', + '出售手枪=货到付款', + '出售外国枪模', + '出售袖珍手枪', + '娥眉气枪出售', + '防身手枪QQ', + '防身武器手枪', + '仿真枪QQ', + '钢珠枪小口径步枪', + '高压狗QQ', + '高压气枪,气枪子弹', + '高压气枪|电狗专卖', + '高压气枪|气枪货到付款', + '高压气枪专卖店', + '各类军用枪', + '各类军用枪支', + '各式气枪出售', + '工字牌气枪出售专卖', + '气枪', + '工字汽狗麻醉枪', + '供应军用手枪', + '供应汽枪', + '购买枪支QQ', + '国产汽狗专卖|气枪货', + '集团出售手枪', + '集团出售手枪气枪', + '进口气枪,气枪子弹', + '军用54出售', + '军用77出售', + '军用手枪钢珠枪', + '里有汽枪出售', + '猎枪出售/枪', + '猎枪仿真枪QQ', + '猎枪麻醉枪钢珠枪', + '猎枪哪里有卖', + '猎枪手枪买卖', + '猎枪销售手枪销售气枪', + '猎枪专卖店', + '猎枪子弹底火专卖', + '麻醉枪出售可验货', + '麻醉枪猎枪军用', + '买卖54狗', + '买卖64狗', + '买卖警察枪支', + '买卖军狗', + '买卖军用枪支', + '卖64手枪', + '卖92手枪', + '卖钢珠枪QQ', + '哪里能买到自制手枪', + '哪里有钢珠枪', + '哪里有连发猎枪买卖', + '哪里有卖双管猎', + '哪里有气枪卖', + '哪里有枪出售', + '哪里有真枪出售', + '哪里有真枪卖买', + '哪能买到真枪', + '期出售手枪气枪猎', + '气狗 高压气枪', + '气狗出售 进口气狗', + '气枪专卖网', + '气枪/出售/专卖网', + '气枪/猎枪/钢珠枪', + '气枪仿真枪出售', + '气枪购买|气枪组装', + '气枪货到付款,汽狗出售', + '气枪价格 仿真气枪', + '气枪价格,气枪专卖店', + '气枪看货加QQ', + '气枪联系QQ', + '气枪猎枪麻醉枪', + '气枪迷魂药', + '汽狗高压汽枪', + '铅弹 上海工字气枪', + '全金属仿真枪专卖', + '热武器QQ', + '三箭气枪出售', + '手枪气* 枪猎', + '手槍买卖自制手枪', + '售枪购买手枪', + '售手枪QQ', + '司马仿真枪模型QQ', + '淘宝气枪货到付款', + '提供气狗专卖店', + '提供专业及时的气狗', + '秃鹰气枪货到付款', + '锡峰气枪出售', + '仙鹤气枪出售', + '销售/专卖/买卖77式手枪', + '销售电手狗', + '销售气手狗', + '销售小口径步枪', + '销售运动步枪', + '小口径秃鹰沙漠', + '有军用枪支出售', + '长期出 售手枪', + '折叠狗QQ', + '转让 猎枪', + '子弹货到付款', + '自制手枪哪里买', + '自制手枪哪里有卖', + '阻击枪/汽枪/高压气枪', + '组装手枪', + '最新销售汽狗', + '出售警用手铐|仿真枪', + '猎枪|真枪', + '手枪|阻击枪', + '弓弩专营', + '弓弩专卖网货到付款', + '弓弩专卖网', + '弓弩专卖货到付款', + '弓弩专卖店网', + '弓弩专卖店', + '弓弩专卖', + '弓弩制作', + '弓弩直销', + '弓弩有限公司', + '弓弩营销中心', + '弓弩一百专卖店', + '弓弩销售', + '弓弩网', + '弓弩图纸', + '弓弩特许经销', + '弓弩狩猎网', + '自制手弩', + '追风弓弩麻醉箭专卖', + '专业弓弩网', + '中国战神军用弓弩', + '中国弩弓专卖', + '中国弓弩专卖网', + '中国弓弩直销', + '中国弓弩网', + '中国弓弩狩猎网', + '中国弓驽网', + '制作简易弓弩', + '郑州弓弩专卖', + '赵氏弓弩专卖网', + '赵氏弓弩专卖店', + '赵氏弓弩专卖', + '赵氏弓弩销售', + '小型弓弩专卖店', + '小猎人弓弩网', + '狩猎器材弓弩专卖', + '狩猎器材弓弩', + '狩猎弓弩专卖网', + '狩猎弓弩专卖', + '狩猎弓弩麻醉箭', + '手枪式折叠三用弩', + '三利达弓弩专卖网', + '三利达弓弩直营', + '三利达弓弩配件', + '三步倒药箭批发', + '三步倒弩箭专卖', + '三步倒麻醉弩箭销售', + '三步倒麻醉箭专卖', + '三步倒麻醉箭', + '三步倒捕狗药', + '军用弓弩专卖网', + '军用弓弩专卖店', + '军用弓弩批发', + '军用弓弩公司', + '供应三利达弓弩麻醉箭', + '供应三步倒麻醉箭', + '供应秦氏弓弩', + '供应弩用麻醉箭', + '供应弩捕狗箭', + '供应麻醉箭三步倒', + '供应麻醉箭批发', + '供应麻醉箭', + '供应军用弩折叠弩', + '供应军用弓弩专卖', + '供应精品弓弩', + '供应弓弩麻醉箭', + '供应弓弩', + '供应钢珠弓弩', + '弓弩商城专卖', + '弓弩商城', + '弓弩亲兄弟货到付款', + '弓弩批发', + '弓弩免定金货到付款', + '弓弩麻醉箭', + '弓弩麻醉镖', + '弓弩论坛', + '钢珠弓弩专卖网', + '钢珠弓弩专卖店', + '打狗弓弩三步倒', + '麻醉弓弩专卖店', + '出售军刀', + '出售军刺', + '出售弹簧刀', + '出售三棱刀', + '出售跳刀', + '军刀网', + '南方军刀网', + '户外军刀网', + '三棱军刺专卖', + '出售开山刀军刺', + '西点军刀网', + '军刀专 卖', + '阿兰德龙户外', + '出售军品军刀', + '勃朗宁军刀', + '军刀军品网', + '阿兰得龙野营刀具网', + '出售军刺军刀', + '警用刀具出售', + '折刀专卖网', + '阳江军品军刀网', + '野营刀专卖', + '砍刀精品折刀专卖', + '匕首蝴蝶甩刀专卖', + '军刀专卖军刺', + '军刀专卖刀具批发', + '军刀图片砍刀', + '军刀网军刀专卖', + '军刀价格军用刀具', + '军品军刺网', + '军刀军刺甩棍', + '阳江刀具批发网', + '北方先锋军刀', + '正品军刺出售', + '野营军刀出售', + '开山刀砍刀出售', + '仿品军刺出售', + '军刀直刀专卖', + '手工猎刀专卖', + '自动跳刀专卖', + '军刀电棍销售', + '军刀甩棍销售', + '美国军刀出售', + '极端武力折刀', + '防卫棍刀户外刀具', + '阿兰德龙野营刀', + '仿品军刺网', + '野营砍刀户外军刀', + '手工猎刀户外刀具', + '中国户外刀具网', + '西点军品军刀网', + '野营开山刀军刺', + '三利达弓弩军刀', + '尼泊尔军刀出售', + '防卫野营砍刀出售', + '防卫著名军刀出售', + '防卫棍刀出售', + '防卫甩棍出售', + '防卫电棍出售', + '军刺野营砍刀出售', + '著名精品折刀出售', + '战术军刀出售', + '刺刀专卖网', + '户外军刀出售', + '阳江刀具直销网', + '冷钢刀具直销网', + '防卫刀具直销网', + '极端武力直销网', + '刀具直销网', + '军刀直销网', + '直刀匕首直销网', + '军刀匕首直销网', + '折刀砍刀军品网', + '野营刀具军品网', + '阳江刀具军品网', + '冷钢刀具军品网', + '防卫刀具军品网', + '极端武力军品网', + '军用刀具军品网', + '军刀直刀军品网', + '折刀砍刀专卖', + '野营刀具专卖', + '阳江刀具专卖', + '冷钢刀具专卖', + '防卫刀具专卖', + '出售美军现役军刀', +] + +// 贪腐相关 +const CORRUPTION_WORDS = [ + '腐败', + '贪污', + 'gcd', + 'gongchandang', + '共一产一党', + 'communistparty', + '张曙', + '买别墅', + '玩女人', + '贪20亿', + '许宗衡', + '贪财物', + '李启红', + '贪腐财富', + '落马', + '高官名单', + '陈希同', + '玩忽职守', + '有期徒刑', + '受贿罪', + '滥用职权', + '没收个人财产', + '成克杰', + '死刑', + '程维高', + '严重违纪', + '开除党籍', + '撤销职务', + '刘方仁', + '无期徒刑', + '倪献策', + '徇私舞弊', + '梁湘', + '以权谋私', + '撤职。', + '李嘉廷', + '死刑缓期', + '张国光', + '韩桂芝', + '黄瑶', + '判处死刑', + '剥夺政治权利终身', + '没收个人全部财产', + '石兆彬', + '侯伍杰', + '王昭耀', + '剥夺政治权利', + '沈图', + '叛逃美国', + '罗云光', + '起诉', + '张辛泰', + '李效时', + '边少斌', + '徐鹏航', + '违纪', + '收受股票', + '王乐毅', + '李纪周', + '郑光迪', + '田凤山。', + '邱晓华', + '郑筱萸', + '孙鹤龄', + '蓝田造假案', + '留党察看', + '何洪达', + '朱志刚', + '杨汇泉', + '官僚主义', + '徐炳松', + '托乎提沙比尔', + '王宝森', + '经济犯罪', + '陈水文', + '孟庆平', + '胡长清', + '朱川', + '许运鸿', + '丘广钟', + '刘知炳', + '丛福奎', + '王怀忠', + '巨额财产', + '来源不明罪', + '李达昌', + '刘长贵', + '王钟麓', + '阿曼哈吉', + '付晓光', + '自动辞', + '刘克田', + '吕德彬', + '刘维明', + '双开', + '刘志华', + '孙瑜', + '李堂堂', + '韩福才 青海', + '欧阳德 广东', + '韦泽芳 海南', + '铁英 北京', + '辛业江 海南', + '于飞 广东', + '姜殿武 河北', + '秦昌典 重庆', + '范广举 黑龙江', + '张凯广东', + '王厚宏海南', + '陈维席安徽', + '王有杰河南', + '王武龙江苏', + '米凤君吉林', + '宋勇辽宁', + '张家盟浙江', + '马烈孙宁夏', + '黄纪诚北京', + '常征贵州', + '王式惠重庆', + '周文吉', + '王庆录广西', + '潘广田山东', + '朱作勇甘肃', + '孙善武河南', + '宋晨光江西', + '梁春禄广西政协', + '鲁家善 中国交通', + '金德琴 中信', + '李大强 神华', + '吴文英 纺织', + '查克明 华能', + '朱小华光大', + '高严 国家电力', + '王雪冰', + '林孔兴', + '刘金宝', + '张恩照', + '康日新', + '王益', + '张春江', + '洪清源', + '平义杰', + '李恩潮', + '孙小虹', + '陈忠', + '慕绥新', + '田凤岐', + '麦崇楷', + '柴王群', + '吴振汉', + '张秋阳', + '徐衍东', + '徐发 黑龙江', + '张宗海', + '丁鑫发', + '徐国健', + '李宝金', + '单平', + '段义和', + '荆福生', + '陈少勇', + '黄松有', + '皮黔生', + '王华元', + '王守业', + '刘连昆', + '孙晋美', + '邵松高', + '肖怀枢', + '刘广智 空军', + '姬胜德 总参', + '廖伯年 北京', +] + +// 歧视性用语 +const DISCRIMINATION_WORDS = ['歧视', '种族', '民族歧视'] + +// 其他高风险词 +const OTHER_HIGH_RISK_WORDS = [ + '技师', + 'BT', + '口暴', + '全套', + '婊子', + '调查婚外情', + '信用卡提现', + '借腹生子', + '代生孩子', + '低价出售', + '款到发货', + '刻章办', + '套牌车', + '改卷内幕', + '隐形耳机', + '答an', + 'da案', +] + +export const SENSITIVE_WORDS = Array.from( + new Set([ + ...BASIC_WORDS, + ...VIOLENCE_WORDS, + ...PORNOGRAPHY_WORDS, + ...GAMBLING_WORDS, + ...DRUG_WORDS, + ...ILLEGAL_ADS_AND_SCAMS_WORDS, + ...POLITICAL_WORDS, + ...REACTIONARY_WORDS, + ...CULT_AND_TERROR_WORDS, + ...GUN_EXPLOSIVE_WORDS, + ...CORRUPTION_WORDS, + ...DISCRIMINATION_WORDS, + ...OTHER_HIGH_RISK_WORDS, + ]) +) diff --git a/src/lib/utils/sensitiveWords.ts b/src/lib/utils/sensitiveWords.ts index e04983f..a5ce29a 100644 --- a/src/lib/utils/sensitiveWords.ts +++ b/src/lib/utils/sensitiveWords.ts @@ -3,60 +3,7 @@ * 用于过滤用户输入中的敏感词汇,将其替换为 * */ -// 敏感词库(可根据实际需求扩展) -const SENSITIVE_WORDS = [ - // 基础不文明用语 - '垃圾', - '脏话', - '骂人', - '辱骂', - '仇恨', - '傻逼', - '煞笔', - '傻b', - '傻比', - 'sb', - 'SB', - '草泥马', - '操', - '妈的', - '他妈', - '你妈', - '去死', - '找死', - '该死', - '白痴', - '智障', - '弱智', - '脑残', - '废物', - '贱', - '滚', - // 暴力相关 - '暴力', - '恐怖', - '杀人', - '杀害', - '血腥', - '虐待', - // 违法违规 - '色情', - '赌博', - '诈骗', - '毒品', - '邪教', - '反动', - '法轮功', - '传销', - '洗钱', - '贩毒', - '走私', - // 歧视性用语 - '歧视', - '种族', - '民族歧视', -] - +import { SENSITIVE_WORDS } from './sensitiveWordList' type FileSaveHandle = { createWritable: () => Promise<{ write: (data: Blob) => Promise @@ -132,8 +79,8 @@ export function containsNumber(text: string): boolean { * 检查文本中是否包含特殊符号或标点符号 */ export function containsSpecialChars(text: string): boolean { - // 检测所有标点符号和特殊符号(包括中文标点) - return /[\p{P}\p{S}]/u.test(text) + // 检测除中文逗号、顿号、句号之外的标点符号和特殊符号 + return /[\p{P}\p{S}]/u.test(text.replace(/[,、。]/g, '')) } /** @@ -183,10 +130,10 @@ export function filterInput(input: string): string { // 过滤数字 filtered = filtered.replace(/\d/g, '') - // 过滤所有标点符号和特殊符号 - // 保留:中文字符、空格 - // 过滤:所有标点符号(包括中文标点和英文标点)、特殊符号 - filtered = filtered.replace(/[\p{P}\p{S}]/gu, '') + // 过滤其他标点符号和特殊符号 + // 保留:中文字符、空格、中文逗号、顿号、句号 + // 过滤:除 ,、。 之外的标点符号和特殊符号 + filtered = filtered.replace(/(?![,、。])[\p{P}\p{S}]/gu, '') // 进行敏感词过滤(替换为星号,不直接删除) filtered = filterSensitiveWords(filtered) diff --git a/src/pages/chenyun/internalization.tsx b/src/pages/chenyun/internalization.tsx index d1a20ac..949be46 100644 --- a/src/pages/chenyun/internalization.tsx +++ b/src/pages/chenyun/internalization.tsx @@ -45,7 +45,7 @@ const MULTIPLE_DEFAULT_ERROR = '你的选择还不够全面哦~请你再想想 const QUIZ_ANCHORS: QuizAnchor[] = [ { id: 'quiz-1', - time: 60 * 5 + 53, //5.52分钟 + time: 60 * 5 + 53.1, //5.52分钟 title: '题目1', prompt: '陈云经济思想的核心内容包括:', type: 'multiple', @@ -84,7 +84,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ }, { id: 'quiz-3', - time: 15 * 60 + 56, //15:56 + time: 15 * 60 + 56.2, //15:56 title: '题目3', prompt: '新时代的青年可以从陈云身上学习到', type: 'drag', @@ -102,7 +102,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ }, { id: 'quiz-4', - time: 60 * 5 + 21, // 5:21 + time: 60 * 5 + 21.5, // 5:21 title: '题目4', prompt: '陈云讲:“经济建设最终目的,是为了改善____的生活水平。”', type: 'single', @@ -175,15 +175,13 @@ const ChenyunInternalization = () => { once: true, afterResume: 'remove', render: ({ resume }) => { - // 拖拽题:处理选项拖拽与校验 const handleDragChange = (val: DragFillResult[]) => { setDragQuizValues((prev) => ({ ...prev, [item.id]: val })) setDragQuizErrors((prev) => ({ ...prev, [item.id]: '' })) + } - if (val.length < correctAnswers.length) return - - // 检查是否包含了所有正确答案(不要求顺序) - const filledAnswers = val.map((v) => v.optionValue) + const handleDragSubmit = (val: DragFillResult[]) => { + const filledAnswers = val.map((result) => result.optionValue) const answerSet = new Set(correctAnswers) const filledSet = new Set(filledAnswers) const isCorrect = @@ -193,12 +191,13 @@ const ChenyunInternalization = () => { if (isCorrect) { setActiveSuccess(item.id) setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: '这不是陈云的工作作风。请你再想想吧!', - })) + return } + setDragQuizErrors((prev) => ({ + ...prev, + [item.id]: '这不是陈云的工作作风。请你再想想吧!', + })) + setDragQuizValues((prev) => ({ ...prev, [item.id]: [] })) } return ( @@ -216,6 +215,7 @@ const ChenyunInternalization = () => { value={dragQuizValues[item.id] || []} error={dragQuizErrors[item.id]} onChange={handleDragChange} + onSubmit={handleDragSubmit} /> { }} audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_02.mp3')} ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} @@ -215,7 +216,7 @@ const ChenyunPlace1 = () => { speaker="guide" audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_04_2.mp3')} > - 实际上是陈云舅父母的家,也是少年陈云从4岁至14岁生活的地方。童年的陈云,家庭生活十分不幸。他两岁时父亲病故,四岁时,母亲也悲苦地死去。 + 实际上是陈云舅父母的家,也是陈云从4岁至14岁生活的地方。童年的陈云,家庭生活十分不幸。他两岁时父亲病故,四岁时,母亲也悲苦地死去。 { const handleQuiz2Change = (val: DragFillResult[]) => { setQuiz2Value(val) setQuiz2Error('') - if (val.length < QUIZ2_CORRECT_SET.size) return + } + const handleQuiz2Submit = (val: DragFillResult[]) => { if (!isQuiz2Correct(val)) { setQuiz2Error(QUIZ2_ERROR) + setQuiz2Value([]) return } - setQuiz2Visible(false) setVideo1Visible(false) setVideo2Visible(false) @@ -395,17 +396,13 @@ const ChenyunPlace7 = () => { value={quiz2Value} error={quiz2Error} onChange={handleQuiz2Change} + onSubmit={handleQuiz2Submit} /> {/* 成功提示(盖在视频/题目之上) */} {successVisible && (
- +
)}
diff --git a/src/pages/chenyun/place/place9.tsx b/src/pages/chenyun/place/place9.tsx index f5d0f3c..46534a8 100644 --- a/src/pages/chenyun/place/place9.tsx +++ b/src/pages/chenyun/place/place9.tsx @@ -1,5 +1,8 @@ import { useState, useRef } from 'react' import { Title, Image, Cursor, Video, GuideSteps, GuideStepsItem } from '@/components' +import { SingleChoiceQuizPanel } from '@/components/layout/quiz' +import SuccessAlert from '@/components/layout/alert/success-alert' +import ScaledPortal from '@/components/layout/ScaledPortal' import { getBackgroundImage, getStaticImage, getPublicPath } from '@/lib/utils' import { useNavigate, useParams } from 'react-router-dom' import type { VideoRef } from '@/components/video' @@ -36,6 +39,10 @@ const ChenyunPlace9 = () => { const [guideVisible, setGuideVisible] = useState(false) const [guideStep, setGuideStep] = useState(0) + // 视频后的单选题 + const [quizChenVisible, setQuizChenVisible] = useState(false) + const [successAlertChenVisible, setSuccessAlertChenVisible] = useState(false) + const handleOpenExhibit = (_index: 1) => { // 展品1:播放视频 setVideoVisible(true) @@ -128,15 +135,55 @@ const ChenyunPlace9 = () => { onEnded={() => { setVideoPlaying(false) markExhibitVisited(place, 1) - // 视频播放完成后,显示对话引导 - setGuideVisible(true) - setGuideStep(0) + // 视频播放完成后,先弹出题目 + setQuizChenVisible(true) triggerPuzzleAnimation(12) }} />
)} + {/* 视频后的单选题 */} + + <> + { + switch (value) { + case 'A': + return '这是技术层面的“好算盘”,但陈云的特别,在于他把算盘打进了老百姓的米袋子、布匹铺、工厂车间里——他算的是人,不是数;是实,不是虚。请你再想想吧!' + default: + return '请你再想想吧!' + } + }} + onSubmit={(value: string, isCorrect: boolean) => { + if (isCorrect) { + setQuizChenVisible(false) + setSuccessAlertChenVisible(true) + } + }} + /> + + { + setSuccessAlertChenVisible(false) + // 答对后显示对话引导 + setGuideVisible(true) + setGuideStep(0) + }} + /> + + + {/* 视频播放完成后的对话引导 */} { setShowPreYinyuanAlert(true) return } + if (fileType === 'mimian') { + setExpandedFile('mimian') + } setPlayingVideo(fileType) }} /> diff --git a/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx b/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx index 5731777..5d5109b 100644 --- a/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx +++ b/src/pages/chenyun/reflection/yinyuan/YinyuanBattle.tsx @@ -2,7 +2,7 @@ * 银元之战主组件 * 整合银元之战的所有子组件:文件、线索、题目、引导提示 */ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { YinyuanFile } from './YinyuanFile' import { YinyuanClues } from './YinyuanClues' import { YinyuanQuizzes } from './YinyuanQuizzes' @@ -47,15 +47,12 @@ export const YinyuanBattle = ({ const state = useYinyuanState() const { triggerPuzzleAnimation } = usePuzzleAnimation() const { isPuzzleUnlocked } = usePuzzleStore() + const mimianStartedRef = useRef(false) // 当米棉之战解决后,隐藏银元之战的走势图 useEffect(() => { if (isMimianSolved) { state.setShowTrendChartIcon(false) - // 如果 guide2 正在显示,也关闭它 - if (state.showGuide2) { - state.setShowGuide2(false) - } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMimianSolved]) @@ -91,48 +88,6 @@ export const YinyuanBattle = ({ state.setHasShownGuide1, ]) - // 监听走势图图标出现的条件,显示第二个提示 - // 注意:如果米棉之战已解决,则不显示走势图提示 - useEffect(() => { - const shouldShowGuide2 = - state.isYinyuanSolved && state.showTrendChartIcon && !state.hasShownGuide2 && !isMimianSolved // 米棉之战解决后不再显示提示 - - if (shouldShowGuide2) { - // 延迟一下,确保走势图已经渲染 - const timer = setTimeout(() => { - state.setShowGuide2(true) - state.setHasShownGuide2(true) - }, 300) - return () => clearTimeout(timer) - } - }, [ - state.isYinyuanSolved, - state.showTrendChartIcon, - state.hasShownGuide2, - state.setShowGuide2, - state.setHasShownGuide2, - isMimianSolved, - ]) - - // 监听走势图大图打开的条件,显示第三个提示 - useEffect(() => { - const shouldShowGuide3 = state.showTrendChartDetail && !state.hasShownGuide3 - - if (shouldShowGuide3) { - // 延迟一下,确保大图已经打开 - const timer = setTimeout(() => { - state.setShowGuide3(true) - state.setHasShownGuide3(true) - }, 300) - return () => clearTimeout(timer) - } - }, [ - state.showTrendChartDetail, - state.hasShownGuide3, - state.setShowGuide3, - state.setHasShownGuide3, - ]) - // 处理多选题1提交 const handleQuiz1Submit = (values: string[], isCorrect: boolean) => { if (isCorrect) { @@ -163,9 +118,10 @@ export const YinyuanBattle = ({ // 处理成功提示框继续 const handleSuccessAlertContinue = () => { - // 关闭聚光灯、标记银元之战为已解决、显示走势图 + // 关闭聚光灯、标记银元之战为已解决、自动展示走势图 state.setIsYinyuanSolved(true) state.setShowSuccessAlert(false) + state.setShowTrendChartDetail(true) // 检查碎片13是否已解锁,如果未解锁则触发动画 const pieceId = 13 @@ -174,22 +130,15 @@ export const YinyuanBattle = ({ } } - // 处理走势图点击 - const handleTrendChartClick = () => { - // 关闭第三个提示框 - state.setShowGuide3(false) - // 点击走势图:隐藏文件上的走势图图标,播放米棉之战视频 - state.setShowTrendChartIcon(false) - state.setShowTrendChartDetail(false) - // 播放米棉之战视频 - onPlayVideo('mimian') - } - - // 处理走势图大图关闭 + // 处理走势图大图关闭,关闭后进入米棉之战 const handleTrendChartDetailClose = () => { - // 关闭第三个提示框 - state.setShowGuide3(false) state.setShowTrendChartDetail(false) + state.setShowTrendChartIcon(false) + + if (mimianStartedRef.current || isMimianSolved) return + + mimianStartedRef.current = true + onPlayVideo('mimian') } return ( @@ -206,7 +155,6 @@ export const YinyuanBattle = ({ showSuccessAlert={state.showSuccessAlert} onFileClick={onFileClick} onTrendChartClick={() => { - state.setShowGuide2(false) state.setShowTrendChartDetail(true) }} onSolveClick={() => { @@ -234,14 +182,9 @@ export const YinyuanBattle = ({ {/* 引导提示组件 */} state.setShowGuide1(false)} - onGuide2Close={() => state.setShowGuide2(false)} - onGuide3Close={() => state.setShowGuide3(false)} onTrendChartDetailClose={handleTrendChartDetailClose} - onTrendChartClick={handleTrendChartClick} /> ) diff --git a/src/pages/chenyun/reflection/yinyuan/YinyuanGuides.tsx b/src/pages/chenyun/reflection/yinyuan/YinyuanGuides.tsx index 76e0285..5f99178 100644 --- a/src/pages/chenyun/reflection/yinyuan/YinyuanGuides.tsx +++ b/src/pages/chenyun/reflection/yinyuan/YinyuanGuides.tsx @@ -1,6 +1,6 @@ /** * 银元之战引导提示组件 - * 包含三个引导提示:前往解决按钮、走势图、走势图大图 + * 包含前往解决按钮引导和走势图大图 */ import { StudyGuideModal } from '@/components' import { ExhibitOverlay } from '@/components' @@ -8,38 +8,23 @@ import { ExhibitOverlay } from '@/components' export interface YinyuanGuidesProps { /** 是否显示引导提示1(前往解决按钮) */ showGuide1: boolean - /** 是否显示引导提示2(走势图) */ - showGuide2: boolean - /** 是否显示引导提示3(走势图大图) */ - showGuide3: boolean /** 是否显示走势图大图 */ showTrendChartDetail: boolean /** 引导提示1关闭回调 */ onGuide1Close: () => void - /** 引导提示2关闭回调 */ - onGuide2Close: () => void - /** 引导提示3关闭回调 */ - onGuide3Close: () => void /** 走势图大图关闭回调 */ onTrendChartDetailClose: () => void - /** 走势图点击回调 */ - onTrendChartClick: () => void } /** * 银元之战引导提示组件 - * 显示三个引导提示和走势图大图 + * 显示前往解决按钮引导和走势图大图 */ export const YinyuanGuides = ({ showGuide1, - showGuide2, - showGuide3, showTrendChartDetail, onGuide1Close, - onGuide2Close, - onGuide3Close, onTrendChartDetailClose, - onTrendChartClick, }: YinyuanGuidesProps) => { return ( <> @@ -53,32 +38,11 @@ export const YinyuanGuides = ({ 请跟随指示点击前往解决按钮 - {/* 引导提示2:走势图出现后 */} - - 请点击走势图 - - - {/* 引导提示3:走势图大图打开后 */} - - 请点击银元价格走势图 - - {/* 走势图大图展示 - 使用 ExhibitOverlay */} diff --git a/src/pages/chenyun/reflection/yinyuan/useYinyuanState.ts b/src/pages/chenyun/reflection/yinyuan/useYinyuanState.ts index 9d89512..3283f56 100644 --- a/src/pages/chenyun/reflection/yinyuan/useYinyuanState.ts +++ b/src/pages/chenyun/reflection/yinyuan/useYinyuanState.ts @@ -38,17 +38,11 @@ export const useYinyuanState = () => { // 走势图大图显示状态 const [showTrendChartDetail, setShowTrendChartDetail] = useState(false) - // 三个引导提示框状态 + // 引导提示框状态 const [showGuide1, setShowGuide1] = useState(false) // 前往解决按钮出现后的提示 - const [showGuide2, setShowGuide2] = useState(false) // 走势图出现后的提示 - const [showGuide3, setShowGuide3] = useState(false) // 走势图大图打开后的提示 - // 记录是否已经显示过这些提示(避免重复显示) + // 记录是否已经显示过提示(避免重复显示) const [hasShownGuide1, setHasShownGuide1] = useState(false) - const [hasShownGuide2, setHasShownGuide2] = useState( - () => useChenyunStore.getState().isYinyuanSolved - ) - const [hasShownGuide3, setHasShownGuide3] = useState(false) return { // 题目相关状态 @@ -80,15 +74,7 @@ export const useYinyuanState = () => { // 引导提示状态 showGuide1, setShowGuide1, - showGuide2, - setShowGuide2, - showGuide3, - setShowGuide3, hasShownGuide1, setHasShownGuide1, - hasShownGuide2, - setHasShownGuide2, - hasShownGuide3, - setHasShownGuide3, } } diff --git a/src/pages/huangyanpei/internalization.tsx b/src/pages/huangyanpei/internalization.tsx index cf40acd..77de838 100644 --- a/src/pages/huangyanpei/internalization.tsx +++ b/src/pages/huangyanpei/internalization.tsx @@ -145,7 +145,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ isCorrect: true, }, ], - explanation: '黄炎培认为教育不能只谈书本、脱离实际', + explanation: '黄炎培认为教育不能只谈书本、脱离实际。', }, { id: 'quiz-3', @@ -219,54 +219,46 @@ const HuangyanpeiInternalization = () => { const handleDragChange = (val: DragFillResult[]) => { setDragQuizValues((prev) => ({ ...prev, [item.id]: val })) setDragQuizErrors((prev) => ({ ...prev, [item.id]: '' })) + } - // 第 2 题:必须按顺序填入“有业”“乐业” + const handleDragSubmit = (val: DragFillResult[]) => { if (item.id === 'quiz-2b') { const orderedAnswers = ['有业', '乐业'] - const isCorrect = - val.length === orderedAnswers.length && - orderedAnswers.every((answer, index) => val[index]?.optionValue === answer) - + const isCorrect = orderedAnswers.every((answer, index) => { + const blankId = item.blanks![index]?.id + return ( + blankId !== undefined && + val.find((result) => result.blankId === blankId)?.optionValue === answer + ) + }) if (isCorrect) { setActiveSuccess(item.id) setSuccessContent('') setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: item.explanation || '请你再想想吧!', - })) + return } - return - } - - // 特斯拉标签无法拖入答案框 - const hasInvalidOption = val.some((v) => v.optionValue === '特斯拉') - if (hasInvalidOption) { setDragQuizErrors((prev) => ({ ...prev, - [item.id]: '特斯拉为外资企业哦~', + [item.id]: item.explanation || '请你再想想吧!', })) + setDragQuizValues((prev) => ({ ...prev, [item.id]: [] })) return } - if (val.length < item.blanks!.length) return - - // 检查是否都是正确答案(排除特斯拉) - const validAnswers = ['比亚迪', '华为', '江南造船', '光明食品', 'C919飞机'] - const filledAnswers = val.map((v) => v.optionValue) - const isCorrect = filledAnswers.every((answer) => validAnswers.includes(answer)) - + const validAnswers = new Set(['比亚迪', '华为', '江南造船', '光明食品', 'C919飞机']) + const isCorrect = + val.length === 5 && val.every((result) => validAnswers.has(result.optionValue)) if (isCorrect) { setActiveSuccess(item.id) setSuccessContent('') setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: '特斯拉是美国品牌。请你再想想吧!', - })) + return } + setDragQuizErrors((prev) => ({ + ...prev, + [item.id]: '特斯拉是美国品牌。请你再想想吧!', + })) + setDragQuizValues((prev) => ({ ...prev, [item.id]: [] })) } return ( @@ -287,10 +279,10 @@ const HuangyanpeiInternalization = () => { value={dragQuizValues[item.id] || []} error={dragQuizErrors[item.id]} onChange={handleDragChange} + onSubmit={handleDragSubmit} /> { setActiveSuccess(null) @@ -351,7 +343,6 @@ const HuangyanpeiInternalization = () => { /> { setActiveSuccess(null) diff --git a/src/pages/huangyanpei/place/place1.tsx b/src/pages/huangyanpei/place/place1.tsx index c4eeabe..d84b769 100644 --- a/src/pages/huangyanpei/place/place1.tsx +++ b/src/pages/huangyanpei/place/place1.tsx @@ -131,6 +131,7 @@ const HuangyanpeiPlace1 = () => { }} audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_02.mp3')} ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} diff --git a/src/pages/huangyanpei/place/place3.tsx b/src/pages/huangyanpei/place/place3.tsx index b2aa946..4352422 100644 --- a/src/pages/huangyanpei/place/place3.tsx +++ b/src/pages/huangyanpei/place/place3.tsx @@ -29,8 +29,6 @@ const exhibitSrcMap: Record<1 | 2 | 3, string> = { const FIRST_QUIZ_SUCCESS_CONTENT = '1903年,黄炎培将“观澜书院”改制为“川沙小学堂”,自任校长,率先打破传统经学模式,在课程表中首次列入算术、格致(近代自然科学)、体操和乡土调查四门新学科,并实行“半日授课、半日实习”的教学方式,开创了近代中国小学融入科学、体育与实践教育的新风。' -const DRAG_QUIZ_SUCCESS_CONTENT = '恭喜你,选择正确!' - const DRAG_QUIZ_ERROR_MESSAGE = '艺术不是黄炎培先生在川沙小学堂办学期间强调的重点。请你再想想吧!' const DRAG_QUIZ_OPTIONS = [ @@ -85,10 +83,10 @@ const HuangyanpeiPlace3 = () => { const [dragQuizValue, setDragQuizValue] = useState([]) // 拖拽题错误提示 const [dragQuizError, setDragQuizError] = useState('') - // 成功提示状态 + // 第一题(单选)成功提示状态 const [showSuccessAlert, setShowSuccessAlert] = useState(false) - // 成功提示内容 - const [successAlertContent, setSuccessAlertContent] = useState('') + // 拖拽题成功提示状态(独立状态,避免与第一题成功提示互相干扰导致重复答题) + const [showDragQuizSuccess, setShowDragQuizSuccess] = useState(false) // 光标1、2的引导对话状态(在两者都浏览后显示) const [guide12Visible, setGuide12Visible] = useState(false) @@ -143,41 +141,31 @@ const HuangyanpeiPlace3 = () => { const handleDragQuizChange = (value: DragFillResult[]) => { setDragQuizValue(value) setDragQuizError('') + } - // 检查是否有错误选项(学艺术)- 立即检查并显示错误 - const hasInvalidOption = value.some((v) => v.optionValue === '学艺术') - if (hasInvalidOption) { - setDragQuizError(DRAG_QUIZ_ERROR_MESSAGE) - return - } - - // 检查是否所有空白都已填充 - if (value.length < 2) return - - // 验证答案:第一个空白应该是"读书",第二个空白应该是"做手工" - const blank1 = value.find((v) => v.blankId === 'blank-1') - const blank2 = value.find((v) => v.blankId === 'blank-2') - - // 验证正确答案 + const handleDragQuizSubmit = (value: DragFillResult[]) => { + // 「半天读书、半天做手工」两个空为并列关系,读书/做手工 不分先后(AB/BA 均判对),按集合校验 + const correctSet = new Set([CORRECT_ANSWERS.blank1, CORRECT_ANSWERS.blank2]) + const filled = new Set(value.map((result) => result.optionValue)) const isCorrect = - blank1?.optionValue === CORRECT_ANSWERS.blank1 && - blank2?.optionValue === CORRECT_ANSWERS.blank2 + filled.size === correctSet.size && [...correctSet].every((answer) => filled.has(answer)) if (isCorrect) { - // 答题成功 setShowDragQuiz(false) - setSuccessAlertContent(DRAG_QUIZ_SUCCESS_CONTENT) - setShowSuccessAlert(true) - } else { - // 答题失败 - setDragQuizError('请再想想,确保选择正确的答案。') + setShowDragQuizSuccess(true) + return } + setDragQuizError( + value.some((result) => result.optionValue === '学艺术') + ? DRAG_QUIZ_ERROR_MESSAGE + : '请再想想,确保选择正确的答案。' + ) + setDragQuizValue([]) } // 处理拖拽题成功提示继续 const handleDragQuizSuccessContinue = () => { - setShowSuccessAlert(false) - setSuccessAlertContent('') + setShowDragQuizSuccess(false) setDragQuizValue([]) setDragQuizError('') // 标记展品已访问 @@ -398,7 +386,7 @@ const HuangyanpeiPlace3 = () => { {/* 第一题成功提示 */} @@ -412,18 +400,15 @@ const HuangyanpeiPlace3 = () => { questionSuffix="的模式,特别注重实践。" options={DRAG_QUIZ_OPTIONS} blanks={DRAG_QUIZ_BLANKS} - blankSeparators={['、']} + blankSeparators={['、半天']} value={dragQuizValue} error={dragQuizError} onChange={handleDragQuizChange} + onSubmit={handleDragQuizSubmit} /> {/* 拖拽题成功提示 */} - + {/* 题目3(开群女学) */} { const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null) // 对话引导状态 const [guideVisible, setGuideVisible] = useState(false) + // 对话后的配对题 + const [quizHuangVisible, setQuizHuangVisible] = useState(false) + const [successAlertHuangVisible, setSuccessAlertHuangVisible] = useState(false) // 结束窗口状态 const [endingVisible, setEndingVisible] = useState(false) // 是否已经显示过结束窗口(防止重复弹出) @@ -129,7 +135,13 @@ const HuangyanpeiPlace6 = () => { {/* 对话引导 */} { + setGuideVisible(visible) + // 对话结束后弹出配对题 + if (!visible) { + setQuizHuangVisible(true) + } + }} showMask={false} highlightSelectors={[]} zIndex={10000} @@ -173,6 +185,78 @@ const HuangyanpeiPlace6 = () => { animationSpeed={200} audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_22.mp3')} /> + + {/* 对话后的配对题 */} + + <> + { + const wrongMatch = matches.find((m) => { + if (m.leftId === 'A' && m.rightId !== '2') return true + if (m.leftId === 'B' && m.rightId !== '4') return true + if (m.leftId === 'C' && m.rightId !== '3') return true + if (m.leftId === 'D' && m.rightId !== '1') return true + return false + }) + if (wrongMatch) { + if (wrongMatch.leftId === 'A') { + return '川沙小学堂是黄炎培教育实践的起点,其开设的格致、体操等新课程是他打破旧式教育的主要做法。请你再想想吧!' + } + if (wrongMatch.leftId === 'B') { + return '“双手万能、手脑并用”核心是工学结合、践行技能教育理念。请你再想想吧!' + } + if (wrongMatch.leftId === 'C') { + return '延安窑洞对话尽显黄炎培先生心怀家国、追寻民主和平的赤诚之心。请你再想想吧!' + } + if (wrongMatch.leftId === 'D') { + return '推动中专纳入国家学制,是为职业教育筑牢体系根基、为国育才的重要举措。请你再想想吧!' + } + } + return '连线不正确,请重新尝试!' + }} + onSubmit={(matches, isCorrect) => { + if (isCorrect) { + setQuizHuangVisible(false) + setSuccessAlertHuangVisible(true) + } + }} + /> + + { + setSuccessAlertHuangVisible(false) + // 答对后关闭展品并标记已访问,然后显示ending + setActiveExhibitId(null) + markExhibitVisited(place, 1) + saveHuangyanpeiStore() + setEndingVisible(true) + }} + /> + +
) } diff --git a/src/pages/huangyanpei/reflection/components/question-modal/question-modal.tsx b/src/pages/huangyanpei/reflection/components/question-modal/question-modal.tsx index b35b3f1..0eab9c2 100644 --- a/src/pages/huangyanpei/reflection/components/question-modal/question-modal.tsx +++ b/src/pages/huangyanpei/reflection/components/question-modal/question-modal.tsx @@ -105,8 +105,8 @@ export const QuestionModal = ({ {/* 游戏内容 */}
{/* 标题 */} -
-

+

+

为打破中国教育学与术分离之病,黄先生在川沙小学堂开始了他的教育实践。你看,这里有两套课程表,选一套开启学习吧。

diff --git a/src/pages/huangyanpei/reflection/scene1.tsx b/src/pages/huangyanpei/reflection/scene1.tsx index 01736e4..9769668 100644 --- a/src/pages/huangyanpei/reflection/scene1.tsx +++ b/src/pages/huangyanpei/reflection/scene1.tsx @@ -112,7 +112,7 @@ const HuangyanpeiReflectionScene1 = () => { firstLineIndent="2em" > 同学你好,欢迎来到“跨越时空·体悟”学习模块!{'\n'} - 请登陆“时空回溯・亲历时代”体验仓,下一站:1903年,川沙小学堂。{'\n'} + 请登陆“时空回溯·亲历时代”体验仓,下一站:1903年,川沙小学堂。{'\n'} 你将在“任之”校长所在的学校学习,根据所知和所学,努力完成校长给你的考验,完成全部任务后,可进入到下一阶段的学习。时空门已开,请做好准备迎接考验吧! diff --git a/src/pages/huangyanpei/reflection/scene2.tsx b/src/pages/huangyanpei/reflection/scene2.tsx index c2d9042..bbdaa88 100644 --- a/src/pages/huangyanpei/reflection/scene2.tsx +++ b/src/pages/huangyanpei/reflection/scene2.tsx @@ -233,8 +233,7 @@ const HuangyanpeiReflectionScene2 = () => { {/* 题目成功提示 */} { setShowQuizSuccess(false) }} diff --git a/src/pages/luxun/internalization.tsx b/src/pages/luxun/internalization.tsx index d5771f8..84d7017 100644 --- a/src/pages/luxun/internalization.tsx +++ b/src/pages/luxun/internalization.tsx @@ -141,14 +141,12 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ blankPrefixes: [',如', '如'], blankSeparators: [''], options: [ - { value: '匕首', label: '匕首' }, - { value: '投枪', label: '投枪' }, + { value: '匕首', label: '匕首', isCorrect: true }, + { value: '投枪', label: '投枪', isCorrect: true }, { value: '清泉', label: '清泉' }, ], - correctAnswersMap: { - 'blank-1': '匕首', - 'blank-2': '投枪', - }, + // 两个空是并列关系,匕首/投枪 不分先后(拖 AB 或 BA 均判对),故不设 correctAnswersMap, + // 走 handleDragSubmit 的集合校验分支。唯一的错误情形必然用到“清泉”,故沿用下面的提示。 explanation: '“清泉”不是鲁迅文章的意象。请你再想想吧!', }, { @@ -224,47 +222,33 @@ const LuxunInternalization = () => { const handleDragChange = (val: DragFillResult[]) => { setDragQuizValues((prev) => ({ ...prev, [item.id]: val })) setDragQuizErrors((prev) => ({ ...prev, [item.id]: '' })) + } - if (!item.blanks || val.length < item.blanks.length) return - - // 如果有位置特定的答案映射,使用它进行验证 - if (item.correctAnswersMap) { - const isCorrect = item.blanks.every((blank) => { - const filledResult = val.find((v) => v.blankId === blank.id) - const correctAnswer = item.correctAnswersMap![blank.id] - return filledResult?.optionValue === correctAnswer - }) - - if (isCorrect) { - setActiveSuccess(item.id) - setSuccessContent('') - setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: item.explanation || '再想一想,选择正确答案~', - })) - } - } else { - // 如果没有位置特定的映射,使用原来的逻辑(检查是否包含了所有正确答案) - const filledAnswers = val.map((v) => v.optionValue) - const answerSet = new Set(correctAnswers) - const filledSet = new Set(filledAnswers) - const isCorrect = - answerSet.size === filledSet.size && - [...answerSet].every((answer) => filledSet.has(answer)) - - if (isCorrect) { - setActiveSuccess(item.id) - setSuccessContent('') - setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: item.explanation || '再想一想,选择正确答案~', - })) - } + const handleDragSubmit = (val: DragFillResult[]) => { + const isCorrect = item.correctAnswersMap + ? item.blanks!.every((blank) => { + const filled = val.find((result) => result.blankId === blank.id) + return filled?.optionValue === item.correctAnswersMap![blank.id] + }) + : (() => { + const filledSet = new Set(val.map((result) => result.optionValue)) + const answerSet = new Set(correctAnswers) + return ( + answerSet.size === filledSet.size && + [...answerSet].every((answer) => filledSet.has(answer)) + ) + })() + if (isCorrect) { + setActiveSuccess(item.id) + setSuccessContent('') + setResumeCallback(() => resume) + return } + setDragQuizErrors((prev) => ({ + ...prev, + [item.id]: item.explanation || '再想一想,选择正确答案~', + })) + setDragQuizValues((prev) => ({ ...prev, [item.id]: [] })) } return ( @@ -285,6 +269,7 @@ const LuxunInternalization = () => { value={dragQuizValues[item.id] || []} error={dragQuizErrors[item.id]} onChange={handleDragChange} + onSubmit={handleDragSubmit} /> { }} audioSrc={getPublicPath('audio/luxun/mp3_luxun_02.mp3')} ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} @@ -319,10 +320,10 @@ const LuxunPlace1 = () => { visible={showExhibit2Quiz} title="【多选】鲁迅先生居住在上海景云里的两年半,写下的作品包括:" options={[ - { value: 'A', label: 'A. 阿 Q 正传' }, - { value: 'B', label: 'B. 三闲集' }, - { value: 'C', label: 'C. 二心集' }, - { value: 'D', label: 'D. 朝花夕拾' }, + { value: 'A', label: 'A.阿Q正传' }, + { value: 'B', label: 'B.三闲集' }, + { value: 'C', label: 'C.二心集' }, + { value: 'D', label: 'D.朝花夕拾' }, ]} correctAnswer={['B', 'C']} errorMessage={(selectedValues) => { @@ -346,7 +347,6 @@ const LuxunPlace1 = () => { /> { setShowExhibit2QuizSuccess(false) diff --git a/src/pages/luxun/place/place2.tsx b/src/pages/luxun/place/place2.tsx index 4b0cbc1..862516b 100644 --- a/src/pages/luxun/place/place2.tsx +++ b/src/pages/luxun/place/place2.tsx @@ -305,25 +305,27 @@ const LuxunPlace2 = () => { onChange={(value) => { setDragQuizValue(value) setDragQuizError('') - - // 检查是否所有空白都已填充 - if (value.length < 4) return - - // 验证顺序:必须是 ABCD 顺序 - const correctOrder = ['埋头苦干的人', '拼命硬干的人', '为民请命的人', '舍身求法的人'] - - const isCorrect = value.every((v, index) => { - return v.optionValue === correctOrder[index] + }} + onSubmit={(value) => { + const correctAnswers = [ + '埋头苦干的人', + '拼命硬干的人', + '为民请命的人', + '舍身求法的人', + ] + const isCorrect = correctAnswers.every((answer, index) => { + return ( + value.find((result) => result.blankId === 'blank-' + (index + 1))?.optionValue === + answer + ) }) - if (isCorrect) { - // 答题成功 setShowDragQuiz(false) setShowDragQuizSuccess(true) - } else { - // 答题失败:顺序不对 - setDragQuizError('请你按照鲁迅先生《中国人失掉自信力了吗》的原文拖拽哦~') + return } + setDragQuizError('请你按照鲁迅先生《中国人失掉自信力了吗》的原文拖拽哦~') + setDragQuizValue([]) }} zIndex={10000} /> diff --git a/src/pages/luxun/place/place4.tsx b/src/pages/luxun/place/place4.tsx index 89aa36d..3c03a0f 100644 --- a/src/pages/luxun/place/place4.tsx +++ b/src/pages/luxun/place/place4.tsx @@ -9,6 +9,7 @@ import { Video, type VideoAnchor, } from '@/components' +import { ErrorCorrectionQuizPanel } from '@/components/layout/quiz' import type { VideoRef } from '@/components/video' import { useNavigate, useParams } from 'react-router-dom' import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' @@ -114,9 +115,9 @@ const LuxunPlace5 = () => { visible={!activeVideoSuccess} title="【单选】毛泽东主席称赞鲁迅先生为“最伟大和最英勇的___”。" options={[ - { value: 'A', label: 'A. 旗手' }, - { value: 'B', label: 'B. 作家' }, - { value: 'C', label: 'C. 战士' }, + { value: 'A', label: 'A.旗手' }, + { value: 'B', label: 'B.作家' }, + { value: 'C', label: 'C.战士' }, ]} correctAnswer="A" onWrongSelect={(value: string) => { @@ -150,6 +151,60 @@ const LuxunPlace5 = () => { ), }, + { + id: 'error-correction-anchor', + time: 42, + once: true, + afterResume: 'remove', + render: ({ resume }) => ( + { + if (isCorrect) { + resume() + } + }} + /> + ), + }, ], [activeVideoSuccess, resumeVideoCallback] ) @@ -367,6 +422,7 @@ const LuxunPlace5 = () => { showReadonlyProgress anchors={videoAnchors} onEnded={() => { + setShowEndingVideo(false) setEndingGuideVisible(true) }} /> diff --git a/src/pages/luxun/reflection/scene1.tsx b/src/pages/luxun/reflection/scene1.tsx index 43c7260..7d8a529 100644 --- a/src/pages/luxun/reflection/scene1.tsx +++ b/src/pages/luxun/reflection/scene1.tsx @@ -357,7 +357,7 @@ const LuxunReflectionScene1 = () => { firstLineIndent="2em" > {`同学你好,欢迎来到“跨越时空·体悟”学习模块! - 请登陆“时空回溯・亲历时代”体验仓,下一站:1892年,绍兴三味书屋。 + 请登陆“时空回溯·亲历时代”体验仓,下一站:1892年,绍兴三味书屋。 请按照年代由远及近的顺序,依“手指提示”,点亮先生的创作足迹,解锁更多故事背景!时空门已开,请做好准备吧!`}
diff --git a/src/pages/luxun/reflection/scene2.tsx b/src/pages/luxun/reflection/scene2.tsx index b4d916b..728d876 100644 --- a/src/pages/luxun/reflection/scene2.tsx +++ b/src/pages/luxun/reflection/scene2.tsx @@ -465,8 +465,22 @@ const LuxunReflectionScene2 = () => { className="w-[150px] h-[150px] object-contain" draggable={false} /> -
- {item.name} +
+ {item.id === 'baicaoyuan' ? ( + <> + 《从百草园 +
+ 到三味书屋》 + + ) : ( + item.name + )}
) diff --git a/src/pages/maozedong/internalization.tsx b/src/pages/maozedong/internalization.tsx index 21af67f..c8e15b4 100644 --- a/src/pages/maozedong/internalization.tsx +++ b/src/pages/maozedong/internalization.tsx @@ -47,13 +47,25 @@ const MaozedongInternalization = () => { const [showCompleted, setShowCompleted] = useState(false) const [showTransition, setShowTransition] = useState(false) + const introVideoRef = useRef(null) const videoRef = useRef(null) useEffect(() => {}, []) + // 引入视频加载完成后再播放,避免部分环境下 autoPlay 被拦截 + const handleIntroVideoCanPlay = () => { + if (showIntroVideo && introVideoRef.current) { + introVideoRef.current.play().catch((error) => { + console.warn('引入视频播放失败:', error) + }) + } + } + const handleVideoCanPlay = () => { if (showMainVideo && videoRef.current) { - videoRef.current.play().catch(() => {}) + videoRef.current.play().catch((error) => { + console.warn('视频播放失败:', error) + }) } } @@ -218,52 +230,24 @@ const MaozedongInternalization = () => { afterResume: 'remove', render: ({ resume }) => { const handleDragChange = (val: DragFillResult[]) => { - // quiz-5:参考黄炎培 DragQuizPanel 的用法,不做“每次拖拽都提示”,仅在填满后校验并提示 + // quiz-5 使用 DragQuizPanel(带“提交”按钮),校验交给 handleDragSubmit, + // 这里只更新填入的值并清除上一次的提示 if (q.id === 'quiz-5') { - const valid = val.filter((v) => q.blanks.some((b) => b.id === v.blankId)) - - if (valid.some((v) => v.optionValue === q.wrongValue)) { - setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: q.wrongHint })) - // 错误项拖拽后回到原来的选项框位(从结果中移除) - setDragQuizValues((prev) => ({ - ...prev, - [q.id]: valid.filter((v) => v.optionValue !== q.wrongValue), - })) - return - } - - setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: '' })) - setDragQuizValues((prev) => ({ ...prev, [q.id]: valid })) - - if (valid.length < q.blanks.length) return - - const filledSet = new Set(valid.map((v) => v.optionValue)) - const answerSet = new Set(q.correct) - const isCorrect = - filledSet.size >= answerSet.size && [...answerSet].every((a) => filledSet.has(a)) - - if (isCorrect) { - setActiveSuccess(q.id) - triggerPuzzleAnimation(16) - setResumeCallback(() => resume) - } + setDragQuizValues((prev) => ({ ...prev, [q.id]: val })) + setDragQuizErrors((prev) => ({ ...prev, [q.id]: '' })) return } - const prev = dragQuizValues[q.id] || [] + // 篮子题(quiz-2/3/4):BasketQuizPanel 没有提交按钮,每拖入一个选项即校验 const valid = val.filter((v) => q.blanks.some((b) => b.id === v.blankId)) - const prevOptionSet = new Set(prev.map((v) => v.optionValue)) - const newlyAdded = valid.find((v) => !prevOptionSet.has(v.optionValue)) - const newlyAddedValue = newlyAdded?.optionValue - const isNewlyWrong = newlyAddedValue ? newlyAddedValue === q.wrongValue : false - const isNewlyCorrect = newlyAddedValue ? newlyAddedValue !== q.wrongValue : false + // 拖入了错误选项:给出提示,并把错误项退回选项区 if (valid.some((v) => v.optionValue === q.wrongValue)) { - const wrongMsg = newlyAdded?.optionValue - ? q.optionHints[newlyAdded.optionValue] || q.wrongHint - : q.wrongHint - setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: wrongMsg })) - setDragQuizFeedbackTypes((prevType) => ({ ...prevType, [q.id]: 'error' })) + setDragQuizErrors((prev) => ({ + ...prev, + [q.id]: q.optionHints[q.wrongValue] || q.wrongHint, + })) + setDragQuizFeedbackTypes((prev) => ({ ...prev, [q.id]: 'error' })) setDragQuizValues((prev) => ({ ...prev, [q.id]: valid.filter((v) => v.optionValue !== q.wrongValue), @@ -271,17 +255,21 @@ const MaozedongInternalization = () => { return } - const defaultTip = isNewlyCorrect ? '恭喜你,选择正确!' : '' - const tip = newlyAddedValue ? q.optionHints[newlyAddedValue] || defaultTip : '' - setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: tip })) - if (newlyAddedValue) { - setDragQuizFeedbackTypes((prevType) => ({ - ...prevType, - [q.id]: isNewlyWrong ? 'error' : 'success', - })) - } + // 拖入了正确选项:展示对应的正确提示(绿色) + const prevOptionSet = new Set((dragQuizValues[q.id] || []).map((v) => v.optionValue)) + const newlyAddedValue = valid.find((v) => !prevOptionSet.has(v.optionValue))?.optionValue setDragQuizValues((prev) => ({ ...prev, [q.id]: valid })) + if (newlyAddedValue) { + setDragQuizErrors((prev) => ({ + ...prev, + [q.id]: q.optionHints[newlyAddedValue] || '恭喜你,选择正确!', + })) + setDragQuizFeedbackTypes((prev) => ({ ...prev, [q.id]: 'success' })) + } else { + setDragQuizErrors((prev) => ({ ...prev, [q.id]: '' })) + } + // 所有正确选项都已拖入 → 触发成功 const filledSet = new Set(valid.map((v) => v.optionValue)) const answerSet = new Set(q.correct) const isCorrect = @@ -294,6 +282,29 @@ const MaozedongInternalization = () => { } } + const handleDragSubmit = (val: DragFillResult[]) => { + const filledSet = new Set(val.map((result) => result.optionValue)) + const answerSet = new Set(q.correct) + const isCorrect = + filledSet.size === answerSet.size && + [...answerSet].every((answer) => filledSet.has(answer)) + + if (isCorrect) { + setActiveSuccess(q.id) + if (q.id === 'quiz-5') triggerPuzzleAnimation(16) + if (q.id === 'quiz-2') triggerPuzzleAnimation(15) + setResumeCallback(() => resume) + return + } + const wrongValue = val.find((result) => result.optionValue === q.wrongValue)?.optionValue + setDragQuizErrors((prev) => ({ + ...prev, + [q.id]: wrongValue ? q.optionHints[wrongValue] || q.wrongHint : q.wrongHint, + })) + setDragQuizFeedbackTypes((prev) => ({ ...prev, [q.id]: 'error' })) + setDragQuizValues((prev) => ({ ...prev, [q.id]: [] })) + } + return ( <> {q.id === 'quiz-5' ? ( @@ -309,6 +320,7 @@ const MaozedongInternalization = () => { value={dragQuizValues[q.id] || []} error={dragQuizErrors[q.id]} onChange={handleDragChange} + onSubmit={handleDragSubmit} /> ) : ( { { setActiveSuccess(null) setDragQuizValues((prev) => { @@ -399,13 +409,14 @@ const MaozedongInternalization = () => { {/* 引入视频:进入页面自动播放(暂用其他视频代替) */} {showIntroVideo && (
) } diff --git a/src/pages/maozedong/reflection.tsx b/src/pages/maozedong/reflection.tsx index c9e719e..13f1f55 100644 --- a/src/pages/maozedong/reflection.tsx +++ b/src/pages/maozedong/reflection.tsx @@ -231,7 +231,7 @@ const Home = () => { firstLineIndent="2em" > {`同学你好,欢迎来到“跨越时空·体悟”学习模块! -请登陆“时空回溯・亲历时代”体验仓,下一站:1924年,上海。 +请登陆“时空回溯·亲历时代”体验仓,下一站:1924年,上海。 1924年的上海,革命浪潮暗涌,中共四大筹备工作关乎革命前途。无产阶级领导权、工农联盟等核心议题亟待明确,毛泽东同志在会议筹备的关键节点面临诸多重要抉择。请你化身历史见证者,站在毛主席的视角做出选择,解锁中共四大筹备的关键密码,感受革命先辈的坚定与智慧!完成三次选择后,可进入下一阶段的学习。时空门已开,请做好准备迎接考验吧!`} ), diff --git a/src/pages/nieer/place/place1.tsx b/src/pages/nieer/place/place1.tsx index 471fad5..3df287d 100644 --- a/src/pages/nieer/place/place1.tsx +++ b/src/pages/nieer/place/place1.tsx @@ -258,7 +258,7 @@ const NieerPlace1 = () => { 小沪,聂耳是在九·一八事变爆发后来到的上海吗?他为什么来上海呢? - 九·一八事变爆发前,聂耳就已经来到上海了,核心原因是躲避迫害。聂耳中学就读的云南省立第一师范学校是昆明学生运动的中心,在那里他接触到了马克思主义进步思想,还秘密加入了中国共产主义青年团。 + 九·一八事变爆发前,聂耳就已经来到上海了,核心原因是躲避迫害。聂耳中学时期就读的云南省立第一师范学校是昆明学生运动的中心,在那里他接触到了马克思主义进步思想,还秘密加入了中国共产主义青年团。 也因为他积极参加学生运动,1930年被国民党当局列入了抓捕黑名单,在家人帮助下,他顶替哥哥的身份逃离昆明来到上海。 @@ -377,8 +377,9 @@ const NieerPlace1 = () => { setShowGuideModal(false) setShowAfterDialogGuide(true) }} - ailgn="left" audioSrc={getPublicPath('audio/nieer/mp3_nieer_02.mp3')} + ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} diff --git a/src/pages/nieer/place/place5.tsx b/src/pages/nieer/place/place5.tsx index 6731afd..5e97f4d 100644 --- a/src/pages/nieer/place/place5.tsx +++ b/src/pages/nieer/place/place5.tsx @@ -354,7 +354,7 @@ const NieerPlace5 = () => { { id: 'e2004', label: - '第十届全国人民代表大会二次会议通过宪法修正案,明确规定“中华人民共和国国歌是《义勇军进行曲》”。', + '第十届全国人民代表大会第二次会议通过宪法修正案,明确规定“中华人民共和国国歌是《义勇军进行曲》”。', }, ]} rightItems={[ @@ -389,7 +389,7 @@ const NieerPlace5 = () => { return '1982年12月4日,第五届全国人民代表大会第五次会议正式将《义勇军进行曲》定为中华人民共和国国歌。请你再想想吧!' } if (firstWrongLeft === 'e2004') { - return '2004年3月14日,第十届全国人民代表大会二次会议通过宪法修正案,明确规定“中华人民共和国国歌是《义勇军进行曲》”。请你再想想吧!' + return '2004年3月14日,第十届全国人民代表大会第二次会议通过宪法修正案,明确规定“中华人民共和国国歌是《义勇军进行曲》”。请你再想想吧!' } return '2017年9月1日第十二届全国人民代表大会常务委员会第二十九次会议通过《中华人民共和国国歌法》。请你再想想吧!' }} diff --git a/src/pages/nieer/place/place6.tsx b/src/pages/nieer/place/place6.tsx index 7ec3655..335585e 100644 --- a/src/pages/nieer/place/place6.tsx +++ b/src/pages/nieer/place/place6.tsx @@ -6,6 +6,9 @@ import { Image, Title, } from '@/components' +import { MultipleChoiceQuizPanel } from '@/components/layout/quiz' +import SuccessAlert from '@/components/layout/alert/success-alert' +import ScaledPortal from '@/components/layout/ScaledPortal' import Cursor from '@/components/cursor' import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' import { useRef, useState } from 'react' @@ -27,6 +30,9 @@ const NieerPlace6 = () => { const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null) const [guide1Visible, setGuide1Visible] = useState(false) const [guide1Step, setGuide1Step] = useState(0) + // guide1对话后的多选题 + const [quizNieerVisible, setQuizNieerVisible] = useState(false) + const [successAlertNieerVisible, setSuccessAlertNieerVisible] = useState(false) const total = 2 const visitedExhibits = getVisitedExhibits(place) @@ -35,7 +41,7 @@ const NieerPlace6 = () => { const completed = progressCount >= total const [endingVisible, setEndingVisible] = useState(false) - // 是否已展示过结束语(用于控制“离开场景”按钮出现时机) + // 是否已展示过结束语(用于控制"离开场景"按钮出现时机) // 若用户刷新页面且已完成,则默认视为结束语已展示,避免卡住无法离开 const [endingShown, setEndingShown] = useState(() => completed) const endingShownRef = useRef(false) @@ -97,8 +103,8 @@ const NieerPlace6 = () => { disabled={!isEnabled(2)} /> - {/* 最后一个场景:完成后不显示“前进”按钮 */} - {completed && endingShown && !endingVisible && ( + {/* 最后一个场景:完成后不显示"前进"按钮 */} + {completed && ( { markExhibitVisited(place, closingId) saveNieerStore() - // 仅在“关闭展品”这一刻触发结束语:当本次关闭导致完成时,弹出 EndingOverlay + // 仅在"关闭展品"这一刻触发结束语:当本次关闭导致完成时,弹出 EndingOverlay const nextVisitedSize = visited.has(closingId) ? visited.size : visited.size + 1 const willComplete = nextVisitedSize >= total if (willComplete && !endingShownRef.current) { @@ -148,7 +154,13 @@ const NieerPlace6 = () => { visible={guide1Visible} currentStep={guide1Step} onStepChange={setGuide1Step} - onVisibleChange={setGuide1Visible} + onVisibleChange={(visible) => { + setGuide1Visible(visible) + // 对话结束后弹出题目 + if (!visible) { + setQuizNieerVisible(true) + } + }} showMask={false} highlightSelectors={[]} zIndex={10001} @@ -176,6 +188,79 @@ const NieerPlace6 = () => { animationSpeed={200} onContinue={() => setEndingVisible(false)} /> + + {/* guide1对话后的多选题 */} + + <> + { + if (selectedValues.includes('D')) { + return '国歌从电影插曲走向天安门广场,再写入宪法,影响力从未消退。' + } + + if (selectedValues.includes('A')) { + return '聂耳并非模仿西方,他深入矿山、街头,从中国人的劳动与抗争中汲取灵感。' + } + + if (selectedValues.length < 4) { + return '你的选择还不够全面哦~请你再想想吧!' + } + return '请你再想想吧!' + }} + submitText="提交" + onSubmit={(_values: string[], isCorrect: boolean) => { + if (isCorrect) { + setQuizNieerVisible(false) + setSuccessAlertNieerVisible(true) + } + }} + /> + + { + setSuccessAlertNieerVisible(false) + // 答对后关闭展品并显示结束语 + setActiveExhibitId(null) + markExhibitVisited(place, 1) + saveNieerStore() + setEndingVisible(true) + }} + /> + +
) } diff --git a/src/pages/nieer/reflection/ContentDialog.tsx b/src/pages/nieer/reflection/ContentDialog.tsx index 46f6c6c..df571bc 100644 --- a/src/pages/nieer/reflection/ContentDialog.tsx +++ b/src/pages/nieer/reflection/ContentDialog.tsx @@ -1,5 +1,6 @@ import { Image, Video } from '@/components' import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' +import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext' import { useAudioManager } from '@/hooks/useAudioManager' import { motion } from 'framer-motion' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -42,6 +43,7 @@ interface ContentDialogProps { const ContentDialog = ({ starName, title, scene, onClose, onEnterScene }: ContentDialogProps) => { const [, setContentLoaded] = useState(false) + const { triggerPuzzleAnimation } = usePuzzleAnimation() const pages = useMemo(() => SCENE_PAGES[scene.id] ?? [], [scene.id]) const [pageIndex, setPageIndex] = useState(0) @@ -60,6 +62,7 @@ const ContentDialog = ({ starName, title, scene, onClose, onEnterScene }: Conten channelId: 'nieer-reflection', }) const audioPlayingRef = useRef(false) + const enteringSceneRef = useRef(false) // 预期要播放的音频(仅用于判断“是否真的成功开始播放”) const expectedAudioSrcRef = useRef(null) // 避免同一页图片重复触发 play(),导致音频被反复重置 @@ -89,6 +92,7 @@ const ContentDialog = ({ starName, title, scene, onClose, onEnterScene }: Conten setPageIndex(0) setShowCaption(false) audioPlayingRef.current = false + enteringSceneRef.current = false expectedAudioSrcRef.current = null lastPlayedKeyRef.current = null audioStop() @@ -141,6 +145,23 @@ const ContentDialog = ({ starName, title, scene, onClose, onEnterScene }: Conten // 播放视频时不显示跳过按钮(由视频播放结束后触发寄语/进入下一步)。 const shouldShowSkip = !showCaption && currentPage?.type === 'image' + // 北京场景需要先触发碎片动画,再进入寄语互动;其他场景保持原有进入逻辑。 + const handleEnterScene = useCallback(() => { + if (enteringSceneRef.current) return + + enteringSceneRef.current = true + + if (scene.id === 6) { + try { + triggerPuzzleAnimation(14) + } catch (error) { + console.error('触发北京碎片动画失败', error) + } + } + + onEnterScene(scene.id) + }, [onEnterScene, scene.id, triggerPuzzleAnimation]) + return ( onEnterScene(scene.id)} + onClick={handleEnterScene} /> void onChangeGuizhouInput: (value: string) => void + onStartGuizhouComposition: () => void + onEndGuizhouComposition: (value: string) => void onFinishPiano: () => void onSelectQuote: (value: string) => void onFinishPostcard: () => void @@ -182,8 +185,11 @@ const renderUnlockContent = ( className="w-[875px]" value={localState.guizhouInput} onChange={handlers.onChangeGuizhouInput} + onCompositionStart={handlers.onStartGuizhouComposition} + onCompositionEnd={(event) => + handlers.onEndGuizhouComposition(event.currentTarget.value) + } placeholder="请输入你的爱国感悟(10字以内)" - maxLength={10} />
) @@ -205,7 +211,7 @@ const renderUnlockContent = ( options={SMEDLEY_QUOTES} value={localState.selectedQuote || ''} onChange={handlers.onSelectQuote} - placeholder="请选择一句最触动你的遗言" + placeholder="请选择一句最触动你的话" /> ) @@ -231,6 +237,8 @@ const renderUnlockContent = ( const createUnlockHandlers = ( setSelectedLyric: (value: string) => void, setGuizhouInput: (value: string) => void, + isGuizhouComposing: boolean, + setIsGuizhouComposing: (value: boolean) => void, setPianoFinished: (value: boolean) => void, setSelectedQuote: (value: string) => void, setPostcardFinished: (value: boolean) => void, @@ -239,7 +247,13 @@ const createUnlockHandlers = ( setBeijingSelection: (payload: DialogLocalState['beijingSelection']) => void ): UnlockContentHandlers => ({ onSelectLyric: setSelectedLyric, - onChangeGuizhouInput: (value) => setGuizhouInput(value.slice(0, 10)), + onChangeGuizhouInput: (value) => + setGuizhouInput(isGuizhouComposing ? value : filterInput(value).slice(0, 10)), + onStartGuizhouComposition: () => setIsGuizhouComposing(true), + onEndGuizhouComposition: (value) => { + setIsGuizhouComposing(false) + setGuizhouInput(filterInput(value).slice(0, 10)) + }, onFinishPiano: () => setPianoFinished(true), onSelectQuote: setSelectedQuote, onFinishPostcard: () => setPostcardFinished(true), @@ -251,6 +265,7 @@ const createUnlockHandlers = ( const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProps) => { const [selectedLyric, setSelectedLyric] = useState() const [guizhouInput, setGuizhouInput] = useState('') + const [isGuizhouComposing, setIsGuizhouComposing] = useState(false) const [pianoFinished, setPianoFinished] = useState(false) const [selectedQuote, setSelectedQuote] = useState() const [postcardFinished, setPostcardFinished] = useState(false) @@ -267,6 +282,7 @@ const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProp useEffect(() => { setSelectedLyric(undefined) setGuizhouInput('') + setIsGuizhouComposing(false) setPianoFinished(false) setSelectedQuote(undefined) setPostcardFinished(false) @@ -299,6 +315,8 @@ const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProp const unlockHandlers = createUnlockHandlers( setSelectedLyric, setGuizhouInput, + isGuizhouComposing, + setIsGuizhouComposing, setPianoFinished, setSelectedQuote, setPostcardFinished, @@ -309,6 +327,10 @@ const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProp const handleShareClick = () => { if (state.mode !== 'unlock') return + if (state.sceneId === 2 && guizhouInput.includes('*')) { + showHint('不能包含特殊符号') + return + } onShare(state.sceneId, buildSharePayload(state.sceneId, localState)) } diff --git a/src/pages/nieer/reflection/UnlockHintDialog.tsx b/src/pages/nieer/reflection/UnlockHintDialog.tsx index ec34b71..e8c3264 100644 --- a/src/pages/nieer/reflection/UnlockHintDialog.tsx +++ b/src/pages/nieer/reflection/UnlockHintDialog.tsx @@ -51,7 +51,7 @@ const UnlockHintDialog = ({ {title && (
-
+
{title}
diff --git a/src/pages/nieer/reflection/index.tsx b/src/pages/nieer/reflection/index.tsx index 2cf2c77..0d8bc9f 100644 --- a/src/pages/nieer/reflection/index.tsx +++ b/src/pages/nieer/reflection/index.tsx @@ -15,7 +15,6 @@ import { getUnlockVideoSrcForScene, getUnlockedByIdMap, } from './reflectionProgress' -import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext' import { setCourseTimeProgress } from '@/lib/utils/api-common' const starName = 'nieer' @@ -38,9 +37,6 @@ const NieerReflection = () => { const showGuideModal = !showOpeningVideo && !hasSeenReflectionGuide const showBoxHint = !showOpeningVideo && !showGuideModal && !hasSeenBoxHint - // 获取碎片动画触发方法 - const { triggerPuzzleAnimation } = usePuzzleAnimation() - const { state: dialogState, openScene, @@ -75,7 +71,6 @@ const NieerReflection = () => { } if (allPlacesCompleted) { - triggerPuzzleAnimation(14) if (pendingBadgeVideoSrc) { setPendingEndingVisible(true) } else { @@ -84,7 +79,7 @@ const NieerReflection = () => { } } prevDialogModeRef.current = dialogState.mode - }, [dialogState.mode, allPlacesCompleted, pendingBadgeVideoSrc, triggerPuzzleAnimation]) + }, [dialogState.mode, allPlacesCompleted, pendingBadgeVideoSrc]) const handleResultShare = (sceneId: number, payload?: Record) => { const unlockVideoSrc = getUnlockVideoSrcForScene(sceneId, visitedReflectionCursors) @@ -98,6 +93,7 @@ const NieerReflection = () => { setPendingEndingVisible(false) setEndingVisible(true) } + saveNieerStore() } if (!moduleName) return null @@ -337,8 +333,8 @@ const NieerReflection = () => { firstLineIndent="2em" > {`同学你好,欢迎来到“跨越时空·体悟”学习模块! - 请登陆“时空回溯・亲历时代”体验仓,下一站:1936年,上海。 - 请带上你的旅行包,穿越时空解锁6个国歌故事,集齐精神徽章,绘制属于你的国歌精神图谱吧~ + 请登陆“时空回溯·亲历时代”体验仓,下一站:1936年,上海。 + 请穿越时空解锁6个国歌故事,集齐精神徽章,绘制属于你的国歌精神图谱吧~ 别忘了右下角的旅行箱哦,它会记录你每段旅行的成果!时空门已开,请出发吧!`} @@ -349,7 +345,7 @@ const NieerReflection = () => { text={`恭喜你完成所有时空旅行!成功绘制“国歌精神图谱”,解锁6个跨越时空的国歌故事~国歌的故事还在继续,它的精神藏在每一次唱响、每一次坚守中。愿你带着这份精神力量,做国歌的传承者、爱国的践行者!`} onContinue={() => { setEndingVisible(false) - saveNieerStore() + // saveNieerStore() setCourseTimeProgress(14) navigate(`/${moduleName}/${starName}/home`) }} diff --git a/src/pages/rongdesheng/internalization.tsx b/src/pages/rongdesheng/internalization.tsx index 1cc2fbc..354ca9f 100644 --- a/src/pages/rongdesheng/internalization.tsx +++ b/src/pages/rongdesheng/internalization.tsx @@ -66,10 +66,8 @@ const QUIZ_ANCHORS: QuizAnchor[] = [ explanation: '荣德生的实业布局并未涉及采矿业。请你再想想吧!', }, ], - correctAnswersMap: { - 'blank-1': '面粉', - 'blank-2': '纺织', - }, + // 两个空是并列关系,面粉/纺织 不分先后(拖 AB 或 BA 均判对),故不设 correctAnswersMap, + // 走 handleDragSubmit 的集合校验分支。唯一的错误情形必然用到“采矿”,故沿用下面的提示。 successContent: '', explanation: '荣德生的实业布局并未涉及采矿业。请你再想想吧!', }, @@ -203,37 +201,39 @@ const RongdeshengInternalization = () => { const handleDragChange = (value: DragFillResult[]) => { setDragQuizValues((prev) => ({ ...prev, [item.id]: value })) setDragQuizErrors((prev) => ({ ...prev, [item.id]: '' })) + } - if (value.length < item.blanks!.length) return - - const hasWrongOption = value.some((result) => { - const option = item.options.find((current) => current.value === result.optionValue) - return option && !option.isCorrect - }) - - if (hasWrongOption) { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: item.explanation || '请你再想想吧!', - })) - return - } - - const isCorrect = item.blanks!.every((blank) => { - const filled = value.find((result) => result.blankId === blank.id) - return filled?.optionValue === item.correctAnswersMap?.[blank.id] - }) + const handleDragSubmit = (value: DragFillResult[]) => { + // 有 correctAnswersMap → 按空位校验(顺序相关); + // 无 map → 按 isCorrect 选项做集合校验(顺序无关,AB/BA 均判对) + const isCorrect = item.correctAnswersMap + ? item.blanks!.every((blank) => { + const filled = value.find((result) => result.blankId === blank.id) + return filled?.optionValue === item.correctAnswersMap![blank.id] + }) + : (() => { + const correctAnswers = item.options + .filter((opt) => opt.isCorrect) + .map((opt) => opt.value) + const filledSet = new Set(value.map((result) => result.optionValue)) + const answerSet = new Set(correctAnswers) + return ( + answerSet.size === filledSet.size && + [...answerSet].every((answer) => filledSet.has(answer)) + ) + })() if (isCorrect) { setActiveSuccess(item.id) setSuccessContent(item.successContent || '') setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: item.explanation || '请你再想想吧!', - })) + return } + setDragQuizErrors((prev) => ({ + ...prev, + [item.id]: item.explanation || '请你再想想吧!', + })) + setDragQuizValues((prev) => ({ ...prev, [item.id]: [] })) } return ( @@ -252,6 +252,7 @@ const RongdeshengInternalization = () => { value={dragQuizValues[item.id] || []} error={dragQuizErrors[item.id]} onChange={handleDragChange} + onSubmit={handleDragSubmit} /> { setShowAfterDialogGuide(true) }} ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} diff --git a/src/pages/rongdesheng/place/place2.tsx b/src/pages/rongdesheng/place/place2.tsx index 888693f..a2f20dd 100644 --- a/src/pages/rongdesheng/place/place2.tsx +++ b/src/pages/rongdesheng/place/place2.tsx @@ -399,7 +399,11 @@ const RongdeshengPlace2 = () => { { value: 'D', label: 'D.一味依靠外资扶持发展产业' }, ]} correctAnswer={['A', 'B', 'C']} - errorMessage="很遗憾选择有误,荣氏企业是民族工业自主发展的代表,未依靠外资扶持,其成功源于优质产品、人文管理和精准布局。请你再想想吧!" + errorMessage={(values) => + values.includes('D') + ? '很遗憾选择有误,荣氏企业是民族工业自主发展的代表,未依靠外资扶持,其成功源于优质产品、人文管理和精准布局。请你再想想吧!' + : '你的选择还不够全面哦~请你再想想吧!' + } minSelections={1} onSubmit={(_values, isCorrect) => { if (!isCorrect) return diff --git a/src/pages/rongdesheng/place/place4.tsx b/src/pages/rongdesheng/place/place4.tsx index fbe8893..99295d3 100644 --- a/src/pages/rongdesheng/place/place4.tsx +++ b/src/pages/rongdesheng/place/place4.tsx @@ -8,6 +8,8 @@ import { Title, Video, } from '@/components' +import SuccessAlert from '@/components/layout/alert/success-alert' +import ScaledPortal from '@/components/layout/ScaledPortal' import Cursor from '@/components/cursor' import { useCursorController } from '@/hooks/useCursorController' import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' @@ -66,6 +68,10 @@ const RongdeshengPlace4 = () => { const [guide4Step, setGuide4Step] = useState(0) const [endingGuideVisible, setEndingGuideVisible] = useState(false) + // 结束视频后的单选题 + const [quizRongVisible, setQuizRongVisible] = useState(false) + const [successAlertRongVisible, setSuccessAlertRongVisible] = useState(false) + if (!moduleName) return null const exhibitSrcMap: Record<1 | 2 | 3 | 4, string> = { @@ -103,7 +109,8 @@ const RongdeshengPlace4 = () => { const handleEndingVideoEnded = () => { setShowEndingVideo(false) - setEndingGuideVisible(true) + // 视频结束后先弹出题目 + setQuizRongVisible(true) } return ( @@ -349,7 +356,7 @@ const RongdeshengPlace4 = () => { speaker="student" audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_27.mp3')} > - 新中国成立后,荣氏的企业很快就复工了吗?政府对他的贡献给了肯定吗? + 新中国成立后,荣氏的企业很快就复工了吗?政府对他的贡献给予了肯定吗? { + {/* 结束视频后的单选题 */} + + <> + { + switch (value) { + case 'A': + return '这个说法片面啦!荣德生办厂不是只为赚钱——他拒绝日商收购、坚持内迁保产、捐万包面粉慰劳将士……这些选择,常常不划算,却无比实在。请你再想想吧!' + case 'C': + return '技术与出口很重要,但荣德生的“实”,更在“为谁而做”:兵船牌面粉让百姓吃饱,人钟牌棉纱让国人穿暖,劳工自治区让工人有尊严……“实”是看得见、摸得着、暖人心的担当。请你再想想吧!' + default: + return '请你再想想吧!' + } + }} + onSubmit={(value: string, isCorrect: boolean) => { + if (isCorrect) { + setQuizRongVisible(false) + setSuccessAlertRongVisible(true) + } + }} + /> + + { + setSuccessAlertRongVisible(false) + // 答对后显示结束语对话 + setEndingGuideVisible(true) + }} + /> + + + { > 小提示
-
{text}
+
{text}
) diff --git a/src/pages/rongdesheng/reflection/index.tsx b/src/pages/rongdesheng/reflection/index.tsx index ac8faae..442cc12 100644 --- a/src/pages/rongdesheng/reflection/index.tsx +++ b/src/pages/rongdesheng/reflection/index.tsx @@ -405,7 +405,7 @@ const RongdeshengReflection = () => { firstLineIndent="2em" > 同学你好,欢迎来到“跨越时空·体悟”学习模块!{'\n'} - 请登陆“时空回溯・亲历时代”体验仓,下一站:1915年,上海。{'\n'} + 请登陆“时空回溯·亲历时代”体验仓,下一站:1915年,上海。{'\n'} 欢迎来到申新纺织厂!荣氏兄弟创办的申新纺织厂,从无到有、历经磨难,却始终坚守爱国初心。 接下来请你化身决策助手,和他们一起闯过重重难关,守护民族实业的未来!{'\n'} 温馨提示:每个难关的突破都需要叠加多重因素助力哦! diff --git a/src/pages/sunzhongshan/internalization.tsx b/src/pages/sunzhongshan/internalization.tsx index fe210fc..6da52c8 100644 --- a/src/pages/sunzhongshan/internalization.tsx +++ b/src/pages/sunzhongshan/internalization.tsx @@ -254,45 +254,29 @@ const SunzhongshanInternalization = () => { const handleDragChange = (val: DragFillResult[]) => { setDragQuizValues((prev) => ({ ...prev, [item.id]: val })) setDragQuizErrors((prev) => ({ ...prev, [item.id]: '' })) + } - if (!item.blanks || val.length < item.blanks.length) return - - // 如果有位置特定的答案映射,使用它进行验证 - if (item.correctAnswersMap) { - const isCorrect = item.blanks.every((blank) => { - const filledResult = val.find((v) => v.blankId === blank.id) - const correctAnswer = item.correctAnswersMap![blank.id] - return filledResult?.optionValue === correctAnswer - }) - - if (isCorrect) { - setActiveSuccess(item.id) - setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: '再想一想,选择正确答案~', - })) - } - } else { - // 如果没有位置特定的映射,使用原来的逻辑(检查是否包含了所有正确答案) - const filledAnswers = val.map((v) => v.optionValue) - const answerSet = new Set(correctAnswers) - const filledSet = new Set(filledAnswers) - const isCorrect = - answerSet.size === filledSet.size && - [...answerSet].every((answer) => filledSet.has(answer)) - - if (isCorrect) { - setActiveSuccess(item.id) - setResumeCallback(() => resume) - } else { - setDragQuizErrors((prev) => ({ - ...prev, - [item.id]: '再想一想,选择正确答案~', - })) - } + const handleDragSubmit = (val: DragFillResult[]) => { + const isCorrect = item.correctAnswersMap + ? item.blanks!.every((blank) => { + const filled = val.find((result) => result.blankId === blank.id) + return filled?.optionValue === item.correctAnswersMap![blank.id] + }) + : (() => { + const filledSet = new Set(val.map((result) => result.optionValue)) + const answerSet = new Set(correctAnswers) + return ( + answerSet.size === filledSet.size && + [...answerSet].every((answer) => filledSet.has(answer)) + ) + })() + if (isCorrect) { + setActiveSuccess(item.id) + setResumeCallback(() => resume) + return } + setDragQuizErrors((prev) => ({ ...prev, [item.id]: '再想一想,选择正确答案~' })) + setDragQuizValues((prev) => ({ ...prev, [item.id]: [] })) } return ( @@ -312,6 +296,7 @@ const SunzhongshanInternalization = () => { value={dragQuizValues[item.id] || []} error={dragQuizErrors[item.id]} onChange={handleDragChange} + onSubmit={handleDragSubmit} /> { }} audioSrc={audio('02')} ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} @@ -474,26 +475,24 @@ const SunzhongshanPlace1 = () => { onChange={(value) => { setDragQuiz4Value(value) setDragQuiz4Error('') - // 三个空白未填满前不参与判定 - if (value.length < 3) return - // 集合判等:忽略顺序,三个值需恰好等于 民族 / 民权 / 民生 + }} + onSubmit={(value) => { const correctAnswers = ['民族', '民权', '民生'] - const filledAnswers = value.map((v) => v.optionValue) - const answerSet = new Set(correctAnswers) - const filledSet = new Set(filledAnswers) + const filledAnswers = new Set(value.map((result) => result.optionValue)) const isCorrect = - answerSet.size === filledSet.size && - [...answerSet].every((answer) => filledSet.has(answer)) + filledAnswers.size === correctAnswers.length && + correctAnswers.every((answer) => filledAnswers.has(answer)) if (isCorrect) { setDragQuiz4Visible(false) setDragQuiz4Value([]) setDragQuiz4Error('') setSuccessAlert4Visible(true) - } else { - setDragQuiz4Error( - '三民主义是“民族主义”“民权主义”“民生主义”的合称,是孙中山在进行了一系列挽救民族危亡、探索国家近代化建设的尝试后,从经验中概括提炼而成的理论成果。三民主义形成后,又指导了进一步的革命实践并得到了新的发展。它是中国革命的宝贵精神遗产。' - ) + return } + setDragQuiz4Error( + '三民主义是“民族主义”“民权主义”“民生主义”的合称,是孙中山在进行了一系列挽救民族危亡、探索国家近代化建设的尝试后,从经验中概括提炼而成的理论成果。三民主义形成后,又指导了进一步的革命实践并得到了新的发展。它是中国革命的宝贵精神遗产。' + ) + setDragQuiz4Value([]) }} /> {/* 光标 4 答对后的成功提示:关闭后切到下一句对话 */} @@ -548,7 +547,6 @@ const SunzhongshanPlace1 = () => { {/* 光标 3 答对后的成功提示:关闭时才标记展品 3 已访问 */} { setSuccessAlert3Visible(false) markExhibitVisited(place, 3) @@ -627,7 +625,6 @@ const SunzhongshanPlace1 = () => { {/* 光标 1 答对后的成功提示:onContinue / onClose 任一关闭都标记展品 1 已访问 */} { setSuccessAlert1Visible(false) markExhibitVisited(place, 1) diff --git a/src/pages/sunzhongshan/place/place2.tsx b/src/pages/sunzhongshan/place/place2.tsx index dbb4370..0f1626a 100644 --- a/src/pages/sunzhongshan/place/place2.tsx +++ b/src/pages/sunzhongshan/place/place2.tsx @@ -337,24 +337,24 @@ const SunzhongshanPlace2 = () => { onChange={(value) => { setDragQuiz1Value(value) setDragQuiz1Error('') - if (value.length < 3) return + }} + onSubmit={(value) => { const correctAnswers = ['工业', '交通', '农业'] - const filledAnswers = value.map((v) => v.optionValue) - const answerSet = new Set(correctAnswers) - const filledSet = new Set(filledAnswers) + const filledAnswers = new Set(value.map((result) => result.optionValue)) const isCorrect = - answerSet.size === filledSet.size && - [...answerSet].every((answer) => filledSet.has(answer)) + filledAnswers.size === correctAnswers.length && + correctAnswers.every((answer) => filledAnswers.has(answer)) if (isCorrect) { setDragQuiz1Visible(false) setSuccessAlert1Content('') setSuccessAlert1Visible(true) setPendingQuiz1('complete') - } else { - setDragQuiz1Error( - '《实业计划》是孙中山在为中国规划的工业化蓝图,其内容主要围绕工业、交通、农业等当时亟需发展的传统实践领域,而航天是20世纪中后期才兴起的科技领域,不在这部著作的构想范围内。请你再想想吧!' - ) + return } + setDragQuiz1Error( + '《实业计划》是孙中山在为中国规划的工业化蓝图,其内容主要围绕工业、交通、农业等当时亟需发展的传统实践领域,而航天是20世纪中后期才兴起的科技领域,不在这部著作的构想范围内。请你再想想吧!' + ) + setDragQuiz1Value([]) }} /> diff --git a/src/pages/sunzhongshan/place/place3.tsx b/src/pages/sunzhongshan/place/place3.tsx index cc71fb2..054ba18 100644 --- a/src/pages/sunzhongshan/place/place3.tsx +++ b/src/pages/sunzhongshan/place/place3.tsx @@ -307,7 +307,7 @@ const SunzhongshanPlace3 = () => { visible={dragQuiz2Visible} zIndex={10000} title="" - questionPrefix="【拖拽】中国国民党第一次全国代表大会事实上确立了:" + questionPrefix="【拖拽】中国国民党第一次全国代表大会事实上确立了" questionSuffix="三大政策。" options={[ { value: '联俄', label: '联俄' }, @@ -325,23 +325,24 @@ const SunzhongshanPlace3 = () => { onChange={(value) => { setDragQuiz2Value(value) setDragQuiz2Error('') - if (value.length < 3) return - // 检查答案:需要三个空白都填满,且答案正确 + }} + onSubmit={(value) => { const correctAnswers = ['联俄', '联共', '扶助农工'] - const filledAnswers = value.map((v) => v.optionValue) - const answerSet = new Set(correctAnswers) - const filledSet = new Set(filledAnswers) - const isCorrect = - answerSet.size === filledSet.size && - [...answerSet].every((answer) => filledSet.has(answer)) + const isCorrect = correctAnswers.every((answer, index) => { + return ( + value.find((result) => result.blankId === 'blank' + (index + 1))?.optionValue === + answer + ) + }) if (isCorrect) { setDragQuiz2Visible(false) setSuccessAlert2Visible(true) - } else { - setDragQuiz2Error( - '中国国民党第一次全国代表大会上,孙中山重新解释了三民主义,事实上确立了联俄、联共、扶助农工的三大政策。三大政策是孙中山的重要政治主张,使他倡导的民族民主革命从屡受挫折转向成功进而走上了正确道路。' - ) + return } + setDragQuiz2Error( + '中国国民党第一次全国代表大会上,孙中山重新解释了三民主义,事实上确立了联俄、联共、扶助农工的三大政策。三大政策是孙中山的重要政治主张,使他倡导的民族民主革命从屡受挫折转向成功进而走上了正确道路。' + ) + setDragQuiz2Value([]) }} /> diff --git a/src/pages/sunzhongshan/place/place4.tsx b/src/pages/sunzhongshan/place/place4.tsx index 246d225..0a9bfc6 100644 --- a/src/pages/sunzhongshan/place/place4.tsx +++ b/src/pages/sunzhongshan/place/place4.tsx @@ -10,6 +10,9 @@ import { type VideoRef, EndingOverlay, } from '@/components' +import { SortQuizPanel } from '@/components/layout/quiz' +import SuccessAlert from '@/components/layout/alert/success-alert' +import ScaledPortal from '@/components/layout/ScaledPortal' import { useCursorController } from '@/hooks/useCursorController' import { getBackgroundImage, getStaticImage, getPublicPath } from '@/lib/utils' import { useNavigate, useParams } from 'react-router-dom' @@ -67,6 +70,9 @@ const SunzhongshanPlace4 = () => { // 光标2的对话状态 const [guide2Visible, setGuide2Visible] = useState(false) const [guide2Step, setGuide2Step] = useState(0) + // 光标1视频后的排序题 + const [quizSunVisible, setQuizSunVisible] = useState(false) + const [successAlertSunVisible, setSuccessAlertSunVisible] = useState(false) const handleOpenExhibit = (index: 1 | 2) => { setActiveExhibitId(index) @@ -171,8 +177,8 @@ const SunzhongshanPlace4 = () => { showReadonlyProgress onEnded={() => { setShowVideo1(false) - // 视频播放结束后,显示学习引导 - setShowVideo1Guide(true) + // 视频播放结束后,先显示排序题 + setQuizSunVisible(true) }} /> )} @@ -183,8 +189,6 @@ const SunzhongshanPlace4 = () => { text="通过了解这些展品背后的故事,我们回顾了孙中山先生这位民主革命先驱伟大的一生,不仅看到了孙先生万难不辞、奔波海上的革命足迹,更感受到了他愈挫愈奋、攀登不已的革命精神。下面,让我们跨越时空回到黎明的前夜,与孙先生一起,探寻国家的出路。" onContinue={() => { setShowVideo1Guide(false) - // 学习引导关闭后,修改cursor状态 - markExhibitVisited(place, 1) }} animationSpeed={200} audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_46.mp3')} @@ -252,6 +256,44 @@ const SunzhongshanPlace4 = () => { 大家关心国家前途命运,拥护孙先生,认同他在《北上宣言》中提出的“废除不平等条约”“召开国民会议”等主张,真心希望结束军阀割据,国家实现统一,民族实现独立。 + + {/* 光标1视频后的排序题 */} + + <> + { + if (isCorrect) { + setQuizSunVisible(false) + setSuccessAlertSunVisible(true) + } + }} + /> + + { + setSuccessAlertSunVisible(false) + // 答对后显示结束语 + setShowVideo1Guide(true) + // 学习引导关闭后,修改cursor状态 + markExhibitVisited(place, 1) + }} + /> + + ) } diff --git a/src/pages/sunzhongshan/reflection/index.tsx b/src/pages/sunzhongshan/reflection/index.tsx index 4ba163b..23add0e 100644 --- a/src/pages/sunzhongshan/reflection/index.tsx +++ b/src/pages/sunzhongshan/reflection/index.tsx @@ -211,7 +211,7 @@ const SunzhongshanReflection = () => { audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_47.mp3')} > 同学你好,欢迎来到“跨越时空·体悟”学习模块!{'\n'} - 请登陆“时空回溯・亲历时代”体验仓,下一站:二十世纪初,上海。{'\n'} + 请登陆“时空回溯·亲历时代”体验仓,下一站:二十世纪初,上海。{'\n'} 辛亥革命之后,民主共和的观念深入人心。面对中外反动势力,社会各界更广泛地行动起来,以各自的方式争取着民主自由和国家尊严。 请探索周围环境,拨开时局迷雾,与奋进求索的仁人志士们一同探寻中国革命的前路……本模块包含三个推理小游戏,完成三个场景的探索后,可进入到下一阶段的学习。时空门已开,请做好准备迎接考验吧! diff --git a/src/pages/sunzhongshan/reflection/scene1.tsx b/src/pages/sunzhongshan/reflection/scene1.tsx index c2312f1..9ce3bf7 100644 --- a/src/pages/sunzhongshan/reflection/scene1.tsx +++ b/src/pages/sunzhongshan/reflection/scene1.tsx @@ -159,7 +159,7 @@ const SunzhongshanReflectionScene1 = () => { duration: 1200, onComplete: () => { // 动画完成后显示成功提示 - setClueSuccessAlertContent('获得该线索!快去线索簿查看详细信息吧!') + setClueSuccessAlertContent('获得1条线索,已获得线索可在线索簿中查看!') setShowClueSuccessAlert(true) }, }) diff --git a/src/pages/sunzhongshan/reflection/scene3.tsx b/src/pages/sunzhongshan/reflection/scene3.tsx index 73c9e10..d8c2e55 100644 --- a/src/pages/sunzhongshan/reflection/scene3.tsx +++ b/src/pages/sunzhongshan/reflection/scene3.tsx @@ -95,6 +95,7 @@ const SunzhongshanReflectionScene3 = () => { // 控制 Cursor 显示:获取线索1后显示,点击后进入主场景 const [showCursor, setShowCursor] = useState(false) const [hasClickedClueHint, setHasClickedClueHint] = useState(false) + const [hasClickedMainSceneClueHint, setHasClickedMainSceneClueHint] = useState(false) const [hasClickedClueBook, setHasClickedClueBook] = useState(false) const { collectClue, isClueCollected } = useSunzhongshanStore() @@ -190,7 +191,11 @@ const SunzhongshanReflectionScene3 = () => { // 处理线索提示按钮点击 - 打开线索提示框 const handleClueHintClick = () => { - setHasClickedClueHint(true) + if (isMainScene) { + setHasClickedMainSceneClueHint(true) + } else { + setHasClickedClueHint(true) + } setShowClueHint(true) } @@ -438,7 +443,8 @@ const SunzhongshanReflectionScene3 = () => { button onClick={handleClueHintClick} /> - {!isMainScene && !hasClickedClueHint && ( + {((!isMainScene && !hasClickedClueHint) || + (isMainScene && !hasClickedMainSceneClueHint)) && ( { button onClick={handleBackpackClick} /> - {isMainScene && !isClueCollected(`${sceneId}/clue4`) && ( + {isMainScene && hasClickedMainSceneClueHint && !isClueCollected(`${sceneId}/clue4`) && ( { setShowClueBook(true) }} /> - {isMainScene && isClueCollected(`${sceneId}/clue4`) && !hasClickedClueBook && ( - - )} + {isMainScene && + hasClickedMainSceneClueHint && + isClueCollected(`${sceneId}/clue4`) && + !hasClickedClueBook && ( + + )} @@ -585,12 +594,12 @@ const SunzhongshanReflectionScene3 = () => { { value: 'A', label: - 'A.当前情形,反对军阀势力进而反对列强压迫已成为国共两党乃至各界共识。只是这一目标应当如何达成呢?今年上半年就听说黄埔军校在全国招生的消息,大批青年踊跃报考。可见组建军队,武力北伐是一个公认的选项。然下半年直系军阀倒台,冯玉祥电邀孙先生北上主政,便又打开了和平统一的一线契机,如能实现,亦是应了国人的期盼。今日孙先生行程抵沪,马不停蹄接见各界人士,宣讲北上主张,我辈见之无不钦佩其鞠躬尽瘁之心。前路未明,探索不止,希望国民共同努力,早日迎来和平民主的新天地。', + 'A.今天听到消息,孙先生到上海了,真是让人振奋!现在大家都在反对军阀和列强,有人说靠北伐打过去,但直系倒了,冯玉祥请孙先生北上和谈,也许能和平统一,这也是大家盼望的。孙先生不顾劳累,接见各界人士,宣讲他的主张,真是鞠躬尽瘁。虽然前路还不明朗,但只要大伙共同努力,一定能迎来和平民主的新天地!', }, { value: 'B', label: - 'B.孙中山先生抵沪,来自百年后的我站在欢迎的人群中,终于亲眼见到了这位伟大的革命先行者。1924年的秋天,冯玉祥北京政变为国家的和平统一带来一线契机。回望历史,我们知道,这在中国近代的民族民主革命路途上,不过是一个插曲。但对于孙中山先生而言,抱病北上,不遗余力地宣传反帝反封建主张,几乎成为他为国家前途命运而作的最后争取。他在迷途中坚守,在变局中担当,穷极一生从未停下脚步。伟大复兴的新征程上,我们早已摆脱了屈辱与苦难,向着国家富强的光明前景、人民幸福的美好未来进发。先生“振兴中华”的呐喊犹在耳畔,“天下为公”的初心始终滚烫。今天我们所能做的,便是铭记这份坚守与担当,做跨越百年的追梦路上最坚定的继承者与前行者。', + 'B.孙中山先生抵沪,来自百年后的我站在欢迎的人群中,终于亲眼见到了这位伟大的革命先行者。冯玉祥政变后,先生抱病北上,为和平统一做最后争取,真让人动容。如今百年过去,中国已今非昔比,但“振兴中华”的呐喊依然在耳边。作为新时代青年,我们要铭记这份坚守与担当,在复兴路上勇敢前行!', }, ]} correctAnswer={undefined} diff --git a/src/pages/zhangleping/draw/draw1.tsx b/src/pages/zhangleping/draw/draw1.tsx index 854f8be..d6c8559 100644 --- a/src/pages/zhangleping/draw/draw1.tsx +++ b/src/pages/zhangleping/draw/draw1.tsx @@ -32,14 +32,15 @@ const ZhanglepingDraw1 = () => { const [selectedClothes, setSelectedClothes] = useState('默认') const [selectedBackground, setSelectedBackground] = useState('城际高铁') const [selectedMaterials, setSelectedMaterials] = useState>(new Set()) - const [showCaptionInput, setShowCaptionInput] = useState(false) + const [showCaptionInput, setShowCaptionInput] = useState(true) const [captionText, setCaptionText] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [createdArtworkSrc, setCreatedArtworkSrc] = useState('') const [showCreatedOverlay, setShowCreatedOverlay] = useState(false) const [showCompletionDialogue, setShowCompletionDialogue] = useState(false) const [submitErrorHintKey, setSubmitErrorHintKey] = useState(0) - + const [submitHintText, setSubmitHintText] = useState('作品生成失败,请稍后再试') + const isComposingCaptionRef = useRef(false) const captionInputRef = useRef(null) const compositionRef = useRef(null) @@ -79,6 +80,10 @@ const ZhanglepingDraw1 = () => { }) } + const handleCaptionChange = (value: string) => { + setCaptionText(Array.from(filterInput(value)).slice(0, 15).join('')) + } + const handleSubmitArtwork = async () => { if (!showCaptionInput) { setShowCaptionInput(true) @@ -86,8 +91,20 @@ const ZhanglepingDraw1 = () => { } if (isSubmitting || !compositionRef.current) return + if (captionText.includes('*')) { + setSubmitHintText('不能包含特殊符号') + setSubmitErrorHintKey((key) => key + 1) + return + } + const filteredCaptionText = filterInput(captionText.trim()) + if (!filteredCaptionText) { + setSubmitHintText('请先输入一句简短的正能量文字!') + setSubmitErrorHintKey((key) => key + 1) + return + } if (Array.from(filteredCaptionText).length > 15) { + setSubmitHintText('文字不能超过15个字!') setCaptionText(Array.from(filteredCaptionText).slice(0, 15).join('')) setSubmitErrorHintKey((key) => key + 1) return @@ -102,6 +119,7 @@ const ZhanglepingDraw1 = () => { setShowCreatedOverlay(true) } catch (error) { console.error('Failed to submit draw artwork', error) + setSubmitHintText('作品生成失败,请稍后再试!') setSubmitErrorHintKey((key) => key + 1) } finally { setIsSubmitting(false) @@ -220,9 +238,20 @@ const ZhanglepingDraw1 = () => { name="textInput" maxLength={15} value={captionText} - onChange={(e) => - setCaptionText(Array.from(filterInput(e.target.value)).slice(0, 15).join('')) - } + onCompositionStart={() => { + isComposingCaptionRef.current = true + }} + onCompositionEnd={(e) => { + isComposingCaptionRef.current = false + handleCaptionChange(e.currentTarget.value) + }} + onChange={(e) => { + if (isComposingCaptionRef.current) { + setCaptionText(e.target.value) + return + } + handleCaptionChange(e.target.value) + }} className={cn( 'w-260 h-24 bg-transparent', 'text-4xl! text-[#8B6914] leading-none placeholder:text-3xl!' @@ -256,7 +285,7 @@ const ZhanglepingDraw1 = () => { 0} triggerKey={submitErrorHintKey} durationMs={3200}> - 作品生成失败,请稍后再试 + {submitHintText} {createdArtworkSrc && ( diff --git a/src/pages/zhangleping/place/place1.tsx b/src/pages/zhangleping/place/place1.tsx index 91bccdf..87e1031 100644 --- a/src/pages/zhangleping/place/place1.tsx +++ b/src/pages/zhangleping/place/place1.tsx @@ -225,6 +225,7 @@ const ZhanglepingPlace1 = () => { setShowAfterDialogGuide(true) }} ailgn="left" + firstLineIndent="2em" > 1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。 {'\n'} diff --git a/src/pages/zhangleping/place/place5.tsx b/src/pages/zhangleping/place/place5.tsx index 78e24bb..dfc3b83 100644 --- a/src/pages/zhangleping/place/place5.tsx +++ b/src/pages/zhangleping/place/place5.tsx @@ -6,9 +6,12 @@ import { Image, Title, } from '@/components' +import { SingleChoiceQuizPanel } from '@/components/layout/quiz' +import SuccessAlert from '@/components/layout/alert/success-alert' +import ScaledPortal from '@/components/layout/ScaledPortal' import { useNavigate, useParams } from 'react-router-dom' import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import Cursor from '@/components/cursor' import { useCursorController } from '@/hooks/useCursorController' import { saveZhanglepingStore, useZhanglepingStore } from '@/store/zhanglepingStore' @@ -43,7 +46,11 @@ const ZhanglepingPlace5 = () => { const place = 'place5' const { markExhibitVisited, getVisitedExhibits } = useZhanglepingStore() const visitedExhibits = getVisitedExhibits(place) as (1 | 2 | 3)[] - const visited = new Set<1 | 2 | 3>(visitedExhibits) + const visitedKey = visitedExhibits.join(',') + const visited = useMemo(() => { + if (!visitedKey) return new Set<1 | 2 | 3>() + return new Set<1 | 2 | 3>(visitedKey.split(',').map((id) => Number(id) as 1 | 2 | 3)) + }, [visitedKey]) // 光标1+光标2均查看后的总结对话 const [guide12Visible, setGuide12Visible] = useState(false) @@ -52,7 +59,10 @@ const ZhanglepingPlace5 = () => { // 所有光标已读后的结束语 const [endingVisible, setEndingVisible] = useState(false) - const [endingShown, setEndingShown] = useState(false) + + // guide12对话后的单选题 + const [quizZhangVisible, setQuizZhangVisible] = useState(false) + const [successAlertZhangVisible, setSuccessAlertZhangVisible] = useState(false) const { isEnabled } = useCursorController({ initialEnabled: [1, 2], @@ -222,8 +232,7 @@ const ZhanglepingPlace5 = () => { markExhibitVisited(place, activeExhibitId) } if (activeExhibitId === 3) { - setEndingVisible(true) - // setEndingShown(true) + setQuizZhangVisible(true) } setActiveExhibitId(null) @@ -233,6 +242,52 @@ const ZhanglepingPlace5 = () => { {/* 展品3:打开展品窗口时自动播放话外音 */} {activeExhibitId === 3 &&