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) => (
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 && (
-
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 && }
+ {/* 结束语前的单选题 */}
+
+ <>
+ {
+ switch (value) {
+ case 'A':
+ return '张乐平先生的绘画技艺确实精湛,但这只是他艺术创作的基础,并非贯穿其一生艺术追求的核心主题。他更注重的是用画笔回应时代、服务社会。请你再想想吧!'
+ case 'C':
+ return '张乐平先生的创作初衷并非追求商业利益,而是关注社会现实、表达民众心声。请你再想想吧!'
+ case 'D':
+ return '张乐平先生被誉为“平民大师”,他始终秉持平民立场,将一生献给漫画事业和儿童事业。他追求的不是个人成名,而是用艺术服务社会、传递真善美。请你再想想吧!'
+ default:
+ return '请你再想想吧!'
+ }
+ }}
+ onSubmit={(value: string, isCorrect: boolean) => {
+ if (isCorrect) {
+ setQuizZhangVisible(false)
+ setSuccessAlertZhangVisible(true)
+ }
+ }}
+ />
+
+ {
+ setSuccessAlertZhangVisible(false)
+ // 答对后显示结束语
+ setEndingVisible(true)
+ }}
+ />
+ >
+
+
{/* 结束语:所有光标已读后出现 */}
{
>
同学你好,欢迎来到“跨越时空·体悟”学习模块!
{'\n'}
- 请登陆“时空回溯・亲历时代”体验仓,下一站:1930年,上海。
+ 请登陆“时空回溯·亲历时代”体验仓,下一站:1930年,上海。
{'\n'}
三毛向你发送求助——他的“成长记忆碎片”散落在张乐平故居的5个关键场景中。你需要化身“时光探员”,收集记忆碎片,拼出三毛的成长故事。时空门已开,请做好准备吧!
diff --git a/src/store/chenyunStore.ts b/src/store/chenyunStore.ts
index 64d445b..08d0503 100644
--- a/src/store/chenyunStore.ts
+++ b/src/store/chenyunStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -218,6 +219,7 @@ export const applyChenyunPersistedState = (payload: ChenyunPersistedState): void
})
}
+
export const loadChenyunStore = async (): Promise => {
try {
const result = await getUserDefineResult(CHENYUN_LESSON_ID, CHENYUN_KEY)
@@ -228,7 +230,8 @@ export const loadChenyunStore = async (): Promise => {
return result.status
}
- applyChenyunPersistedState(JSON.parse(result.content) as ChenyunPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyChenyunPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Chenyun store', error)
diff --git a/src/store/huangyanpeiStore.ts b/src/store/huangyanpeiStore.ts
index 2a6a48b..be514e6 100644
--- a/src/store/huangyanpeiStore.ts
+++ b/src/store/huangyanpeiStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -305,6 +306,7 @@ export const applyHuangyanpeiPersistedState = (payload: HuangyanpeiPersistedStat
})
}
+
export const loadHuangyanpeiStore = async (): Promise => {
try {
const result = await getUserDefineResult(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
@@ -315,7 +317,8 @@ export const loadHuangyanpeiStore = async (): Promise =>
return result.status
}
- applyHuangyanpeiPersistedState(JSON.parse(result.content) as HuangyanpeiPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyHuangyanpeiPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Huangyanpei store', error)
diff --git a/src/store/luxunStore.ts b/src/store/luxunStore.ts
index 2cbd6b4..f7eb7ac 100644
--- a/src/store/luxunStore.ts
+++ b/src/store/luxunStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -194,6 +195,7 @@ export const applyLuxunPersistedState = (payload: LuxunPersistedState): void =>
})
}
+
export const loadLuxunStore = async (): Promise => {
try {
const result = await getUserDefineResult(LUXUN_LESSON_ID, LUXUN_KEY)
@@ -204,7 +206,8 @@ export const loadLuxunStore = async (): Promise => {
return result.status
}
- applyLuxunPersistedState(JSON.parse(result.content) as LuxunPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyLuxunPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Luxun store', error)
diff --git a/src/store/maozedongStore.ts b/src/store/maozedongStore.ts
index 94bfbd9..d896d85 100644
--- a/src/store/maozedongStore.ts
+++ b/src/store/maozedongStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -136,7 +137,7 @@ export const saveMaozedongStore = (): void => {
}
}
-export const applyMaozedongPersistedState = (payload: MaozedongPersistedState): void => {
+const applyMaozedongPersistedState = (payload: MaozedongPersistedState): void => {
useMaozedongStore.setState({
visitedExhibits: deserializeVisited(payload.visitedExhibits),
lastVisitedPlace: payload.lastVisitedPlace ?? null,
@@ -154,7 +155,8 @@ export const loadMaozedongStore = async (): Promise => {
return result.status
}
- applyMaozedongPersistedState(JSON.parse(result.content) as MaozedongPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyMaozedongPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Maozedong store', error)
diff --git a/src/store/nieerStore.ts b/src/store/nieerStore.ts
index c094a0d..65e10cf 100644
--- a/src/store/nieerStore.ts
+++ b/src/store/nieerStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -305,6 +306,7 @@ export const applyNieerPersistedState = (payload: NieerPersistedState): void =>
})
}
+
export const loadNieerStore = async (): Promise => {
try {
const result = await getUserDefineResult(NIEER_LESSON_ID, NIEER_KEY)
@@ -315,7 +317,8 @@ export const loadNieerStore = async (): Promise => {
return result.status
}
- applyNieerPersistedState(JSON.parse(result.content) as NieerPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyNieerPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Nieer store', error)
diff --git a/src/store/puzzleStore.ts b/src/store/puzzleStore.ts
index 75160d7..33ecdb9 100644
--- a/src/store/puzzleStore.ts
+++ b/src/store/puzzleStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type StorageOptions,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -209,9 +210,8 @@ export const loadPuzzleStore = async (
return result.status
}
- usePuzzleStore
- .getState()
- .applyPersistedState(JSON.parse(result.content) as PuzzlePersistedState)
+ const payload = parseUserDefineContent(result.content)
+ usePuzzleStore.getState().applyPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load puzzle store', error)
diff --git a/src/store/rongdeshengStore.ts b/src/store/rongdeshengStore.ts
index 63d4789..dc17fa4 100644
--- a/src/store/rongdeshengStore.ts
+++ b/src/store/rongdeshengStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -251,6 +252,7 @@ export const applyRongdeshengPersistedState = (payload: RongdeshengPersistedStat
})
}
+
export const loadRongdeshengStore = async (): Promise => {
try {
const result = await getUserDefineResult(RONGDESHENG_LESSON_ID, RONGDESHENG_KEY)
@@ -261,7 +263,8 @@ export const loadRongdeshengStore = async (): Promise =>
return result.status
}
- applyRongdeshengPersistedState(JSON.parse(result.content) as RongdeshengPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyRongdeshengPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Rongdesheng store', error)
diff --git a/src/store/sunzhongshanStore.ts b/src/store/sunzhongshanStore.ts
index a6e5c82..d4ced5c 100644
--- a/src/store/sunzhongshanStore.ts
+++ b/src/store/sunzhongshanStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -292,6 +293,7 @@ export const applySunzhongshanPersistedState = (payload: SunzhongshanPersistedSt
})
}
+
export const loadSunzhongshanStore = async (): Promise => {
try {
const result = await getUserDefineResult(SUNZHONGSHAN_LESSON_ID, SUNZHONGSHAN_KEY)
@@ -302,7 +304,8 @@ export const loadSunzhongshanStore = async (): Promise =>
return result.status
}
- applySunzhongshanPersistedState(JSON.parse(result.content) as SunzhongshanPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applySunzhongshanPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Sunzhongshan store', error)
diff --git a/src/store/zhanglepingStore.ts b/src/store/zhanglepingStore.ts
index 0bbfb62..ad2c31b 100644
--- a/src/store/zhanglepingStore.ts
+++ b/src/store/zhanglepingStore.ts
@@ -4,6 +4,7 @@ import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
+ parseUserDefineContent,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
@@ -280,6 +281,7 @@ export const applyZhanglepingPersistedState = (payload: ZhanglepingPersistedStat
})
}
+
export const loadZhanglepingStore = async (): Promise => {
try {
const result = await getUserDefineResult(ZHANGLEPING_LESSON_ID, ZHANGLEPING_KEY)
@@ -290,7 +292,8 @@ export const loadZhanglepingStore = async (): Promise =>
return result.status
}
- applyZhanglepingPersistedState(JSON.parse(result.content) as ZhanglepingPersistedState)
+ const payload = parseUserDefineContent(result.content)
+ applyZhanglepingPersistedState(payload)
return result.status
} catch (error) {
console.error('Failed to load Zhangleping store', error)
diff --git a/types/api-common.d.ts b/types/api-common.d.ts
index 938671d..e278c58 100644
--- a/types/api-common.d.ts
+++ b/types/api-common.d.ts
@@ -26,6 +26,8 @@ declare module '@/lib/utils/api-common' {
options?: StorageOptions
): Promise
+ export function parseUserDefineContent(content: string): T
+
export function getUserDefine(
lessonId: string | number,
key: string | number,
diff --git a/vite.config.ts b/vite.config.ts
index 568028c..f03d711 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -13,7 +13,7 @@ export default defineConfig({
},
server: {
host: '0.0.0.0',
- // port: 5000,
+ port: 5174,
},
build: {
rollupOptions: {