feat: update residence learning flows
Commit non-media application and asset changes while leaving audio, video, and environment files out of version control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No files matched your search
@@ -53,7 +53,12 @@
|
||||
"Bash(awk 'NR>=216 && NR<=380' src/config/puzzleData.ts)",
|
||||
"Bash(awk 'NR>=380 && NR<=540' src/config/puzzleData.ts)",
|
||||
"Bash(awk '/^worktree / {wt=$2} /^branch / {br=$2; if \\(wt ~ /\\\\.claude\\\\/worktrees\\\\/agent-/\\) print wt\"\\\\t\"br}')",
|
||||
"Read(//tmp/**)"
|
||||
"Read(//tmp/**)",
|
||||
"Bash(git check-ignore *)",
|
||||
"Bash(git config *)",
|
||||
"WebSearch",
|
||||
"Bash(grep -v \"^--$\")",
|
||||
"Bash(grep -E \"^\\\\S+.*>.*</GuideStepsItem>$\")"
|
||||
],
|
||||
"deny": [
|
||||
"Read(public/**)",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
"bun": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite --mode development",
|
||||
"dev:test": "vite --mode test",
|
||||
"build": "tsc && vite build --mode production",
|
||||
"build:test": "tsc && vite build --mode test",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
|
||||
@@ -8,400 +8,450 @@
|
||||
* http://en.wikipedia.org/wiki/Base64
|
||||
*/
|
||||
|
||||
(function(global) {
|
||||
'use strict';
|
||||
// existing version for noConflict()
|
||||
var _Base64 = global.Base64;
|
||||
var version = "2.1.5";
|
||||
// if node.js, we use Buffer
|
||||
var buffer;
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
buffer = require('buffer').Buffer;
|
||||
;(function (global) {
|
||||
'use strict'
|
||||
// existing version for noConflict()
|
||||
var _Base64 = global.Base64
|
||||
var version = '2.1.5'
|
||||
// if node.js, we use Buffer
|
||||
var buffer
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
buffer = require('buffer').Buffer
|
||||
}
|
||||
// constants
|
||||
var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
var b64tab = (function (bin) {
|
||||
var t = {}
|
||||
for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i
|
||||
return t
|
||||
})(b64chars)
|
||||
var fromCharCode = String.fromCharCode
|
||||
// encoder stuff
|
||||
var cb_utob = function (c) {
|
||||
if (c.length < 2) {
|
||||
var cc = c.charCodeAt(0)
|
||||
return cc < 0x80
|
||||
? c
|
||||
: cc < 0x800
|
||||
? fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f))
|
||||
: fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
|
||||
fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
|
||||
fromCharCode(0x80 | (cc & 0x3f))
|
||||
} else {
|
||||
var cc = 0x10000 + (c.charCodeAt(0) - 0xd800) * 0x400 + (c.charCodeAt(1) - 0xdc00)
|
||||
return (
|
||||
fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) +
|
||||
fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) +
|
||||
fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
|
||||
fromCharCode(0x80 | (cc & 0x3f))
|
||||
)
|
||||
}
|
||||
// constants
|
||||
var b64chars
|
||||
= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
var b64tab = function(bin) {
|
||||
var t = {};
|
||||
for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i;
|
||||
return t;
|
||||
}(b64chars);
|
||||
var fromCharCode = String.fromCharCode;
|
||||
// encoder stuff
|
||||
var cb_utob = function(c) {
|
||||
if (c.length < 2) {
|
||||
var cc = c.charCodeAt(0);
|
||||
return cc < 0x80 ? c
|
||||
: cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
|
||||
+ fromCharCode(0x80 | (cc & 0x3f)))
|
||||
: (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
|
||||
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
|
||||
+ fromCharCode(0x80 | ( cc & 0x3f)));
|
||||
} else {
|
||||
var cc = 0x10000
|
||||
+ (c.charCodeAt(0) - 0xD800) * 0x400
|
||||
+ (c.charCodeAt(1) - 0xDC00);
|
||||
return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
|
||||
+ fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
|
||||
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
|
||||
+ fromCharCode(0x80 | ( cc & 0x3f)));
|
||||
}
|
||||
};
|
||||
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
|
||||
var utob = function(u) {
|
||||
return u.replace(re_utob, cb_utob);
|
||||
};
|
||||
var cb_encode = function(ccc) {
|
||||
var padlen = [0, 2, 1][ccc.length % 3],
|
||||
ord = ccc.charCodeAt(0) << 16
|
||||
| ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
|
||||
| ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
|
||||
chars = [
|
||||
b64chars.charAt( ord >>> 18),
|
||||
b64chars.charAt((ord >>> 12) & 63),
|
||||
padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
|
||||
padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
|
||||
];
|
||||
return chars.join('');
|
||||
};
|
||||
var btoa = global.btoa ? function(b) {
|
||||
return global.btoa(b);
|
||||
} : function(b) {
|
||||
return b.replace(/[\s\S]{1,3}/g, cb_encode);
|
||||
};
|
||||
var _encode = buffer
|
||||
? function (u) { return (new buffer(u)).toString('base64') }
|
||||
: function (u) { return btoa(utob(u)) }
|
||||
;
|
||||
var encode = function(u, urisafe) {
|
||||
return !urisafe
|
||||
? _encode(u)
|
||||
: _encode(u).replace(/[+\/]/g, function(m0) {
|
||||
return m0 == '+' ? '-' : '_';
|
||||
}).replace(/=/g, '');
|
||||
};
|
||||
var encodeURI = function(u) { return encode(u, true) };
|
||||
// decoder stuff
|
||||
var re_btou = new RegExp([
|
||||
'[\xC0-\xDF][\x80-\xBF]',
|
||||
'[\xE0-\xEF][\x80-\xBF]{2}',
|
||||
'[\xF0-\xF7][\x80-\xBF]{3}'
|
||||
].join('|'), 'g');
|
||||
var cb_btou = function(cccc) {
|
||||
switch(cccc.length) {
|
||||
case 4:
|
||||
var cp = ((0x07 & cccc.charCodeAt(0)) << 18)
|
||||
| ((0x3f & cccc.charCodeAt(1)) << 12)
|
||||
| ((0x3f & cccc.charCodeAt(2)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(3)),
|
||||
offset = cp - 0x10000;
|
||||
return (fromCharCode((offset >>> 10) + 0xD800)
|
||||
+ fromCharCode((offset & 0x3FF) + 0xDC00));
|
||||
case 3:
|
||||
return fromCharCode(
|
||||
((0x0f & cccc.charCodeAt(0)) << 12)
|
||||
| ((0x3f & cccc.charCodeAt(1)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(2))
|
||||
);
|
||||
default:
|
||||
return fromCharCode(
|
||||
((0x1f & cccc.charCodeAt(0)) << 6)
|
||||
| (0x3f & cccc.charCodeAt(1))
|
||||
);
|
||||
}
|
||||
};
|
||||
var btou = function(b) {
|
||||
return b.replace(re_btou, cb_btou);
|
||||
};
|
||||
var cb_decode = function(cccc) {
|
||||
var len = cccc.length,
|
||||
padlen = len % 4,
|
||||
n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
|
||||
| (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
|
||||
| (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0)
|
||||
| (len > 3 ? b64tab[cccc.charAt(3)] : 0),
|
||||
chars = [
|
||||
fromCharCode( n >>> 16),
|
||||
fromCharCode((n >>> 8) & 0xff),
|
||||
fromCharCode( n & 0xff)
|
||||
];
|
||||
chars.length -= [0, 0, 2, 1][padlen];
|
||||
return chars.join('');
|
||||
};
|
||||
var atob = global.atob ? function(a) {
|
||||
return global.atob(a);
|
||||
} : function(a){
|
||||
return a.replace(/[\s\S]{1,4}/g, cb_decode);
|
||||
};
|
||||
var _decode = buffer
|
||||
? function(a) { return (new buffer(a, 'base64')).toString() }
|
||||
: function(a) { return btou(atob(a)) };
|
||||
var decode = function(a){
|
||||
return _decode(
|
||||
a.replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' })
|
||||
.replace(/[^A-Za-z0-9\+\/]/g, '')
|
||||
);
|
||||
};
|
||||
var noConflict = function() {
|
||||
var Base64 = global.Base64;
|
||||
global.Base64 = _Base64;
|
||||
return Base64;
|
||||
};
|
||||
// export Base64
|
||||
global.Base64 = {
|
||||
VERSION: version,
|
||||
atob: atob,
|
||||
btoa: btoa,
|
||||
fromBase64: decode,
|
||||
toBase64: encode,
|
||||
utob: utob,
|
||||
encode: encode,
|
||||
encodeURI: encodeURI,
|
||||
btou: btou,
|
||||
decode: decode,
|
||||
noConflict: noConflict
|
||||
};
|
||||
// if ES5 is available, make Base64.extendString() available
|
||||
if (typeof Object.defineProperty === 'function') {
|
||||
var noEnum = function(v){
|
||||
return {value:v,enumerable:false,writable:true,configurable:true};
|
||||
};
|
||||
global.Base64.extendString = function () {
|
||||
Object.defineProperty(
|
||||
String.prototype, 'fromBase64', noEnum(function () {
|
||||
return decode(this)
|
||||
}));
|
||||
Object.defineProperty(
|
||||
String.prototype, 'toBase64', noEnum(function (urisafe) {
|
||||
return encode(this, urisafe)
|
||||
}));
|
||||
Object.defineProperty(
|
||||
String.prototype, 'toBase64URI', noEnum(function () {
|
||||
return encode(this, true)
|
||||
}));
|
||||
};
|
||||
}
|
||||
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g
|
||||
var utob = function (u) {
|
||||
return u.replace(re_utob, cb_utob)
|
||||
}
|
||||
var cb_encode = function (ccc) {
|
||||
var padlen = [0, 2, 1][ccc.length % 3],
|
||||
ord =
|
||||
(ccc.charCodeAt(0) << 16) |
|
||||
((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) |
|
||||
(ccc.length > 2 ? ccc.charCodeAt(2) : 0),
|
||||
chars = [
|
||||
b64chars.charAt(ord >>> 18),
|
||||
b64chars.charAt((ord >>> 12) & 63),
|
||||
padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
|
||||
padlen >= 1 ? '=' : b64chars.charAt(ord & 63),
|
||||
]
|
||||
return chars.join('')
|
||||
}
|
||||
var btoa = global.btoa
|
||||
? function (b) {
|
||||
return global.btoa(b)
|
||||
}
|
||||
: function (b) {
|
||||
return b.replace(/[\s\S]{1,3}/g, cb_encode)
|
||||
}
|
||||
var _encode = buffer
|
||||
? function (u) {
|
||||
return new buffer(u).toString('base64')
|
||||
}
|
||||
: function (u) {
|
||||
return btoa(utob(u))
|
||||
}
|
||||
var encode = function (u, urisafe) {
|
||||
return !urisafe
|
||||
? _encode(u)
|
||||
: _encode(u)
|
||||
.replace(/[+\/]/g, function (m0) {
|
||||
return m0 == '+' ? '-' : '_'
|
||||
})
|
||||
.replace(/=/g, '')
|
||||
}
|
||||
var encodeURI = function (u) {
|
||||
return encode(u, true)
|
||||
}
|
||||
// decoder stuff
|
||||
var re_btou = new RegExp(
|
||||
['[\xC0-\xDF][\x80-\xBF]', '[\xE0-\xEF][\x80-\xBF]{2}', '[\xF0-\xF7][\x80-\xBF]{3}'].join('|'),
|
||||
'g'
|
||||
)
|
||||
var cb_btou = function (cccc) {
|
||||
switch (cccc.length) {
|
||||
case 4:
|
||||
var cp =
|
||||
((0x07 & cccc.charCodeAt(0)) << 18) |
|
||||
((0x3f & cccc.charCodeAt(1)) << 12) |
|
||||
((0x3f & cccc.charCodeAt(2)) << 6) |
|
||||
(0x3f & cccc.charCodeAt(3)),
|
||||
offset = cp - 0x10000
|
||||
return fromCharCode((offset >>> 10) + 0xd800) + fromCharCode((offset & 0x3ff) + 0xdc00)
|
||||
case 3:
|
||||
return fromCharCode(
|
||||
((0x0f & cccc.charCodeAt(0)) << 12) |
|
||||
((0x3f & cccc.charCodeAt(1)) << 6) |
|
||||
(0x3f & cccc.charCodeAt(2))
|
||||
)
|
||||
default:
|
||||
return fromCharCode(((0x1f & cccc.charCodeAt(0)) << 6) | (0x3f & cccc.charCodeAt(1)))
|
||||
}
|
||||
// that's it!
|
||||
})(this);
|
||||
}
|
||||
var btou = function (b) {
|
||||
return b.replace(re_btou, cb_btou)
|
||||
}
|
||||
var cb_decode = function (cccc) {
|
||||
var len = cccc.length,
|
||||
padlen = len % 4,
|
||||
n =
|
||||
(len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) |
|
||||
(len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) |
|
||||
(len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) |
|
||||
(len > 3 ? b64tab[cccc.charAt(3)] : 0),
|
||||
chars = [fromCharCode(n >>> 16), fromCharCode((n >>> 8) & 0xff), fromCharCode(n & 0xff)]
|
||||
chars.length -= [0, 0, 2, 1][padlen]
|
||||
return chars.join('')
|
||||
}
|
||||
var atob = global.atob
|
||||
? function (a) {
|
||||
return global.atob(a)
|
||||
}
|
||||
: function (a) {
|
||||
return a.replace(/[\s\S]{1,4}/g, cb_decode)
|
||||
}
|
||||
var _decode = buffer
|
||||
? function (a) {
|
||||
return new buffer(a, 'base64').toString()
|
||||
}
|
||||
: function (a) {
|
||||
return btou(atob(a))
|
||||
}
|
||||
var decode = function (a) {
|
||||
return _decode(
|
||||
a
|
||||
.replace(/[-_]/g, function (m0) {
|
||||
return m0 == '-' ? '+' : '/'
|
||||
})
|
||||
.replace(/[^A-Za-z0-9\+\/]/g, '')
|
||||
)
|
||||
}
|
||||
var noConflict = function () {
|
||||
var Base64 = global.Base64
|
||||
global.Base64 = _Base64
|
||||
return Base64
|
||||
}
|
||||
// export Base64
|
||||
global.Base64 = {
|
||||
VERSION: version,
|
||||
atob: atob,
|
||||
btoa: btoa,
|
||||
fromBase64: decode,
|
||||
toBase64: encode,
|
||||
utob: utob,
|
||||
encode: encode,
|
||||
encodeURI: encodeURI,
|
||||
btou: btou,
|
||||
decode: decode,
|
||||
noConflict: noConflict,
|
||||
}
|
||||
// if ES5 is available, make Base64.extendString() available
|
||||
if (typeof Object.defineProperty === 'function') {
|
||||
var noEnum = function (v) {
|
||||
return { value: v, enumerable: false, writable: true, configurable: true }
|
||||
}
|
||||
global.Base64.extendString = function () {
|
||||
Object.defineProperty(
|
||||
String.prototype,
|
||||
'fromBase64',
|
||||
noEnum(function () {
|
||||
return decode(this)
|
||||
})
|
||||
)
|
||||
Object.defineProperty(
|
||||
String.prototype,
|
||||
'toBase64',
|
||||
noEnum(function (urisafe) {
|
||||
return encode(this, urisafe)
|
||||
})
|
||||
)
|
||||
Object.defineProperty(
|
||||
String.prototype,
|
||||
'toBase64URI',
|
||||
noEnum(function () {
|
||||
return encode(this, true)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
// that's it!
|
||||
})(this)
|
||||
|
||||
if (this['Meteor']) {
|
||||
Base64 = global.Base64; // for normal export in Meteor.js
|
||||
Base64 = global.Base64 // for normal export in Meteor.js
|
||||
}
|
||||
|
||||
; (function (root) {
|
||||
;(function (root) {
|
||||
// body...
|
||||
|
||||
var videoajax = function (argument) {
|
||||
// body...
|
||||
var host = '/'
|
||||
|
||||
var videoajax = function (argument) {
|
||||
// body...
|
||||
var host = '/';
|
||||
|
||||
this._setHost=function(hoststr) {
|
||||
host = hoststr;
|
||||
};
|
||||
this._getHost = function(){
|
||||
return host;
|
||||
}
|
||||
};
|
||||
|
||||
$.extend(videoajax.prototype,{
|
||||
/**
|
||||
* 设置主域名
|
||||
* @param hosts {String} default: '/' 主域名字符串
|
||||
*/
|
||||
setHost :function(hosts){
|
||||
this._setHost(hosts);
|
||||
},
|
||||
//user_id,course_id,lesson_id,break_point
|
||||
/**
|
||||
* 播放退出时间点
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* user_id {Number} 用户ID
|
||||
* course_id {Number} 课程ID
|
||||
* lesson_id {Number} 课时ID
|
||||
* break_point {Number} 断点
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
quitBreakPoint:function(data,callback){
|
||||
var paramfiled = ["user_id","course_id","lesson_id","break_point"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=addBreakPoint',{
|
||||
userViewPoint:dataStr
|
||||
},function(data){
|
||||
isFunction(callback) ? callback(data.code == 200,data) : '';
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* user_id:123,course_id:23,lesson_id:3
|
||||
*播放历史记录
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* user_id {Number} 用户ID
|
||||
* course_id {Number} 课程ID
|
||||
* lesson_id {Number} 课时ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getBreakPoint:function(data,callback){
|
||||
var paramfiled = ["user_id","course_id","lesson_id"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=userbreakpoint',{
|
||||
userCourseInfo:dataStr
|
||||
},function(data){
|
||||
isFunction(callback) ? callback(data.code == 200,data) : '';
|
||||
// isFunction(callback) ? callback(data) : '';
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
*用户答题答案
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* user_id {Number} 用户ID
|
||||
* course_id {Number} 课程ID
|
||||
* lesson_id {Number} 课时ID
|
||||
* question_order {Number} 问题序号
|
||||
* answer {String} 问题
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getUserAnswer:function(data,callback){
|
||||
var paramfiled = ["user_id","lesson_id","course_id","question_order","answer"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=addUserAnswer',{
|
||||
userAnswerInfo:dataStr
|
||||
},function(data){
|
||||
isFunction(callback) ? callback(data.code == 200,data) : '';
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
/**
|
||||
*播放结束
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* order {Number} 排序ID
|
||||
* co {Number} 课时ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
finishCourse:function(data,callback){
|
||||
var paramfiled = ["order","co","process"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=user&c=learnrecord&a=playcount',data,function(data){
|
||||
isFunction(callback) ? callback(data.code == 200,data) : '';
|
||||
// isFunction(callback) ? callback(data) : '';
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
*课程进度获取
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* order {Number} 排序ID
|
||||
* co {Number} 课时ID
|
||||
*@param callback {Function} 回调方法
|
||||
* 注回调使用的参数,值类似为: {"status":true,"data":{"process":"91"}}
|
||||
* 或: {"status":false,"data":'参数丢失'}
|
||||
* 其中status为true为调用正常, 如果有值则data返回json形式的进度, 没有值则返回[]
|
||||
* status为false时, data保存的是错误提示
|
||||
*/
|
||||
getfinishCourse:function(data,callback){
|
||||
var paramfiled = ["order","co"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=user&c=learnrecord&a=getuserlessionprocess',data,function(data){
|
||||
isFunction(callback) ? callback(data) : '';
|
||||
// isFunction(callback) ? callback(data) : '';
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
*解码加密的breakpoint
|
||||
* @pparam basecode {String} 要解码的字符串
|
||||
*/
|
||||
fromBaseCode : function(basecode){
|
||||
return getBreakPoint(basecode);
|
||||
},
|
||||
/**
|
||||
*通过key值和课时值获取自定的数据
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* lessonId {Number} 课时ID
|
||||
* order {Number} 排序ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getUserDefineInfo :function(data,callback){
|
||||
var paramfiled = ["lessonId","order"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=getUserDefineInfo',{
|
||||
defineDate:dataStr
|
||||
},function(data){
|
||||
isFunction(callback) ? callback(data) : '';
|
||||
});
|
||||
},
|
||||
/**
|
||||
*通过key值和课时值及用户ID获取自定的数据
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* lessonId {Number} 课时ID
|
||||
* order {Number} 排序ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getUserDefineInfoByUser :function(data,callback){
|
||||
var paramfiled = ["lessonId","order"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=getUserDefineInfoByUser',{
|
||||
defineUserDate:dataStr
|
||||
},function(data){
|
||||
isFunction(callback) ? callback(data) : '';
|
||||
});
|
||||
},
|
||||
/**
|
||||
*增加自定数据
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* lessonId {Number} 课时ID
|
||||
* key {string} 键值ID
|
||||
* content {string} 字内容
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
addUserDefine:function(data,callback){
|
||||
var paramfiled = ["lessonId","key","content"];
|
||||
var dataStr = verfiyData(paramfiled,data);
|
||||
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=addUserDefine',{
|
||||
addDefineDate:dataStr
|
||||
},function(data){
|
||||
isFunction(callback) ? callback(data) : '';
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
var getDate = function(url,data,callback){
|
||||
$.get(url,data,function(data){
|
||||
callback(data);
|
||||
},'json')
|
||||
};
|
||||
|
||||
var isFunction = function(fn){
|
||||
if(typeof fn === 'function')return true;
|
||||
return false;
|
||||
this._setHost = function (hoststr) {
|
||||
host = hoststr
|
||||
}
|
||||
var verfiyData = function(field,data){
|
||||
var arr=[];
|
||||
console.log(field);
|
||||
console.log('<br/>');
|
||||
console.log(data);
|
||||
for(var i in field){
|
||||
this._getHost = function () {
|
||||
return host
|
||||
}
|
||||
}
|
||||
|
||||
if(!data[field[i]]){
|
||||
throw new Error('data is error');
|
||||
}
|
||||
arr.push(field[i]+':'+data[field[i]]);
|
||||
$.extend(videoajax.prototype, {
|
||||
/**
|
||||
* 设置主域名
|
||||
* @param hosts {String} default: '/' 主域名字符串
|
||||
*/
|
||||
setHost: function (hosts) {
|
||||
this._setHost(hosts)
|
||||
},
|
||||
//user_id,course_id,lesson_id,break_point
|
||||
/**
|
||||
* 播放退出时间点
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* user_id {Number} 用户ID
|
||||
* course_id {Number} 课程ID
|
||||
* lesson_id {Number} 课时ID
|
||||
* break_point {Number} 断点
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
quitBreakPoint: function (data, callback) {
|
||||
var paramfiled = ['user_id', 'course_id', 'lesson_id', 'break_point']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=zzplatform&c=userview&a=addBreakPoint',
|
||||
{
|
||||
userViewPoint: dataStr,
|
||||
},
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data.code == 200, data) : ''
|
||||
}
|
||||
if(root.JSON){
|
||||
return JSON.stringify(data);
|
||||
}else{
|
||||
return '{'+arr.join(',')+'}';
|
||||
)
|
||||
},
|
||||
/**
|
||||
* user_id:123,course_id:23,lesson_id:3
|
||||
*播放历史记录
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* user_id {Number} 用户ID
|
||||
* course_id {Number} 课程ID
|
||||
* lesson_id {Number} 课时ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getBreakPoint: function (data, callback) {
|
||||
var paramfiled = ['user_id', 'course_id', 'lesson_id']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=zzplatform&c=userview&a=userbreakpoint',
|
||||
{
|
||||
userCourseInfo: dataStr,
|
||||
},
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data.code == 200, data) : ''
|
||||
// isFunction(callback) ? callback(data) : '';
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
*用户答题答案
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* user_id {Number} 用户ID
|
||||
* course_id {Number} 课程ID
|
||||
* lesson_id {Number} 课时ID
|
||||
* question_order {Number} 问题序号
|
||||
* answer {String} 问题
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getUserAnswer: function (data, callback) {
|
||||
var paramfiled = ['user_id', 'lesson_id', 'course_id', 'question_order', 'answer']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=zzplatform&c=userview&a=addUserAnswer',
|
||||
{
|
||||
userAnswerInfo: dataStr,
|
||||
},
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data.code == 200, data) : ''
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
*播放结束
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* order {Number} 排序ID
|
||||
* co {Number} 课时ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
finishCourse: function (data, callback) {
|
||||
var paramfiled = ['order', 'co', 'process']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=user&c=learnrecord&a=playcount',
|
||||
data,
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data.code == 200, data) : ''
|
||||
// isFunction(callback) ? callback(data) : '';
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
*课程进度获取
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* order {Number} 排序ID
|
||||
* co {Number} 课时ID
|
||||
*@param callback {Function} 回调方法
|
||||
* 注回调使用的参数,值类似为: {"status":true,"data":{"process":"91"}}
|
||||
* 或: {"status":false,"data":'参数丢失'}
|
||||
* 其中status为true为调用正常, 如果有值则data返回json形式的进度, 没有值则返回[]
|
||||
* status为false时, data保存的是错误提示
|
||||
*/
|
||||
getfinishCourse: function (data, callback) {
|
||||
var paramfiled = ['order', 'co']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=user&c=learnrecord&a=getuserlessionprocess',
|
||||
data,
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data) : ''
|
||||
// isFunction(callback) ? callback(data) : '';
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
*解码加密的breakpoint
|
||||
* @pparam basecode {String} 要解码的字符串
|
||||
*/
|
||||
fromBaseCode: function (basecode) {
|
||||
return getBreakPoint(basecode)
|
||||
},
|
||||
/**
|
||||
*通过key值和课时值获取自定的数据
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* lessonId {Number} 课时ID
|
||||
* order {Number} 排序ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getUserDefineInfo: function (data, callback) {
|
||||
var paramfiled = ['lessonId', 'order']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=zzplatform&c=userview&a=getUserDefineInfo',
|
||||
{
|
||||
defineDate: dataStr,
|
||||
},
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data) : ''
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
*通过key值和课时值及用户ID获取自定的数据
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* lessonId {Number} 课时ID
|
||||
* order {Number} 排序ID
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
getUserDefineInfoByUser: function (data, callback) {
|
||||
var paramfiled = ['lessonId', 'order']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=zzplatform&c=userview&a=getUserDefineInfoByUser',
|
||||
{
|
||||
defineUserDate: dataStr,
|
||||
},
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data) : ''
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
*增加自定数据
|
||||
* @param data {dataObject} 向后台传递的数据对象,应该包含
|
||||
* lessonId {Number} 课时ID
|
||||
* key {string} 键值ID
|
||||
* content {string} 字内容
|
||||
*@param callback {Function} 回调方法
|
||||
*/
|
||||
addUserDefine: function (data, callback) {
|
||||
var paramfiled = ['lessonId', 'key', 'content']
|
||||
var dataStr = verfiyData(paramfiled, data)
|
||||
getDate(
|
||||
this._getHost() + 'index.php?m=zzplatform&c=userview&a=addUserDefine',
|
||||
{
|
||||
addDefineDate: dataStr,
|
||||
},
|
||||
function (data) {
|
||||
isFunction(callback) ? callback(data) : ''
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
var getDate = function (url, data, callback) {
|
||||
$.get(
|
||||
url,
|
||||
data,
|
||||
function (data) {
|
||||
callback(data)
|
||||
},
|
||||
'json'
|
||||
)
|
||||
}
|
||||
|
||||
var getBreakPoint = function (base64point) {
|
||||
var newbc = base64point.split('').reverse().join('');
|
||||
return Base64.fromBase64(newbc);
|
||||
var isFunction = function (fn) {
|
||||
if (typeof fn === 'function') return true
|
||||
return false
|
||||
}
|
||||
var verfiyData = function (field, data) {
|
||||
var arr = []
|
||||
console.log(field)
|
||||
console.log('<br/>')
|
||||
console.log(data)
|
||||
for (var i in field) {
|
||||
if (!data[field[i]]) {
|
||||
throw new Error('data is error')
|
||||
}
|
||||
arr.push(field[i] + ':' + data[field[i]])
|
||||
}
|
||||
if (root.JSON) {
|
||||
return JSON.stringify(data)
|
||||
} else {
|
||||
return '{' + arr.join(',') + '}'
|
||||
}
|
||||
}
|
||||
|
||||
root.videoAjax = function(){
|
||||
return new videoajax();
|
||||
}
|
||||
}(this));
|
||||
var getBreakPoint = function (base64point) {
|
||||
var newbc = base64point.split('').reverse().join('')
|
||||
return Base64.fromBase64(newbc)
|
||||
}
|
||||
|
||||
root.videoAjax = function () {
|
||||
return new videoajax()
|
||||
}
|
||||
})(this)
|
||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
@@ -7,9 +7,11 @@
|
||||
## 需要的图片文件列表
|
||||
|
||||
### 1. 首页
|
||||
|
||||
- `鲁迅-首页.png` - 首页背景图(放在 luxun 根目录)
|
||||
|
||||
### 2. place1 - 风雨前夕的思想淬炼(探索鲁迅故居一楼客厅)
|
||||
|
||||
- `背景.png` - 场景背景图
|
||||
- `展品1.png` - 展品1图片
|
||||
- `展品2.png` - 展品2图片
|
||||
@@ -18,6 +20,7 @@
|
||||
- `展品5.png` - 展品5图片
|
||||
|
||||
### 3. place2 - 杂文集里的匕首与投枪(探索卧室兼书房)
|
||||
|
||||
- `背景.png` - 场景背景图
|
||||
- `展品1.png` - 展品1图片
|
||||
- `展品2.png` - 展品2图片
|
||||
@@ -25,6 +28,7 @@
|
||||
- `展品4.png` - 展品4图片
|
||||
|
||||
### 4. place3 - 历史新编中的现实呐喊(探索二楼卧室兼书房)
|
||||
|
||||
- `背景.png` - 场景背景图
|
||||
- `展品1.png` - 展品1图片
|
||||
- `展品2.png` - 展品2图片
|
||||
@@ -32,21 +36,25 @@
|
||||
- `展品4.png` - 展品4图片
|
||||
|
||||
### 5. place4 - 为亡友编纂的精神丰碑(探索二楼卧室兼书房的书桌)
|
||||
|
||||
- `背景.png` - 场景背景图
|
||||
- `展品1.png` - 展品1图片
|
||||
- `展品2.png` - 展品2图片
|
||||
- `展品3.png` - 展品3图片
|
||||
|
||||
### 6. place5 - 6平方米的红色庇护所(探索鲁迅故居的客房)
|
||||
|
||||
- `背景.png` - 场景背景图
|
||||
- `展品1.png` - 展品1图片
|
||||
- `展品2.png` - 展品2图片
|
||||
- `展品3.png` - 展品3图片
|
||||
|
||||
### 7. reflection - 体悟模块
|
||||
|
||||
- `底图.png` - 体悟模块背景图
|
||||
|
||||
### 8. internalization - 内化模块
|
||||
|
||||
- `底图.png` - 内化模块背景图
|
||||
|
||||
## 注意事项
|
||||
@@ -54,4 +62,3 @@
|
||||
1. 所有图片文件请使用 PNG 格式
|
||||
2. 文件名请严格按照上述命名,区分大小写
|
||||
3. 入口页的鲁迅图标文件已存在于 `entrance/鲁迅.png`
|
||||
|
||||
|
Before Width: | Height: | Size: 711 KiB After Width: | Height: | Size: 740 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 455 KiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 459 KiB After Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 418 KiB |
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1005 KiB |
|
Before Width: | Height: | Size: 806 KiB After Width: | Height: | Size: 811 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 34 KiB |
@@ -47,30 +47,25 @@ src/components/
|
||||
## 组件模板
|
||||
|
||||
````tsx
|
||||
import React, {
|
||||
forwardRef,
|
||||
useRef,
|
||||
useState,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { forwardRef, useRef, useState, useImperativeHandle } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** 组件配置项 */
|
||||
export interface ComponentNameProps {
|
||||
/** 必填属性说明 */
|
||||
requiredProp: string;
|
||||
requiredProp: string
|
||||
/** 可选属性说明 */
|
||||
optionalProp?: boolean;
|
||||
optionalProp?: boolean
|
||||
/** 子元素 */
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode
|
||||
/** 自定义类名 */
|
||||
className?: string;
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** 组件 Ref 暴露的方法 */
|
||||
export interface ComponentNameRef {
|
||||
/** 方法说明 */
|
||||
methodName: () => void;
|
||||
methodName: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,33 +79,33 @@ export interface ComponentNameRef {
|
||||
const ComponentName = forwardRef<ComponentNameRef, ComponentNameProps>(
|
||||
({ requiredProp, optionalProp = false, className, children }, ref) => {
|
||||
// 状态定义
|
||||
const [state, setState] = useState<string>("");
|
||||
const [state, setState] = useState<string>('')
|
||||
|
||||
// Ref 定义
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
methodName: () => {
|
||||
// 实现
|
||||
},
|
||||
}));
|
||||
}))
|
||||
|
||||
return <div className={cn("base-class", className)}>{children}</div>;
|
||||
return <div className={cn('base-class', className)}>{children}</div>
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
ComponentName.displayName = "ComponentName";
|
||||
ComponentName.displayName = 'ComponentName'
|
||||
|
||||
export default ComponentName;
|
||||
export default ComponentName
|
||||
````
|
||||
|
||||
## index.ts 导出模板
|
||||
|
||||
```ts
|
||||
export { default } from "./component-name";
|
||||
export { default as ComponentName } from "./component-name";
|
||||
export type { ComponentNameProps, ComponentNameRef } from "./component-name";
|
||||
export { default } from './component-name'
|
||||
export { default as ComponentName } from './component-name'
|
||||
export type { ComponentNameProps, ComponentNameRef } from './component-name'
|
||||
```
|
||||
|
||||
## 类型规范
|
||||
|
||||
@@ -79,6 +79,9 @@ const ExhibitOverlay = ({
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const viewElRef = useRef<HTMLDivElement | null>(null)
|
||||
const frameRef = useRef<number | null>(null)
|
||||
const openTimerRef = useRef<number | null>(null)
|
||||
const wasVisibleRef = useRef(false)
|
||||
const onOpenRef = useRef(onOpen)
|
||||
const pointers = useRef<Map<number, Point>>(new Map())
|
||||
const lastDistance = useRef<number | null>(null)
|
||||
// 拖拽初始状态:记录 pointerDown 时的状态
|
||||
@@ -137,10 +140,25 @@ const ExhibitOverlay = ({
|
||||
}, [applyTransformStyle])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
onOpen?.()
|
||||
onOpenRef.current = onOpen
|
||||
}, [onOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && !wasVisibleRef.current) {
|
||||
openTimerRef.current = window.setTimeout(() => {
|
||||
onOpenRef.current?.()
|
||||
openTimerRef.current = null
|
||||
}, 0)
|
||||
}
|
||||
}, [visible, onOpen])
|
||||
wasVisibleRef.current = visible
|
||||
|
||||
return () => {
|
||||
if (openTimerRef.current) {
|
||||
window.clearTimeout(openTimerRef.current)
|
||||
openTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
// 关闭或切换时重置
|
||||
useEffect(() => {
|
||||
|
||||
@@ -149,6 +149,13 @@ export const StudyGuideModal = ({
|
||||
// }
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (audioSrc && currentSrc === audioSrc) {
|
||||
stop()
|
||||
}
|
||||
onContinue?.()
|
||||
}
|
||||
|
||||
const contentChildren = renderIndentedContent(children, firstLineIndent)
|
||||
|
||||
const content = (
|
||||
@@ -204,7 +211,7 @@ export const StudyGuideModal = ({
|
||||
src={getStaticImage('components/guide/继续')}
|
||||
alt="继续"
|
||||
button
|
||||
onClick={onContinue}
|
||||
onClick={handleContinue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const RESIDENCES_WITH_PUZZLE = [
|
||||
'maozedong',
|
||||
'nieer',
|
||||
'zhangleping',
|
||||
'rongdesheng',
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -186,48 +187,52 @@ const ResidenceLayoutContent = () => {
|
||||
// 处理碎片动画触发(仅支持拼图功能的故居)
|
||||
const handleTriggerPuzzleAnimation = useCallback(
|
||||
(pieceId: number, options?: { onComplete?: () => void }) => {
|
||||
if (!residence || !hasPuzzleFeature) {
|
||||
options?.onComplete?.()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// 防止重复执行动画
|
||||
if (animationInProgressRef.current) {
|
||||
options?.onComplete?.()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// 检查碎片是否已解锁
|
||||
if (isPuzzleUnlocked(residence, pieceId)) {
|
||||
options?.onComplete?.()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// 标记动画正在进行
|
||||
animationInProgressRef.current = true
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
animationCompletionRef.current = resolve
|
||||
animationCompletionCallbackRef.current = options?.onComplete ?? null
|
||||
window.setTimeout(() => {
|
||||
if (!residence || !hasPuzzleFeature) {
|
||||
options?.onComplete?.()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 开始动画流程:禁用所有操作
|
||||
setIsAnimationPlaying(true)
|
||||
setPuzzlePieceId(pieceId)
|
||||
setShowPuzzlePiece(true)
|
||||
// 防止重复执行动画
|
||||
if (animationInProgressRef.current) {
|
||||
options?.onComplete?.()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 使用双重 requestAnimationFrame 确保 Portal 渲染完成
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
// 等待初始出现动画(旋转+放大)完成
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.APPEAR_DURATION))
|
||||
// 检查碎片是否已解锁
|
||||
if (isPuzzleUnlocked(residence, pieceId)) {
|
||||
options?.onComplete?.()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 等待发光动画显示一段时间,让用户看到碎片
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.GLOW_DURATION))
|
||||
// 标记动画正在进行
|
||||
animationInProgressRef.current = true
|
||||
animationCompletionRef.current = resolve
|
||||
animationCompletionCallbackRef.current = options?.onComplete ?? null
|
||||
|
||||
// 等待元素准备就绪并执行飞行动画
|
||||
await waitForElementsAndAnimate(pieceId)
|
||||
// 开始动画流程:禁用所有操作
|
||||
setIsAnimationPlaying(true)
|
||||
setPuzzlePieceId(pieceId)
|
||||
setShowPuzzlePiece(true)
|
||||
|
||||
// 使用双重 requestAnimationFrame 确保 Portal 渲染完成
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
// 等待初始出现动画(旋转+放大)完成
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.APPEAR_DURATION))
|
||||
|
||||
// 等待发光动画显示一段时间,让用户看到碎片
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.GLOW_DURATION))
|
||||
|
||||
// 等待元素准备就绪并执行飞行动画
|
||||
await waitForElementsAndAnimate(pieceId)
|
||||
})
|
||||
})
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
},
|
||||
[
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createPortal } from 'react-dom'
|
||||
import { useScale } from '@/contexts/ScaleContext'
|
||||
|
||||
export interface ScaledPortalProps {
|
||||
/** 是否显示 */
|
||||
show?: boolean
|
||||
/** 是否启用(不启用则直接渲染 children) */
|
||||
enabled?: boolean
|
||||
/** z-index 层级(建议 > ExhibitOverlay 的 9999) */
|
||||
@@ -17,7 +19,12 @@ export interface ScaledPortalProps {
|
||||
* - 使用 Portal 渲染到 document.body,避免被其他 Portal/遮罩层挡住
|
||||
* - 使用 useScale 的 offset/scale 做视觉对齐(与 ExhibitOverlay 一致)
|
||||
*/
|
||||
const ScaledPortal = ({ enabled = true, zIndex = 10050, children }: ScaledPortalProps) => {
|
||||
const ScaledPortal = ({
|
||||
enabled = true,
|
||||
zIndex = 10050,
|
||||
show = true,
|
||||
children,
|
||||
}: ScaledPortalProps) => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale()
|
||||
|
||||
@@ -25,6 +32,7 @@ const ScaledPortal = ({ enabled = true, zIndex = 10050, children }: ScaledPortal
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!show) return null
|
||||
if (!enabled) return <>{children}</>
|
||||
if (!mounted) return null
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const ErrorAlert = ({
|
||||
{/* 标题和副标题容器 - 带深色背景条 */}
|
||||
<div className="w-full bg-black/70 px-20 py-20 flex flex-col items-center gap-4">
|
||||
{/* 副标题 */}
|
||||
<div className="text-white text-5xl font-medium tracking-widest">{text}</div>
|
||||
<div className="text-white text-5xl font-medium tracking-widest leading-18">{text}</div>
|
||||
</div>
|
||||
|
||||
{/* 继续按钮 */}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface SuccessAlertProps {
|
||||
* 使用 components/正确版 作为背景
|
||||
*/
|
||||
const SuccessAlert = ({
|
||||
title = '恭喜你, 选择正确!',
|
||||
title = '恭喜你,选择正确!',
|
||||
content,
|
||||
visible = true,
|
||||
audioSrc,
|
||||
@@ -108,16 +108,12 @@ const SuccessAlert = ({
|
||||
if (audioSrc && currentSrc === audioSrc) {
|
||||
stop()
|
||||
}
|
||||
// 只调用 onContinue,它会处理关闭逻辑
|
||||
if (onContinue) {
|
||||
onContinue()
|
||||
} else if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
onContinue?.()
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center">
|
||||
<div className="fixed inset-0 z-10000 flex items-center justify-center">
|
||||
{/* 蒙版背景 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
|
||||
@@ -55,7 +55,7 @@ const DragQuizPanel = ({
|
||||
'inline-flex items-center justify-center min-w-[140px] h-[56px] px-6 mx-3',
|
||||
'rounded-xl border-2 transition-all duration-300 shadow-[0_4px_16px_rgba(0,0,0,0.2)]',
|
||||
filledValue
|
||||
? 'border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] backdrop-blur-sm'
|
||||
? 'border-[#f0be75] bg-linear-to-br from-[#3a3020] to-[#2a2520] backdrop-blur-sm'
|
||||
: 'border-[#d4c5a9]/60 bg-white/10 backdrop-blur-sm hover:border-[#d4c5a9]'
|
||||
)}
|
||||
>
|
||||
@@ -111,11 +111,11 @@ const DragQuizPanel = ({
|
||||
'text-[32px] font-bold border-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)]',
|
||||
isUsed
|
||||
? 'border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50'
|
||||
: 'border-[#d4c5a9] bg-gradient-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3] cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)] hover:scale-[1.02]'
|
||||
: 'border-[#d4c5a9] bg-linear-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3] cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)] hover:scale-[1.02]'
|
||||
)}
|
||||
>
|
||||
{!isUsed && (
|
||||
<div className="absolute inset-0 rounded-[32px] bg-gradient-to-br from-[#d4c5a9]/20 to-transparent pointer-events-none" />
|
||||
<div className="absolute inset-0 rounded-[32px] bg-linear-to-br from-[#d4c5a9]/20 to-transparent pointer-events-none" />
|
||||
)}
|
||||
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)]">
|
||||
{option.label}
|
||||
|
||||
@@ -22,57 +22,55 @@
|
||||
### 使用示例
|
||||
|
||||
```tsx
|
||||
import { SelectFillQuizPanel } from "@/components";
|
||||
import { SelectFillQuizPanel } from '@/components'
|
||||
|
||||
function MyComponent() {
|
||||
const [showQuiz, setShowQuiz] = useState(false);
|
||||
const [showQuiz, setShowQuiz] = useState(false)
|
||||
|
||||
return (
|
||||
<SelectFillQuizPanel
|
||||
visible={showQuiz}
|
||||
titlePrefix="【填空】毛泽东主席认为,到了"
|
||||
titleSuffix="年夏天,在理论上,而且在某种程度的行动上,他已成为一个马克思主义者。"
|
||||
blanks={[
|
||||
{ id: "year", prefix: "____" },
|
||||
]}
|
||||
blanks={[{ id: 'year', prefix: '____' }]}
|
||||
options={[
|
||||
{ value: "1919", label: "1919" },
|
||||
{ value: "1920", label: "1920" },
|
||||
{ value: "1921", label: "1921" },
|
||||
{ value: '1919', label: '1919' },
|
||||
{ value: '1920', label: '1920' },
|
||||
{ value: '1921', label: '1921' },
|
||||
]}
|
||||
correctAnswers={{ year: "1920" }}
|
||||
correctAnswers={{ year: '1920' }}
|
||||
errorMessage="回想一下 1920 年 5 月到 7 月在上海居住的这段经历~"
|
||||
onSubmit={(results, isCorrect) => {
|
||||
console.log("选择结果:", results, "是否正确:", isCorrect);
|
||||
console.log('选择结果:', results, '是否正确:', isCorrect)
|
||||
if (isCorrect) {
|
||||
setShowQuiz(false);
|
||||
setShowQuiz(false)
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.log("错误信息:", error);
|
||||
console.log('错误信息:', error)
|
||||
}}
|
||||
submitText="确定"
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| titlePrefix | string | - | 整体题干前缀文本(可选) |
|
||||
| titleSuffix | string | - | 整体题干后缀文本(可选) |
|
||||
| blanks | SelectFillBlank[] | - | 每个填空位的配置(必填,包含 id / prefix / suffix) |
|
||||
| options | { value: string; label: string }[] | - | 候选项列表(必填) |
|
||||
| correctAnswers | Record<string, string> | - | 正确答案映射(可选,不传则不做校验,直接判定为正确) |
|
||||
| errorMessage | string | `"还不太对,再仔细对照题干和提示想一想吧~"` | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (results: SelectFillResult[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| -------------- | --------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------- |
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| titlePrefix | string | - | 整体题干前缀文本(可选) |
|
||||
| titleSuffix | string | - | 整体题干后缀文本(可选) |
|
||||
| blanks | SelectFillBlank[] | - | 每个填空位的配置(必填,包含 id / prefix / suffix) |
|
||||
| options | { value: string; label: string }[] | - | 候选项列表(必填) |
|
||||
| correctAnswers | Record<string, string> | - | 正确答案映射(可选,不传则不做校验,直接判定为正确) |
|
||||
| errorMessage | string | `"还不太对,再仔细对照题干和提示想一想吧~"` | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (results: SelectFillResult[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
|
||||
### 行为说明
|
||||
|
||||
@@ -98,53 +96,53 @@ function MyComponent() {
|
||||
### 使用方法
|
||||
|
||||
```tsx
|
||||
import { SingleChoiceQuizPanel } from "@/components";
|
||||
import { SingleChoiceQuizPanel } from '@/components'
|
||||
|
||||
function MyComponent() {
|
||||
const [showQuiz, setShowQuiz] = useState(false);
|
||||
const [showQuiz, setShowQuiz] = useState(false)
|
||||
|
||||
return (
|
||||
<SingleChoiceQuizPanel
|
||||
visible={showQuiz}
|
||||
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="B"
|
||||
errorMessage="再想一想,注意题干中的时间和提示信息哦~"
|
||||
onSubmit={(value, isCorrect) => {
|
||||
console.log("用户选择:", value);
|
||||
console.log("是否正确:", isCorrect);
|
||||
console.log('用户选择:', value)
|
||||
console.log('是否正确:', isCorrect)
|
||||
if (isCorrect) {
|
||||
// 选择正确,可以关闭面板或进入下一步
|
||||
setShowQuiz(false);
|
||||
setShowQuiz(false)
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.log("错误信息:", error);
|
||||
console.log('错误信息:', error)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | SingleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string | - | 正确答案(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (value: string, isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ------------------------------------------- | -------------------------------------- | ------------------------------------ |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------ | ------ | -------- | ------ |
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | SingleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string | - | 正确答案(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (value: string, isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
|
||||
### 行为说明
|
||||
|
||||
@@ -168,61 +166,61 @@ function MyComponent() {
|
||||
### 使用方法
|
||||
|
||||
```tsx
|
||||
import { MultipleChoiceQuizPanel } from "@/components";
|
||||
import { MultipleChoiceQuizPanel } from '@/components'
|
||||
|
||||
function MyComponent() {
|
||||
const [showQuiz, setShowQuiz] = useState(false);
|
||||
const [showQuiz, setShowQuiz] = useState(false)
|
||||
|
||||
return (
|
||||
<MultipleChoiceQuizPanel
|
||||
visible={showQuiz}
|
||||
title="这是一道多选题:以下哪些选项是正确的?(可多选)"
|
||||
options={[
|
||||
{ value: "A", label: "选项A" },
|
||||
{ value: "B", label: "选项B" },
|
||||
{ value: "C", label: "选项C" },
|
||||
{ value: "D", label: "选项D" },
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
{ value: 'D', label: '选项D' },
|
||||
]}
|
||||
correctAnswer={["A", "B"]}
|
||||
correctAnswer={['A', 'B']}
|
||||
errorMessage="很抱歉,你选错了!请你再思考一下吧!"
|
||||
minSelections={1}
|
||||
maxSelections={3}
|
||||
minSelectionsErrorMessage="请至少选择 1 项才能提交"
|
||||
submitText="确认提交"
|
||||
onSubmit={(values, isCorrect) => {
|
||||
console.log("用户选择:", values);
|
||||
console.log("是否正确:", isCorrect);
|
||||
console.log('用户选择:', values)
|
||||
console.log('是否正确:', isCorrect)
|
||||
if (isCorrect) {
|
||||
setShowQuiz(false);
|
||||
setShowQuiz(false)
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.log("错误信息:", error);
|
||||
console.log('错误信息:', error)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | MultipleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string[] | - | 正确答案数组(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| minSelectionsErrorMessage | string | `请至少选择 ${minSelections} 项` | 自定义最小选择数量不足时的提示信息 |
|
||||
| onSubmit | (values: string[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| minSelections | number | - | 最小选择数量 |
|
||||
| maxSelections | number | - | 最大选择数量 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------- | ---------------------------------------------- | -------------------------------------- | ------------------------------------ |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------ | ------ | -------- | ------ |
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | MultipleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string[] | - | 正确答案数组(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| minSelectionsErrorMessage | string | `请至少选择 ${minSelections} 项` | 自定义最小选择数量不足时的提示信息 |
|
||||
| onSubmit | (values: string[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| minSelections | number | - | 最小选择数量 |
|
||||
| maxSelections | number | - | 最大选择数量 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
|
||||
### 行为说明
|
||||
|
||||
@@ -237,6 +235,7 @@ function MyComponent() {
|
||||
### 从旧版 QuizPanel 迁移
|
||||
|
||||
**旧版用法:**
|
||||
|
||||
```tsx
|
||||
<QuizPanel
|
||||
visible={showQuiz}
|
||||
@@ -245,17 +244,18 @@ function MyComponent() {
|
||||
value={quizAnswer}
|
||||
error={quizError}
|
||||
onChange={(val) => {
|
||||
setQuizAnswer(val);
|
||||
if (val === "B") {
|
||||
setQuizError("");
|
||||
setQuizAnswer(val)
|
||||
if (val === 'B') {
|
||||
setQuizError('')
|
||||
} else {
|
||||
setQuizError("很抱歉,你选错了!");
|
||||
setQuizError('很抱歉,你选错了!')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**新版用法:**
|
||||
|
||||
```tsx
|
||||
<SingleChoiceQuizPanel
|
||||
visible={showQuiz}
|
||||
|
||||
@@ -159,7 +159,6 @@ const SingleChoiceQuizPanel = ({
|
||||
<SuccessAlert
|
||||
{...successAlertProps}
|
||||
visible={successAlertVisible}
|
||||
onContinue={handleSuccessContinue}
|
||||
onClose={handleSuccessContinue}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,9 +8,9 @@ import { showHint } from '@/components/hint'
|
||||
const formatCurrentDate = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = `${now.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${now.getDate()}`.padStart(2, '0')
|
||||
return `${year}.${month}.${day}`
|
||||
const month = now.getMonth() + 1
|
||||
const day = now.getDate()
|
||||
return `${year}年${month}月${day}日`
|
||||
}
|
||||
|
||||
export type LetterFieldType = 'text' | 'input' | 'select'
|
||||
@@ -305,12 +305,14 @@ const LetterTemplate = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute right-[190px] bottom-[120px] z-99 flex flex-col items-end gap-2 text-[#4e402e]">
|
||||
<div className="absolute right-[190px] bottom-[120px] z-100 flex flex-col items-end gap-2 text-[#4e402e]">
|
||||
<style>{`.signature-name-input::placeholder { color: #999999; font-size: 26px; }`}</style>
|
||||
<input
|
||||
name="signature-name"
|
||||
className={`h-[36px] bg-transparent border-b-2 border-dashed text-center outline-none w-[120px] ${signatureError ? 'border-red-500' : 'border-[#4e402e]'}`}
|
||||
className={`signature-name-input h-[36px] bg-transparent border-b-2 border-dashed text-center outline-none w-[240px] ${signatureError ? 'border-red-500' : 'border-[#4e402e]'}`}
|
||||
style={{ fontSize: '26px' }}
|
||||
autoComplete="off"
|
||||
placeholder="请郑重签下你的姓名"
|
||||
value={signatureName}
|
||||
maxLength={12}
|
||||
onChange={(e) => handleSignatureNameChange(e.target.value)}
|
||||
|
||||
@@ -82,7 +82,7 @@ const LetterToHuangyanpei = ({
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
onAnimationEnd={onAnimationEnd}
|
||||
title="寄给黄炎培的信"
|
||||
title="写给黄炎培先生"
|
||||
letterContent={letterContent}
|
||||
fields={fields}
|
||||
contentClassName="!text-[25px]"
|
||||
|
||||
@@ -63,7 +63,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
||||
correctValue: '横眉冷对',
|
||||
readOnlyFallback: '横眉冷对',
|
||||
hints: {
|
||||
error: '“横眉冷对于夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
error: '“横眉冷对千夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
},
|
||||
},
|
||||
4: {
|
||||
@@ -73,7 +73,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
||||
correctValue: '孺子牛',
|
||||
readOnlyFallback: '孺子牛',
|
||||
hints: {
|
||||
error: '“横眉冷对于夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
error: '“横眉冷对千夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
},
|
||||
},
|
||||
5: {
|
||||
|
||||
@@ -19,7 +19,7 @@ const commonSelectField: LetterFieldConfig = {
|
||||
className: '!w-[120px] !min-w-[120px] shrink-0 px-0',
|
||||
options: [
|
||||
{ value: '天下为公', label: '天下为公' },
|
||||
{ value: '敢为天下先', label: '敢为天下先' },
|
||||
{ value: '与时俱进', label: '与时俱进' },
|
||||
{ value: '振兴中华', label: '振兴中华' },
|
||||
],
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const commonSelectField: LetterFieldConfig = {
|
||||
const letterContent = `敬爱的孙中山先生:
|
||||
您好!
|
||||
|
||||
今天,我怀着崇敬的心情走进香山路 7 号,看着您亲手设计的房屋、勾画的《中国铁路全图》、撰写的《实业计划》手稿,仿佛能看到您为“振兴中华”奔走的身影。在李教授的解读中,我终于明白:您喊出的“____”,不是遥远的历史,而是我们这代人正在接续的接力;您追求的“____”,不是抽象的理念,而是从服务身边人、做好专业事开始的担当;您秉持的“____”,不是过去的选择,而是面对未来时必须有的勇气。
|
||||
今天,我怀着崇敬的心情走进香山路 7 号,看着您亲手设计的房屋、勾画的《中国铁路全图》、撰写的《实业计划》手稿,仿佛能看到您为“振兴中华”奔走的身影。在这场跨越时空的对话中,我终于明白,您喊出的“____”,不是遥远的历史,而是我们这代人正在接续的接力;您追求的“____”,不是抽象的理念,而是从服务身边人、做好专业事开始的担当;您秉持的“____”,不是过去的选择,而是面对未来时必须有的勇气。
|
||||
|
||||
我是____专业的学生,今后,我会以您“知难行易”的信念,在专业学习中攻克难题,就像您当年为了革命理想屡败屡战;我会以您“心系民众”的情怀,参与____,帮身边人解决实际困难;我更会记住您“适乎世界之潮流”的智慧,紧跟时代步伐,努力用____为民族复兴添砖加瓦。
|
||||
|
||||
@@ -52,11 +52,11 @@ const fields: Record<number, LetterFieldConfig> = {
|
||||
},
|
||||
2: {
|
||||
...commonSelectField,
|
||||
correctValue: '敢为天下先',
|
||||
correctValue: '与时俱进',
|
||||
hints: {
|
||||
error: '再想想哦,这里对应的是孙中山先生敢于探索新道路的胆识。',
|
||||
},
|
||||
readOnlyFallback: '敢为天下先',
|
||||
readOnlyFallback: '与时俱进',
|
||||
},
|
||||
3: {
|
||||
type: 'input',
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface NarrationProps {
|
||||
autoPlayAudio?: boolean
|
||||
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
|
||||
audioChannelId?: string
|
||||
/** 署名/出处,主文本动画结束后显示在右下方 */
|
||||
attribution?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,6 +43,7 @@ const Narration = ({
|
||||
animationSpeed = 100,
|
||||
autoPlayAudio = true,
|
||||
audioChannelId,
|
||||
attribution,
|
||||
}: NarrationProps) => {
|
||||
const [displayedText, setDisplayedText] = useState('')
|
||||
const [isAnimating, setIsAnimating] = useState(true)
|
||||
@@ -146,6 +149,7 @@ const Narration = ({
|
||||
|
||||
<div className="text-white text-3xl leading-relaxed space-y-8">
|
||||
<p className="whitespace-pre-wrap">{displayedText}</p>
|
||||
{attribution && !isAnimating && <p className="text-right">{attribution}</p>}
|
||||
</div>
|
||||
|
||||
{/* 关闭按钮 - 只在动画完成后显示 */}
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
import { Image } from '@/components'
|
||||
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||||
|
||||
/**
|
||||
* 六边形进度条组件
|
||||
* 根据进度值显示不同颜色:100%绿色,50%-99%橙色,0%-49%灰色
|
||||
*/
|
||||
interface HexagonProgressBarProps {
|
||||
progress: number // 0-100
|
||||
}
|
||||
|
||||
const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||||
const progressPercent = Math.min(100, Math.max(0, progress))
|
||||
const progressText =
|
||||
progressPercent.toFixed(2) === '100.00' ? '进度100%' : `进度${Math.round(progressPercent)}%`
|
||||
|
||||
// 根据进度值确定填充颜色
|
||||
const getProgressColor = () => {
|
||||
if (progressPercent >= 100) {
|
||||
return '#90B86B' // 浅橄榄绿色(100%完成)
|
||||
} else if (progressPercent >= 50) {
|
||||
return '#EEB362' // 橙色(50%-99%)
|
||||
} else {
|
||||
return '#E0E0E0' // 浅灰色(0%-49%)
|
||||
}
|
||||
}
|
||||
|
||||
const fillColor = getProgressColor()
|
||||
// 六边形路径,细长的水平六边形,左右两端是尖角
|
||||
// 正确的六边形路径:从左顶点开始,顺时针绘制
|
||||
// 左顶点(0%, 50%) -> 左上角(3%, 0%) -> 右上角(97%, 0%) -> 右顶点(100%, 50%) -> 右下角(97%, 100%) -> 左下角(3%, 100%)
|
||||
// 外层路径(包含边框)
|
||||
const outerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||||
// 内层路径(边框内部)
|
||||
const innerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||||
const borderWidth = 4 // 增加边框宽度从2px到3px
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '260px', height: '50px' }}>
|
||||
{/* 外层:白色边框(使用 clip-path 裁剪形状) */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
clipPath: outerHexagonPath,
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
/>
|
||||
{/* 内容容器(边框内部区域) */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${borderWidth}px`,
|
||||
top: `${borderWidth}px`,
|
||||
right: `${borderWidth}px`,
|
||||
bottom: `${borderWidth}px`,
|
||||
clipPath: innerHexagonPath,
|
||||
}}
|
||||
>
|
||||
{/* 背景层:深灰色背景(未填充部分) */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: '#757575',
|
||||
}}
|
||||
/>
|
||||
{/* 填充层(根据进度百分比从左侧填充) */}
|
||||
{progressPercent > 0 && (
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 overflow-hidden"
|
||||
style={{
|
||||
width: `${progressPercent}%`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: fillColor,
|
||||
transition: 'width 0.3s ease, background-color 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 文字(白色,居中,始终显示在最上层,带阴影效果) */}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center text-white text-2xl font-bold pointer-events-none"
|
||||
style={{
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.8)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MenuPuzzleCenterProps {
|
||||
/** 是否可见 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
/** 选择某个故居后的回调(进入对应的碎片系统) */
|
||||
onSelectResidence: (residence: string) => void
|
||||
/** 是否允许从管理中心进入具体拼图 */
|
||||
allowPuzzleEntry?: boolean
|
||||
}
|
||||
|
||||
const MenuPuzzleCenter = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSelectResidence,
|
||||
allowPuzzleEntry = true,
|
||||
}: MenuPuzzleCenterProps) => {
|
||||
const { getPuzzleCollectProgress } = usePuzzleStore()
|
||||
const [activeMapImage, setActiveMapImage] = useState<string | null>(null)
|
||||
|
||||
const residenceProgress = useMemo(
|
||||
() =>
|
||||
PUZZLE_RESIDENCES.map((item) => ({
|
||||
...item,
|
||||
// 只有已开放拼图的故居才计算收集进度
|
||||
progress: item.hasPuzzle ? getPuzzleCollectProgress(item.residence) : 0,
|
||||
})),
|
||||
[getPuzzleCollectProgress]
|
||||
)
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-90 flex items-center justify-center"
|
||||
style={{ background: 'rgba(0,0,0,0.8)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: getBackgroundImage('puzzle/home/底图') }}
|
||||
>
|
||||
{/* 箭头指示底图 */}
|
||||
<img
|
||||
className="absolute top-84 left-55"
|
||||
src={getStaticImage('puzzle/home/箭头指引')}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<Image
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage('components/关闭')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* 故居列表:按照设计图进行绝对定位 */}
|
||||
<div className="absolute inset-0">
|
||||
{residenceProgress.map(
|
||||
({
|
||||
residence,
|
||||
displayName,
|
||||
left,
|
||||
top,
|
||||
image,
|
||||
hasPuzzle,
|
||||
progress,
|
||||
hasMap,
|
||||
mapImage,
|
||||
}) => (
|
||||
<div
|
||||
key={residence}
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2`}
|
||||
style={{ left, top }}
|
||||
>
|
||||
{/* 故居图片(资源在 puzzle/home 下,可根据实际名称调整) */}
|
||||
<Image src={getStaticImage(`puzzle/home/${image}`)} alt={displayName} />
|
||||
|
||||
{/* 进度展示,仅对已开放拼图的故居显示 */}
|
||||
{hasPuzzle && (
|
||||
<div className="mb-4 flex justify-center">
|
||||
<HexagonProgressBar progress={progress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图标 */}
|
||||
<div className="flex justify-center gap-6">
|
||||
{allowPuzzleEntry && (
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/拼图`)}
|
||||
alt="拼图"
|
||||
button
|
||||
onClick={(e) => {
|
||||
if (!hasPuzzle) return
|
||||
e.stopPropagation()
|
||||
onSelectResidence(residence)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/追光入口`)}
|
||||
alt="导图"
|
||||
button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!hasMap || !mapImage) return
|
||||
setActiveMapImage(mapImage)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 导图弹窗 */}
|
||||
{activeMapImage && (
|
||||
<div
|
||||
className="fixed inset-0 z-110 flex items-center justify-center"
|
||||
style={{ background: 'rgba(0,0,0,0.75)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setActiveMapImage(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
className="absolute -right-4 -top-8 z-10"
|
||||
src={getStaticImage('components/close')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={() => setActiveMapImage(null)}
|
||||
/>
|
||||
|
||||
<img src={getStaticImage(`puzzle/home/map/${activeMapImage}`)} alt="导图" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuPuzzleCenter
|
||||
@@ -12,14 +12,14 @@ import { Image } from '@/components'
|
||||
*/
|
||||
const getResidenceDisplayName = (residence: string): string => {
|
||||
const nameMap: Record<string, string> = {
|
||||
chenyun: '陈云',
|
||||
huangyanpei: '黄炎培',
|
||||
luxun: '鲁迅',
|
||||
sunzhongshan: '孙中山',
|
||||
maozedong: '毛泽东',
|
||||
zhangleping: '张乐平',
|
||||
nieer: '聂耳',
|
||||
rongdesheng: '荣德生',
|
||||
chenyun: '陈云故居',
|
||||
huangyanpei: '黄炎培故居',
|
||||
luxun: '鲁迅故居',
|
||||
sunzhongshan: '孙中山故居',
|
||||
maozedong: '毛泽东旧居',
|
||||
zhangleping: '张乐平故居',
|
||||
nieer: '聂耳旧居',
|
||||
rongdesheng: '荣德生旧居',
|
||||
}
|
||||
|
||||
return nameMap[residence] || residence
|
||||
@@ -299,7 +299,7 @@ const PuzzleSystem = ({
|
||||
'[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]'
|
||||
)}
|
||||
>
|
||||
{getResidenceDisplayName(residence)}故居碎片
|
||||
{getResidenceDisplayName(residence)}碎片
|
||||
</span>
|
||||
<img
|
||||
className="absolute -top-10 left-0 -z-10"
|
||||
|
||||
@@ -86,7 +86,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
||||
}
|
||||
|
||||
setContentWidth(calculatedWidth)
|
||||
}, [fitContent, maxWidth, minWidth, options, placeholder])
|
||||
}, [fitContent, maxWidth, minWidth, options, placeholder, fontSize])
|
||||
|
||||
const updateDropdownPosition = useCallback(() => {
|
||||
if (!triggerRef.current) return
|
||||
@@ -240,7 +240,8 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-[36px] items-center justify-center border-b-2 border-dashed bg-transparent align-baseline outline-none',
|
||||
'mx-2 cursor-pointer whitespace-nowrap px-2 text-center text-[#4e402e]',
|
||||
'mx-2 cursor-pointer whitespace-nowrap px-2 text-center',
|
||||
selectedOption ? 'text-[#4e402e]' : 'text-[#999999]',
|
||||
error ? 'border-red-500' : 'border-[#4e402e]',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
className
|
||||
|
||||
@@ -3,6 +3,15 @@ import { PUZZLE_RESIDENCES } from './puzzleResidences'
|
||||
|
||||
export type PuzzleProgress = Record<string, Set<number>>
|
||||
|
||||
const BUILD_UNLOCK_ALL = import.meta.env.VITE_PROGRESS_UNLOCK_ALL === 'true'
|
||||
const UNLOCK_ALL_STORAGE_KEY = 'debug:progression-unlock-all'
|
||||
|
||||
const isUnlockAllEnabled = () => {
|
||||
if (!BUILD_UNLOCK_ALL) return false
|
||||
if (typeof window === 'undefined') return true
|
||||
return window.sessionStorage.getItem(UNLOCK_ALL_STORAGE_KEY) !== 'false'
|
||||
}
|
||||
|
||||
const getResidenceIndex = (residence: string) =>
|
||||
PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence)
|
||||
|
||||
@@ -24,10 +33,13 @@ export const isResidenceStarted = (residence: string, puzzles: PuzzleProgress) =
|
||||
}
|
||||
|
||||
export const isResidenceComplete = (residence: string, puzzles: PuzzleProgress) => {
|
||||
if (isUnlockAllEnabled()) return true
|
||||
return arePiecesUnlocked(residence, puzzles, getConfiguredPieceIds(residence))
|
||||
}
|
||||
|
||||
export const isResidenceUnlocked = (residence: string, puzzles: PuzzleProgress) => {
|
||||
if (isUnlockAllEnabled()) return true
|
||||
|
||||
const index = getResidenceIndex(residence)
|
||||
if (index === 0) return true
|
||||
if (index < 0) return false
|
||||
@@ -50,6 +62,8 @@ export const isMenuModuleStarted = (moduleName: string, puzzles: PuzzleProgress)
|
||||
}
|
||||
|
||||
export const isMenuModuleComplete = (moduleName: string, puzzles: PuzzleProgress) => {
|
||||
if (isUnlockAllEnabled()) return true
|
||||
|
||||
const residences = PUZZLE_RESIDENCES.filter((item) => item.moduleName === moduleName)
|
||||
if (residences.length === 0) return false
|
||||
return residences.every((item) => isResidenceComplete(item.residence, puzzles))
|
||||
@@ -65,6 +79,14 @@ const getPieceIdsByRoute = (residence: string, routePrefix: string) => {
|
||||
}
|
||||
|
||||
export const getResidenceStageUnlocks = (residence: string, puzzles: PuzzleProgress) => {
|
||||
if (isUnlockAllEnabled()) {
|
||||
return {
|
||||
dialogueUnlocked: true,
|
||||
reflectionUnlocked: true,
|
||||
internalizationUnlocked: true,
|
||||
}
|
||||
}
|
||||
|
||||
const residenceUnlocked = isResidenceUnlocked(residence, puzzles)
|
||||
|
||||
return {
|
||||
|
||||
@@ -80,7 +80,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
},
|
||||
// 碎片11
|
||||
11: {
|
||||
title: '陈云起草的关于发展国民 经济"一五"计划报告的手稿',
|
||||
title: '陈云起草的关于发展国民 经济“一五”计划报告的手稿',
|
||||
detailImage: 'chenyun/details/11',
|
||||
route: 'place8',
|
||||
},
|
||||
@@ -148,7 +148,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
},
|
||||
// 碎片6
|
||||
6: {
|
||||
title: '中华职业教育 和中华职业学校',
|
||||
title: '职业教育 和中华职业学校',
|
||||
detailImage: 'huangyanpei/details/6',
|
||||
route: 'place4',
|
||||
},
|
||||
@@ -184,7 +184,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
},
|
||||
// 碎片12
|
||||
12: {
|
||||
title: '窑洞对话',
|
||||
title: '窑洞对',
|
||||
detailImage: 'huangyanpei/details/12',
|
||||
route: 'place5',
|
||||
},
|
||||
@@ -196,7 +196,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
},
|
||||
// 碎片14
|
||||
14: {
|
||||
title: '黄炎培对浦东中学新生的教导',
|
||||
title: '黄炎培对浦东中学 新生的教导',
|
||||
detailImage: 'huangyanpei/details/14',
|
||||
route: 'reflection2',
|
||||
},
|
||||
@@ -260,7 +260,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'place3',
|
||||
},
|
||||
10: {
|
||||
title: '《中国国民党第一次 全国代表大会宣言》',
|
||||
title: '中国国民党第一次 全国代表大会宣言',
|
||||
detailImage: 'sunzhongshan/details/10',
|
||||
route: 'place3',
|
||||
},
|
||||
@@ -399,22 +399,22 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'place2',
|
||||
},
|
||||
5: {
|
||||
title: '中共三大会议召开',
|
||||
title: '中共三大召开',
|
||||
detailImage: 'maozedong/details/5',
|
||||
route: 'place3',
|
||||
},
|
||||
6: {
|
||||
title: '《孙中山手书名单》',
|
||||
title: '孙中山手书名单',
|
||||
detailImage: 'maozedong/details/6',
|
||||
route: 'place3',
|
||||
},
|
||||
7: {
|
||||
title: '《孙文越飞联合宣言》',
|
||||
title: '孙文越飞联合宣言',
|
||||
detailImage: 'maozedong/details/7',
|
||||
route: 'place3',
|
||||
},
|
||||
8: {
|
||||
title: '毛泽东签发的两份中共四大通知',
|
||||
title: '毛泽东签发的两份 中共四大通知',
|
||||
detailImage: 'maozedong/details/8',
|
||||
route: 'place4',
|
||||
},
|
||||
@@ -424,7 +424,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'place4',
|
||||
},
|
||||
10: {
|
||||
title: '毛泽东中国社会分析',
|
||||
title: '中国社会各阶级的分析',
|
||||
detailImage: 'maozedong/details/10',
|
||||
route: 'place4',
|
||||
},
|
||||
@@ -501,34 +501,34 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'place4',
|
||||
},
|
||||
9: {
|
||||
title: '刘良模指挥数千人齐唱',
|
||||
title: '国歌选定',
|
||||
detailImage: 'nieer/details/9',
|
||||
route: 'place4',
|
||||
route: 'place5',
|
||||
},
|
||||
10: {
|
||||
title: '国歌选定',
|
||||
title: '国歌入宪',
|
||||
detailImage: 'nieer/details/10',
|
||||
route: 'place5',
|
||||
},
|
||||
11: {
|
||||
title: '国歌入宪',
|
||||
title: '国歌立法',
|
||||
detailImage: 'nieer/details/11',
|
||||
route: 'place5',
|
||||
},
|
||||
12: {
|
||||
title: '国歌立法',
|
||||
title: '新中国奏响国歌的场景(上)',
|
||||
detailImage: 'nieer/details/12',
|
||||
route: 'place5',
|
||||
route: 'place6',
|
||||
},
|
||||
13: {
|
||||
title: '新中国奏响国歌的场景(上)',
|
||||
title: '新中国奏响国歌的场景(下)',
|
||||
detailImage: 'nieer/details/13',
|
||||
route: 'place6',
|
||||
},
|
||||
14: {
|
||||
title: '新中国奏响国歌的场景(下)',
|
||||
title: '唱国家一定要站起来',
|
||||
detailImage: 'nieer/details/14',
|
||||
route: 'place6',
|
||||
route: 'reflection',
|
||||
},
|
||||
15: {
|
||||
title: '聂耳「人民音乐家」',
|
||||
@@ -578,7 +578,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'place3',
|
||||
},
|
||||
8: {
|
||||
title: '三毛新生(《向日葵》张乐平,1950-1965)',
|
||||
title: '三毛新生',
|
||||
detailImage: 'zhangleping/details/8',
|
||||
route: 'place3',
|
||||
},
|
||||
@@ -608,17 +608,17 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'place5',
|
||||
},
|
||||
14: {
|
||||
title: '画自己',
|
||||
title: '创作心路',
|
||||
detailImage: 'zhangleping/details/14',
|
||||
route: 'place5',
|
||||
route: 'reflection',
|
||||
},
|
||||
15: {
|
||||
title: '平民立场',
|
||||
title: '艺术来源于生活',
|
||||
detailImage: 'zhangleping/details/15',
|
||||
route: 'internalization',
|
||||
},
|
||||
16: {
|
||||
title: '笔耕不辍',
|
||||
title: '核心创作理念',
|
||||
detailImage: 'zhangleping/details/16',
|
||||
route: 'internalization',
|
||||
},
|
||||
@@ -705,8 +705,6 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
||||
route: 'internalization',
|
||||
},
|
||||
},
|
||||
// 后续可以添加其他故居的数据
|
||||
// sunzhongshan: { ... },
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const STORAGE_KEY = 'debug:progression-unlock-all'
|
||||
const BADGE_ID = 'progression-debug-badge'
|
||||
|
||||
const isUnlockAllEnabled = () => {
|
||||
return window.sessionStorage.getItem(STORAGE_KEY) !== 'false'
|
||||
}
|
||||
|
||||
const getBadgeText = () => {
|
||||
return isUnlockAllEnabled() ? '测试包:全解锁' : '测试包:真实进度'
|
||||
}
|
||||
|
||||
const updateBadge = () => {
|
||||
let badge = document.getElementById(BADGE_ID)
|
||||
if (!badge) {
|
||||
badge = document.createElement('div')
|
||||
badge.id = BADGE_ID
|
||||
badge.style.position = 'fixed'
|
||||
badge.style.right = '12px'
|
||||
badge.style.top = '12px'
|
||||
badge.style.zIndex = '99999'
|
||||
badge.style.padding = '8px 10px'
|
||||
badge.style.borderRadius = '8px'
|
||||
badge.style.background = 'rgba(0, 0, 0, 0.72)'
|
||||
badge.style.color = '#fff'
|
||||
badge.style.fontSize = '12px'
|
||||
badge.style.lineHeight = '1.5'
|
||||
badge.style.pointerEvents = 'none'
|
||||
badge.style.whiteSpace = 'pre-line'
|
||||
document.body.appendChild(badge)
|
||||
}
|
||||
|
||||
badge.textContent = `${getBadgeText()}\nCtrl+Shift+U 切换`
|
||||
}
|
||||
|
||||
const toggleUnlockAll = () => {
|
||||
const nextValue = isUnlockAllEnabled() ? 'false' : 'true'
|
||||
window.sessionStorage.setItem(STORAGE_KEY, nextValue)
|
||||
updateBadge()
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
export const setupProgressionDebug = () => {
|
||||
updateBadge()
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (!event.ctrlKey || !event.shiftKey || event.key.toLowerCase() !== 'u') return
|
||||
|
||||
event.preventDefault()
|
||||
toggleUnlockAll()
|
||||
})
|
||||
}
|
||||
@@ -17,16 +17,16 @@
|
||||
### 1. 简单点击
|
||||
|
||||
```tsx
|
||||
import { useTouch } from "@/hooks";
|
||||
import { useTouch } from '@/hooks'
|
||||
|
||||
function MyComponent() {
|
||||
const { handlers } = useTouch({
|
||||
onTap: () => {
|
||||
console.log("点击了");
|
||||
console.log('点击了')
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return <div {...handlers}>点击我</div>;
|
||||
return <div {...handlers}>点击我</div>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -34,13 +34,13 @@ function MyComponent() {
|
||||
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onSwipeLeft: () => console.log("向左滑动"),
|
||||
onSwipeRight: () => console.log("向右滑动"),
|
||||
onSwipeUp: () => console.log("向上滑动"),
|
||||
onSwipeDown: () => console.log("向下滑动"),
|
||||
});
|
||||
onSwipeLeft: () => console.log('向左滑动'),
|
||||
onSwipeRight: () => console.log('向右滑动'),
|
||||
onSwipeUp: () => console.log('向上滑动'),
|
||||
onSwipeDown: () => console.log('向下滑动'),
|
||||
})
|
||||
|
||||
return <div {...handlers}>滑动区域</div>;
|
||||
return <div {...handlers}>滑动区域</div>
|
||||
```
|
||||
|
||||
### 3. 双击
|
||||
@@ -48,11 +48,11 @@ return <div {...handlers}>滑动区域</div>;
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onDoubleTap: () => {
|
||||
console.log("双击触发");
|
||||
console.log('双击触发')
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return <div {...handlers}>双击我</div>;
|
||||
return <div {...handlers}>双击我</div>
|
||||
```
|
||||
|
||||
### 4. 长按
|
||||
@@ -60,12 +60,12 @@ return <div {...handlers}>双击我</div>;
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onLongPress: () => {
|
||||
console.log("长按触发");
|
||||
console.log('长按触发')
|
||||
},
|
||||
longPressDelay: 500, // 500ms 触发长按
|
||||
});
|
||||
})
|
||||
|
||||
return <div {...handlers}>长按我</div>;
|
||||
return <div {...handlers}>长按我</div>
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
@@ -75,11 +75,11 @@ return <div {...handlers}>长按我</div>;
|
||||
```tsx
|
||||
const { handlers, touchData, isTouching } = useTouch({
|
||||
onTouchMove: (data) => {
|
||||
console.log("移动距离:", data.deltaX, data.deltaY);
|
||||
console.log("移动速度:", data.velocity);
|
||||
console.log("移动方向:", data.direction);
|
||||
console.log('移动距离:', data.deltaX, data.deltaY)
|
||||
console.log('移动速度:', data.velocity)
|
||||
console.log('移动方向:', data.direction)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div {...handlers}>
|
||||
@@ -89,20 +89,20 @@ return (
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
```
|
||||
|
||||
### 图片查看器示例
|
||||
|
||||
```tsx
|
||||
function ImageViewer() {
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [scale, setScale] = useState(1)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onDoubleTap: () => {
|
||||
// 双击放大/还原
|
||||
setScale(scale === 1 ? 2 : 1);
|
||||
setScale(scale === 1 ? 2 : 1)
|
||||
},
|
||||
onSwipe: (data) => {
|
||||
// 滑动移动图片
|
||||
@@ -110,23 +110,23 @@ function ImageViewer() {
|
||||
setPosition((prev) => ({
|
||||
x: prev.x + data.deltaX,
|
||||
y: prev.y + data.deltaY,
|
||||
}));
|
||||
}))
|
||||
}
|
||||
},
|
||||
swipeThreshold: 5,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div {...handlers} style={{ overflow: "hidden" }}>
|
||||
<div {...handlers} style={{ overflow: 'hidden' }}>
|
||||
<img
|
||||
src="image.jpg"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||
transition: "transform 0.3s",
|
||||
transition: 'transform 0.3s',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -134,36 +134,36 @@ function ImageViewer() {
|
||||
|
||||
```tsx
|
||||
function SwipeableCard() {
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [offset, setOffset] = useState(0)
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onTouchMove: (data) => {
|
||||
if (data.direction === "left" || data.direction === "right") {
|
||||
setOffset(data.deltaX);
|
||||
if (data.direction === 'left' || data.direction === 'right') {
|
||||
setOffset(data.deltaX)
|
||||
}
|
||||
},
|
||||
onTouchEnd: (data) => {
|
||||
if (Math.abs(data.deltaX) > 100) {
|
||||
// 滑动超过 100px,执行删除
|
||||
onDelete();
|
||||
onDelete()
|
||||
} else {
|
||||
// 弹回原位
|
||||
setOffset(0);
|
||||
setOffset(0)
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
{...handlers}
|
||||
style={{
|
||||
transform: `translateX(${offset}px)`,
|
||||
transition: offset === 0 ? "transform 0.3s" : "none",
|
||||
transition: offset === 0 ? 'transform 0.3s' : 'none',
|
||||
}}
|
||||
>
|
||||
<div>卡片内容</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -171,42 +171,38 @@ function SwipeableCard() {
|
||||
|
||||
```tsx
|
||||
function PullToRefresh() {
|
||||
const [pullDistance, setPullDistance] = useState(0);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [pullDistance, setPullDistance] = useState(0)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onTouchMove: (data) => {
|
||||
// 只在向下滑动且在顶部时触发
|
||||
if (data.direction === "down" && window.scrollY === 0) {
|
||||
setPullDistance(Math.min(data.deltaY, 80));
|
||||
if (data.direction === 'down' && window.scrollY === 0) {
|
||||
setPullDistance(Math.min(data.deltaY, 80))
|
||||
}
|
||||
},
|
||||
onTouchEnd: (data) => {
|
||||
if (pullDistance > 50) {
|
||||
setIsRefreshing(true);
|
||||
setIsRefreshing(true)
|
||||
// 执行刷新逻辑
|
||||
refresh().then(() => {
|
||||
setIsRefreshing(false);
|
||||
setPullDistance(0);
|
||||
});
|
||||
setIsRefreshing(false)
|
||||
setPullDistance(0)
|
||||
})
|
||||
} else {
|
||||
setPullDistance(0);
|
||||
setPullDistance(0)
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div {...handlers}>
|
||||
<div style={{ height: pullDistance, transition: "height 0.3s" }}>
|
||||
{isRefreshing
|
||||
? "刷新中..."
|
||||
: pullDistance > 50
|
||||
? "松开刷新"
|
||||
: "下拉刷新"}
|
||||
<div style={{ height: pullDistance, transition: 'height 0.3s' }}>
|
||||
{isRefreshing ? '刷新中...' : pullDistance > 50 ? '松开刷新' : '下拉刷新'}
|
||||
</div>
|
||||
<div>内容区域</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -214,34 +210,34 @@ function PullToRefresh() {
|
||||
|
||||
```tsx
|
||||
function Carousel({ images }: { images: string[] }) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onSwipeLeft: () => {
|
||||
setCurrentIndex((prev) => Math.min(prev + 1, images.length - 1));
|
||||
setCurrentIndex((prev) => Math.min(prev + 1, images.length - 1))
|
||||
},
|
||||
onSwipeRight: () => {
|
||||
setCurrentIndex((prev) => Math.max(prev - 1, 0));
|
||||
setCurrentIndex((prev) => Math.max(prev - 1, 0))
|
||||
},
|
||||
swipeThreshold: 50,
|
||||
swipeVelocityThreshold: 0.3,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div {...handlers} style={{ overflow: "hidden" }}>
|
||||
<div {...handlers} style={{ overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
display: 'flex',
|
||||
transform: `translateX(-${currentIndex * 100}%)`,
|
||||
transition: "transform 0.3s",
|
||||
transition: 'transform 0.3s',
|
||||
}}
|
||||
>
|
||||
{images.map((img, i) => (
|
||||
<img key={i} src={img} style={{ width: "100%", flexShrink: 0 }} />
|
||||
<img key={i} src={img} style={{ width: '100%', flexShrink: 0 }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -250,34 +246,34 @@ function Carousel({ images }: { images: string[] }) {
|
||||
```typescript
|
||||
interface UseTouchOptions {
|
||||
// 基础配置
|
||||
enabled?: boolean; // 是否启用,默认 true
|
||||
preventDefault?: boolean; // 是否阻止默认行为,默认 false
|
||||
stopPropagation?: boolean; // 是否阻止事件冒泡,默认 false
|
||||
enabled?: boolean // 是否启用,默认 true
|
||||
preventDefault?: boolean // 是否阻止默认行为,默认 false
|
||||
stopPropagation?: boolean // 是否阻止事件冒泡,默认 false
|
||||
|
||||
// 滑动配置
|
||||
swipeThreshold?: number; // 滑动阈值(px),默认 10
|
||||
swipeVelocityThreshold?: number; // 滑动速度阈值(px/ms),默认 0.5
|
||||
swipeThreshold?: number // 滑动阈值(px),默认 10
|
||||
swipeVelocityThreshold?: number // 滑动速度阈值(px/ms),默认 0.5
|
||||
|
||||
// 长按配置
|
||||
longPressDelay?: number; // 长按延迟(ms),默认 500
|
||||
longPressMoveThreshold?: number; // 长按允许移动距离(px),默认 10
|
||||
longPressDelay?: number // 长按延迟(ms),默认 500
|
||||
longPressMoveThreshold?: number // 长按允许移动距离(px),默认 10
|
||||
|
||||
// 双击配置
|
||||
doubleTapDelay?: number; // 双击间隔(ms),默认 300
|
||||
doubleTapMoveThreshold?: number; // 双击允许移动距离(px),默认 10
|
||||
doubleTapDelay?: number // 双击间隔(ms),默认 300
|
||||
doubleTapMoveThreshold?: number // 双击允许移动距离(px),默认 10
|
||||
|
||||
// 事件回调
|
||||
onTouchStart?: (data: TouchEventData) => void;
|
||||
onTouchMove?: (data: TouchEventData) => void;
|
||||
onTouchEnd?: (data: TouchEventData) => void;
|
||||
onTap?: (data: TouchEventData) => void;
|
||||
onDoubleTap?: (data: TouchEventData) => void;
|
||||
onLongPress?: (data: TouchEventData) => void;
|
||||
onSwipe?: (data: TouchEventData) => void;
|
||||
onSwipeLeft?: (data: TouchEventData) => void;
|
||||
onSwipeRight?: (data: TouchEventData) => void;
|
||||
onSwipeUp?: (data: TouchEventData) => void;
|
||||
onSwipeDown?: (data: TouchEventData) => void;
|
||||
onTouchStart?: (data: TouchEventData) => void
|
||||
onTouchMove?: (data: TouchEventData) => void
|
||||
onTouchEnd?: (data: TouchEventData) => void
|
||||
onTap?: (data: TouchEventData) => void
|
||||
onDoubleTap?: (data: TouchEventData) => void
|
||||
onLongPress?: (data: TouchEventData) => void
|
||||
onSwipe?: (data: TouchEventData) => void
|
||||
onSwipeLeft?: (data: TouchEventData) => void
|
||||
onSwipeRight?: (data: TouchEventData) => void
|
||||
onSwipeUp?: (data: TouchEventData) => void
|
||||
onSwipeDown?: (data: TouchEventData) => void
|
||||
}
|
||||
```
|
||||
|
||||
@@ -287,14 +283,14 @@ interface UseTouchOptions {
|
||||
|
||||
```typescript
|
||||
interface TouchEventData {
|
||||
startPoint: TouchPoint; // 起始位置 { x, y, timestamp }
|
||||
currentPoint: TouchPoint; // 当前位置 { x, y, timestamp }
|
||||
deltaX: number; // X 轴移动距离
|
||||
deltaY: number; // Y 轴移动距离
|
||||
distance: number; // 移动的总距离
|
||||
velocity: number; // 移动速度(像素/毫秒)
|
||||
direction: SwipeDirection; // 移动方向: 'left' | 'right' | 'up' | 'down' | null
|
||||
event: React.TouchEvent | React.MouseEvent; // 原始事件
|
||||
startPoint: TouchPoint // 起始位置 { x, y, timestamp }
|
||||
currentPoint: TouchPoint // 当前位置 { x, y, timestamp }
|
||||
deltaX: number // X 轴移动距离
|
||||
deltaY: number // Y 轴移动距离
|
||||
distance: number // 移动的总距离
|
||||
velocity: number // 移动速度(像素/毫秒)
|
||||
direction: SwipeDirection // 移动方向: 'left' | 'right' | 'up' | 'down' | null
|
||||
event: React.TouchEvent | React.MouseEvent // 原始事件
|
||||
}
|
||||
```
|
||||
|
||||
@@ -302,14 +298,14 @@ interface TouchEventData {
|
||||
|
||||
```typescript
|
||||
interface UseTouchReturn {
|
||||
isTouching: boolean; // 是否正在触摸
|
||||
touchData: TouchEventData | null; // 当前触摸数据
|
||||
isTouching: boolean // 是否正在触摸
|
||||
touchData: TouchEventData | null // 当前触摸数据
|
||||
handlers: {
|
||||
onTouchStart: (e: React.TouchEvent) => void;
|
||||
onTouchMove: (e: React.TouchEvent) => void;
|
||||
onTouchEnd: (e: React.TouchEvent) => void;
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
};
|
||||
onTouchStart: (e: React.TouchEvent) => void
|
||||
onTouchMove: (e: React.TouchEvent) => void
|
||||
onTouchEnd: (e: React.TouchEvent) => void
|
||||
onMouseDown: (e: React.MouseEvent) => void
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -329,7 +325,7 @@ interface UseTouchReturn {
|
||||
const { handlers } = useTouch({
|
||||
onSwipeLeft: handleSwipe,
|
||||
preventDefault: false, // 不阻止默认行为,允许页面滚动
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 性能优化
|
||||
@@ -342,9 +338,9 @@ const touchOptions = useMemo(
|
||||
swipeThreshold: 50,
|
||||
}),
|
||||
[handleSwipe]
|
||||
);
|
||||
)
|
||||
|
||||
const { handlers } = useTouch(touchOptions);
|
||||
const { handlers } = useTouch(touchOptions)
|
||||
```
|
||||
|
||||
### 3. 条件禁用
|
||||
@@ -353,31 +349,31 @@ const { handlers } = useTouch(touchOptions);
|
||||
const { handlers } = useTouch({
|
||||
enabled: !isLoading, // 加载时禁用触摸
|
||||
onTap: handleTap,
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```tsx
|
||||
import { useTouch } from "@/hooks";
|
||||
import { useState } from "react";
|
||||
import { useTouch } from '@/hooks'
|
||||
import { useState } from 'react'
|
||||
|
||||
function TouchDemo() {
|
||||
const [log, setLog] = useState<string[]>([]);
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
|
||||
const addLog = (message: string) => {
|
||||
setLog((prev) => [...prev, message].slice(-5));
|
||||
};
|
||||
setLog((prev) => [...prev, message].slice(-5))
|
||||
}
|
||||
|
||||
const { handlers, isTouching, touchData } = useTouch({
|
||||
onTap: () => addLog("点击"),
|
||||
onDoubleTap: () => addLog("双击"),
|
||||
onLongPress: () => addLog("长按"),
|
||||
onSwipeLeft: () => addLog("向左滑动"),
|
||||
onSwipeRight: () => addLog("向右滑动"),
|
||||
onSwipeUp: () => addLog("向上滑动"),
|
||||
onSwipeDown: () => addLog("向下滑动"),
|
||||
});
|
||||
onTap: () => addLog('点击'),
|
||||
onDoubleTap: () => addLog('双击'),
|
||||
onLongPress: () => addLog('长按'),
|
||||
onSwipeLeft: () => addLog('向左滑动'),
|
||||
onSwipeRight: () => addLog('向右滑动'),
|
||||
onSwipeUp: () => addLog('向上滑动'),
|
||||
onSwipeDown: () => addLog('向下滑动'),
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -386,12 +382,12 @@ function TouchDemo() {
|
||||
style={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
border: "2px solid #ccc",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
userSelect: "none",
|
||||
background: isTouching ? "#f0f0f0" : "white",
|
||||
border: '2px solid #ccc',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
userSelect: 'none',
|
||||
background: isTouching ? '#f0f0f0' : 'white',
|
||||
}}
|
||||
>
|
||||
触摸测试区域
|
||||
@@ -410,7 +406,7 @@ function TouchDemo() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,244 +1,238 @@
|
||||
@import "tailwindcss";
|
||||
@import "./styles/input.css";
|
||||
@import 'tailwindcss';
|
||||
@import './styles/input.css';
|
||||
|
||||
/* 定义字体 */
|
||||
@font-face {
|
||||
font-family: "SourceHanSerifCN";
|
||||
src: url("/fonts/SourceHanSerifCN-Bold-2.otf") format("opentype");
|
||||
font-family: 'SourceHanSerifCN';
|
||||
src: url('/fonts/SourceHanSerifCN-Bold-2.otf') format('opentype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* *{ */
|
||||
/* margin: 0;
|
||||
/* margin: 0;
|
||||
padding: 0; */
|
||||
/* box-sizing: border-box; */
|
||||
/* box-sizing: border-box; */
|
||||
/* } */
|
||||
|
||||
body{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
font-family: "SourceHanSerifCN", serif;
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
font-family: 'SourceHanSerifCN', serif;
|
||||
}
|
||||
|
||||
#root{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button:not([disabled]){
|
||||
cursor: pointer;
|
||||
button:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img, video{
|
||||
max-width: unset;
|
||||
img,
|
||||
video {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(139, 115, 85, 0.6) transparent;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(139, 115, 85, 0.6) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(to bottom, rgba(139, 115, 85, 0.8), rgba(107, 93, 71, 0.8));
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
transition: background 0.3s ease;
|
||||
background: linear-gradient(to bottom, rgba(139, 115, 85, 0.8), rgba(107, 93, 71, 0.8));
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(to bottom, rgba(139, 115, 85, 1), rgba(107, 93, 71, 1));
|
||||
box-shadow: 0 2px 8px rgba(139, 115, 85, 0.4);
|
||||
background: linear-gradient(to bottom, rgba(139, 115, 85, 1), rgba(107, 93, 71, 1));
|
||||
box-shadow: 0 2px 8px rgba(139, 115, 85, 0.4);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background: linear-gradient(to bottom, rgba(107, 93, 71, 1), rgba(91, 79, 61, 1));
|
||||
background: linear-gradient(to bottom, rgba(107, 93, 71, 1), rgba(91, 79, 61, 1));
|
||||
}
|
||||
|
||||
/* 多选题面板滚动条样式 - 金色主题 */
|
||||
.quiz-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(212, 197, 169, 0.7) transparent;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(212, 197, 169, 0.7) transparent;
|
||||
}
|
||||
|
||||
.quiz-scrollbar::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.quiz-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
margin: 8px 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.quiz-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(212, 197, 169, 0.8),
|
||||
rgba(240, 190, 117, 0.8));
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
background: linear-gradient(to bottom, rgba(212, 197, 169, 0.8), rgba(240, 190, 117, 0.8));
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quiz-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(240, 190, 117, 0.9),
|
||||
rgba(212, 197, 169, 0.9));
|
||||
box-shadow: 0 2px 8px rgba(240, 190, 117, 0.5);
|
||||
border-color: rgba(240, 190, 117, 0.3);
|
||||
background: linear-gradient(to bottom, rgba(240, 190, 117, 0.9), rgba(212, 197, 169, 0.9));
|
||||
box-shadow: 0 2px 8px rgba(240, 190, 117, 0.5);
|
||||
border-color: rgba(240, 190, 117, 0.3);
|
||||
}
|
||||
|
||||
.quiz-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(240, 190, 117, 1),
|
||||
rgba(212, 197, 169, 1));
|
||||
background: linear-gradient(to bottom, rgba(240, 190, 117, 1), rgba(212, 197, 169, 1));
|
||||
}
|
||||
|
||||
/* 搜索图标动画 */
|
||||
@keyframes search-pulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.1) rotate(5deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.1) rotate(5deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-search {
|
||||
animation: search-pulse 2s ease-in-out infinite;
|
||||
animation: search-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 三个点加载动画 */
|
||||
@keyframes dot-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-dots > span {
|
||||
animation: dot-bounce 1.4s ease-in-out infinite both;
|
||||
animation: dot-bounce 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.animate-dots > span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.animate-dots > span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.animate-dots > span:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
/* 页面过渡动画 - 确保初始状态为透明,避免闪烁 */
|
||||
.page-transition-wrapper > div[data-framer-name] {
|
||||
opacity: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 鲁迅页面滚动条样式 - 古典文化风格 */
|
||||
.luxun-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(212, 175, 55, 0.85) rgba(250, 245, 235, 0.08);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(212, 175, 55, 0.85) rgba(250, 245, 235, 0.08);
|
||||
}
|
||||
|
||||
.luxun-scrollbar::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.luxun-scrollbar::-webkit-scrollbar-track {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(250, 245, 235, 0.06),
|
||||
rgba(240, 235, 225, 0.04)
|
||||
);
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid rgba(139, 115, 71, 0.2);
|
||||
margin: 6px 2px;
|
||||
box-shadow:
|
||||
inset 0 0 8px rgba(139, 115, 71, 0.08),
|
||||
inset 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
background: linear-gradient(to bottom, rgba(250, 245, 235, 0.06), rgba(240, 235, 225, 0.04));
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid rgba(139, 115, 71, 0.2);
|
||||
margin: 6px 2px;
|
||||
box-shadow:
|
||||
inset 0 0 8px rgba(139, 115, 71, 0.08),
|
||||
inset 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.luxun-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(240, 190, 117, 0.95),
|
||||
rgba(212, 175, 55, 0.95),
|
||||
rgba(184, 134, 11, 0.95)
|
||||
);
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(250, 245, 235, 0.4);
|
||||
box-shadow:
|
||||
inset 0 1px 3px rgba(255, 255, 255, 0.25),
|
||||
inset 0 -1px 2px rgba(139, 115, 71, 0.2),
|
||||
0 2px 6px rgba(139, 115, 71, 0.25),
|
||||
0 0 10px rgba(212, 175, 55, 0.15);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(240, 190, 117, 0.95),
|
||||
rgba(212, 175, 55, 0.95),
|
||||
rgba(184, 134, 11, 0.95)
|
||||
);
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(250, 245, 235, 0.4);
|
||||
box-shadow:
|
||||
inset 0 1px 3px rgba(255, 255, 255, 0.25),
|
||||
inset 0 -1px 2px rgba(139, 115, 71, 0.2),
|
||||
0 2px 6px rgba(139, 115, 71, 0.25),
|
||||
0 0 10px rgba(212, 175, 55, 0.15);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 滚动条滑块内部装饰线效果 */
|
||||
.luxun-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 215, 140, 1),
|
||||
rgba(240, 190, 117, 1),
|
||||
rgba(212, 175, 55, 1)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 1px 4px rgba(255, 255, 255, 0.35),
|
||||
inset 0 -1px 3px rgba(139, 115, 71, 0.25),
|
||||
0 3px 8px rgba(139, 115, 71, 0.35),
|
||||
0 0 14px rgba(212, 175, 55, 0.3);
|
||||
border-color: rgba(250, 245, 235, 0.6);
|
||||
transform: scaleX(1.05);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 215, 140, 1),
|
||||
rgba(240, 190, 117, 1),
|
||||
rgba(212, 175, 55, 1)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 1px 4px rgba(255, 255, 255, 0.35),
|
||||
inset 0 -1px 3px rgba(139, 115, 71, 0.25),
|
||||
0 3px 8px rgba(139, 115, 71, 0.35),
|
||||
0 0 14px rgba(212, 175, 55, 0.3);
|
||||
border-color: rgba(250, 245, 235, 0.6);
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
|
||||
.luxun-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(184, 134, 11, 1),
|
||||
rgba(139, 115, 71, 1),
|
||||
rgba(107, 93, 71, 1)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 2px 5px rgba(0, 0, 0, 0.4),
|
||||
inset 0 -1px 2px rgba(139, 115, 71, 0.3),
|
||||
0 1px 3px rgba(139, 115, 71, 0.3);
|
||||
border-color: rgba(250, 245, 235, 0.2);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(184, 134, 11, 1),
|
||||
rgba(139, 115, 71, 1),
|
||||
rgba(107, 93, 71, 1)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 2px 5px rgba(0, 0, 0, 0.4),
|
||||
inset 0 -1px 2px rgba(139, 115, 71, 0.3),
|
||||
0 1px 3px rgba(139, 115, 71, 0.3);
|
||||
border-color: rgba(250, 245, 235, 0.2);
|
||||
}
|
||||
|
||||
/* 点一点图标动画 - 手指点击动画(上下移动模拟点击) */
|
||||
@keyframes finger-bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) translateX(5px);
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-finger-bounce {
|
||||
animation: finger-bounce 1.2s ease-in-out infinite;
|
||||
animation: finger-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ import './index.css'
|
||||
import App from './App'
|
||||
import { useChenyunStore, useHuangyanpeiStore, usePuzzleStore, useSunzhongshanStore } from './store'
|
||||
|
||||
if (import.meta.env.VITE_DEBUG_ENTRY === 'true' && typeof window !== 'undefined') {
|
||||
import('@/debug/progressionDebug').then(({ setupProgressionDebug }) => {
|
||||
setupProgressionDebug()
|
||||
})
|
||||
}
|
||||
|
||||
// 在开发模式的移动端启用 vConsole(需先安装 vconsole 依赖)
|
||||
if (import.meta.env.DEV && typeof window !== 'undefined') {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
|
||||
@@ -2,14 +2,12 @@ import { useState } from 'react'
|
||||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||||
import { Image, PuzzleSystem } from '@/components'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import MenuPuzzleCenter from '@/components/puzzle/MenuPuzzleCenter'
|
||||
import { DEFAULT_PUZZLE_RESIDENCE } from '@/config/puzzleResidences'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
import { isMenuModuleComplete } from '@/config/progression'
|
||||
|
||||
const Menu = () => {
|
||||
const navigate = useNavigate()
|
||||
const [isPuzzleCenterVisible, setIsPuzzleCenterVisible] = useState(false)
|
||||
const [activeResidence, setActiveResidence] = useState<string | null>(null)
|
||||
const puzzles = usePuzzleStore((state) => state.puzzles)
|
||||
const isLoaded = usePuzzleStore((state) => state.isLoaded)
|
||||
@@ -65,17 +63,6 @@ const Menu = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 顶层碎片管理中心:查看故居导图与进度 */}
|
||||
<MenuPuzzleCenter
|
||||
visible={isPuzzleCenterVisible}
|
||||
onClose={() => setIsPuzzleCenterVisible(false)}
|
||||
onSelectResidence={(residence) => {
|
||||
setActiveResidence(residence)
|
||||
setIsPuzzleCenterVisible(false)
|
||||
}}
|
||||
allowPuzzleEntry={false}
|
||||
/>
|
||||
|
||||
{/* 进入具体故居的拼图系统(与故居页面一致的 PuzzleSystem) */}
|
||||
{activeResidence && (
|
||||
<PuzzleSystem
|
||||
@@ -102,7 +89,7 @@ const Menu = () => {
|
||||
className="relative w-[241px] h-[282px] bg-cover bg-center bg-no-repeat hover:brightness-110 hover:scale-105 transition-all cursor-pointer"
|
||||
style={{ backgroundImage: getBackgroundImage('menu/模块-通用') }}
|
||||
key={item.id}
|
||||
onClick={() => navigate(`/entrance/${encodeURIComponent(item.moduleName)}`)}
|
||||
onClick={() => navigate(`/${encodeURIComponent(item.moduleName)}`)}
|
||||
>
|
||||
<div className="absolute top-15 flex flex-col items-center">
|
||||
<Image key={item.id} src={getStaticImage(item.image)} />
|
||||
@@ -132,7 +119,7 @@ const Menu = () => {
|
||||
<Image
|
||||
src={getStaticImage('menu/导航地图')}
|
||||
button
|
||||
onClick={() => setIsPuzzleCenterVisible(true)}
|
||||
onClick={() => navigate('/learning-map')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ const Chenyun = () => {
|
||||
className="absolute right-37 top-22"
|
||||
src={getStaticImage('components/革命先驱故居-返回')}
|
||||
button
|
||||
onClick={() => navigate(`/entrance/${moduleName}`)}
|
||||
onClick={() => navigate(`/${moduleName}`)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
||||
|
||||
@@ -55,7 +55,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
value: 'B',
|
||||
label: '鸟笼比喻',
|
||||
},
|
||||
{ value: 'C', label: '正确的处理好计划与市场的关系', isCorrect: true },
|
||||
{ value: 'C', label: '正确地处理好计划与市场的关系', isCorrect: true },
|
||||
{ value: 'D', label: '经济建设的目的论', isCorrect: true },
|
||||
],
|
||||
explanation: '鸟笼比喻是陈云提出的一个比喻。请你再想想吧!',
|
||||
@@ -69,14 +69,14 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '不唯上、不唯书、只唯实,交换、比较、反复',
|
||||
label: '不唯上、不唯书、只唯实,交换、比较、反复',
|
||||
isCorrect: true,
|
||||
},
|
||||
{ value: 'B', label: '无农不稳,无粮则乱', isCorrect: true },
|
||||
{ value: 'C', label: '个人名利淡如水,党的事业重如山', isCorrect: true },
|
||||
{ value: 'B', label: '无农不稳,无粮则乱', isCorrect: true },
|
||||
{ value: 'C', label: '个人名利淡如水,党的事业重如山', isCorrect: true },
|
||||
{
|
||||
value: 'D',
|
||||
label: '领导机关制定政策,要用百分之九十以上的时间作调查研究工作',
|
||||
label: '领导机关制定政策,要用百分之九十以上的时间作调查研究工作',
|
||||
isCorrect: true,
|
||||
},
|
||||
],
|
||||
@@ -378,7 +378,7 @@ const ChenyunInternalization = () => {
|
||||
<Narration
|
||||
visible={narrationVisible}
|
||||
audioChannelId="chenyun-internalization-narration"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_36.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_35_1.mp3')}
|
||||
onClose={() => {
|
||||
setNarrationVisible(false)
|
||||
setVisible(true)
|
||||
@@ -417,11 +417,14 @@ const ChenyunInternalization = () => {
|
||||
>
|
||||
<GuideStepsItem
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_36.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_36_1.mp3')}
|
||||
>
|
||||
小沪,跟着你参观陈云纪念馆真是收获满满!看到馆里展示的“米棉之战”史料、全国财经统筹的实物展品,我真切感受到陈云爷爷在建国初期平抑物价、稳定经济的魄力。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="student">
|
||||
<GuideStepsItem
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_36_2.mp3')}
|
||||
>
|
||||
他用“全国一盘棋”思路化解危机的案例太震撼了,那你能再跟我讲讲,支撑这些成就的核心经济思想是什么吗?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
@@ -447,18 +450,26 @@ const ChenyunInternalization = () => {
|
||||
>
|
||||
<GuideStepsItem
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_38.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_38_1.mp3')}
|
||||
>
|
||||
感谢房老师的讲解!从陈云童年成长的水乡故居,到他主持"银元之战""米棉之战"的史料,再到那台称量粮食的天平、握不住的铅笔头,我真切感受到他"不唯上、不唯书、只唯实"的精神,也懂了他"为老百姓做事"的初心。
|
||||
感谢房老师的讲解!从陈云童年成长的水乡故居,到他主持“银元之战”“米棉之战”的史料,再到那台称量粮食的天平、握不住的铅笔头,我真切感受到他“不唯上、不唯书、只唯实”的精神,也懂了他“为老百姓做事”的初心。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_38_2.mp3')}
|
||||
>
|
||||
这些都让我特别受触动!
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="student">这些都让我特别受触动!</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_39.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_39_1.mp3')}
|
||||
>
|
||||
你的感悟很深刻!陈云同志用一生诠释了“务实”与“为民”——从破解边区经济困境,到擘画国家工业化蓝图,再到改革开放时力挺宝钢建设,每一步都扎根实际、心系百姓。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_39_2.mp3')}
|
||||
>
|
||||
你不妨把这份触动凝练成几句心里话,既总结参观的收获,也想想未来该如何践行这份精神,用自己的行动传承这份初心吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -36,12 +36,11 @@ const ChenyunPlace1 = () => {
|
||||
|
||||
// 学习引导弹窗
|
||||
const [showGuideModal, setShowGuideModal] = useState(true)
|
||||
// 对话结束后的学习引导弹窗
|
||||
// 温馨提示弹窗
|
||||
const [showAfterDialogGuide, setShowAfterDialogGuide] = useState(false)
|
||||
const [afterDialogGuideShown, setAfterDialogGuideShown] = useState(false)
|
||||
// 学习说明对话框显示状态
|
||||
const [showIntroDialog, setShowIntroDialog] = useState(false)
|
||||
// 引导对话显示状态
|
||||
// 温馨提示关闭后的场景对话显示状态
|
||||
const [visible, setVisible] = useState(false)
|
||||
// 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识)
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null)
|
||||
@@ -169,12 +168,12 @@ const ChenyunPlace1 = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 学习引导词 */}
|
||||
{/* 学习引导 */}
|
||||
<StudyGuideModal
|
||||
visible={showGuideModal}
|
||||
onContinue={() => {
|
||||
setShowGuideModal(false)
|
||||
setVisible(true)
|
||||
setShowAfterDialogGuide(true)
|
||||
}}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_02.mp3')}
|
||||
ailgn="left"
|
||||
@@ -198,18 +197,8 @@ const ChenyunPlace1 = () => {
|
||||
</Overlay>
|
||||
)}
|
||||
|
||||
{/* 场景对话 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={(nextVisible) => {
|
||||
setVisible(nextVisible)
|
||||
if (!nextVisible && !afterDialogGuideShown) {
|
||||
setAfterDialogGuideShown(true)
|
||||
setShowAfterDialogGuide(true)
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
>
|
||||
{/* 温馨提示关闭后的场景对话 */}
|
||||
<GuideSteps visible={visible} onVisibleChange={setVisible} showMask={false}>
|
||||
<GuideStepsItem
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_03.mp3')}
|
||||
@@ -218,22 +207,31 @@ const ChenyunPlace1 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_04.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_04_1.mp3')}
|
||||
>
|
||||
这座房子就是陈云故居,位于上海市青浦区练塘镇下塘街95号,是一座坐南朝北、面向河畔的两层楼江南民居。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_04_2.mp3')}
|
||||
>
|
||||
实际上是陈云舅父母的家,也是少年陈云从4岁至14岁生活的地方。童年的陈云,家庭生活十分不幸。他两岁时父亲病故,四岁时,母亲也悲苦地死去。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_04_3.mp3')}
|
||||
>
|
||||
父母去世后,由外婆抚养。六岁时,外婆去世,陈云过继给舅父母当养子。陈云从小跟舅父廖文光和舅母廖倪氏一起生活。家里靠种田和做小生意勉强维持生计。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 对话结束后的学习引导 */}
|
||||
{/* 温馨提示 */}
|
||||
<StudyGuideModal
|
||||
visible={showAfterDialogGuide}
|
||||
onContinue={() => setShowAfterDialogGuide(false)}
|
||||
onContinue={() => {
|
||||
setShowAfterDialogGuide(false)
|
||||
setVisible(true)
|
||||
}}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_01.mp3')}
|
||||
firstLineIndent="2em"
|
||||
>
|
||||
|
||||
@@ -168,11 +168,14 @@ const ChenyunPlace2 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_06.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_06_1.mp3')}
|
||||
>
|
||||
因为家贫,初小读完的陈云便辍学在家。后在舅母亲戚的资助下,陈云才到青浦县立乙种商业学校学习珠算和记账的,他在很短时间里就基本掌握了珠算,还初步学会写账,这为陈云日后从事经济工作打下了基础。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_06_2.mp3')}
|
||||
>
|
||||
陈云在党内以“铁算盘”著称,他在经济工作中能熟练使用算盘,就得益于少年时在青浦县立乙种商业学校的学习。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
@@ -195,11 +198,14 @@ const ChenyunPlace2 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_08.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_08_1.mp3')}
|
||||
>
|
||||
是的,陈云在青浦县立乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_08_2.mp3')}
|
||||
>
|
||||
颜安小学实行的新式教育开阔了陈云的视野,五四运动后,颜安小学师生积极投入反帝爱国运动,为陈云日后加入中国共产党埋下了思想的种子。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -236,11 +236,14 @@ const ChenyunPlace3 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_10.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_10_1.mp3')}
|
||||
>
|
||||
从颜安小学毕业后,陈云进入了商务印书馆当学徒,开始在柜台接待顾客,打珠算、写信封、练包装、理货物、修文具。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_10_2.mp3')}
|
||||
>
|
||||
陈云的珠算能力在商务印书馆工作期间有了很大提高。由于工作认真负责,三年的学徒期,陈云提前两年就破格到商务印书馆虹口分店(现为1925书局)当店员。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -241,14 +241,20 @@ const ChenyunPlace4 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_12.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_12_1.mp3')}
|
||||
>
|
||||
1932年3月,陈云担任中共临时中央政治局常委,兼任全国总工会党团书记。1933年1月,陈云离开上海前往中央苏区首府瑞金,继续参加中共中央和全国总工会的领导工作。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_12_2.mp3')}
|
||||
>
|
||||
虽然陈云在中央苏区的任务主要是领导工人运动,但是陈云总是能针对一些棘手的经济问题提出较出色的建议。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_12_3.mp3')}
|
||||
>
|
||||
在中央苏区时期,陈云也关注和积极参与解决了许多经济难题。1934年,在中共六届五中全会上,陈云被选为中央政治局委员、常委,兼任中央白区工作部部长。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
@@ -271,14 +277,20 @@ const ChenyunPlace4 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_14.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_14_1.mp3')}
|
||||
>
|
||||
中央苏区时期的经济斗争,是陈云经济工作能力的“淬炼场”。陈云分析了“左”倾经济政策的表现、危害以及原因。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_14_2.mp3')}
|
||||
>
|
||||
他强调,当务之急是重新严格审查各个企业、工厂以及商店、大小作坊的实际承受力和经济支付能力。从实际出发,实事求是地制定符合中央苏维埃区域每一个工厂、企业、作坊发展需求的劳动政策。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_14_3.mp3')}
|
||||
>
|
||||
陈云提议建立合作社,广泛吸收失业工人加入,多听取工人建议、增强企业管理的民主性,从而更好地建设苏区经济。这些做法为他日后主持全国财经工作积累了宝贵经验。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -165,11 +165,14 @@ const ChenyunPlace5 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_16.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_16_1.mp3')}
|
||||
>
|
||||
1944年新春伊始,陈云开始担任西北财经办事处副主任,工作转到财经方面,这是陈云以主要精力从事领导经济工作的开始。当时,陕甘宁边区在国民党严密封锁下,财经状况严峻,边区物价猛涨,老百姓日常生活必需的布匹和粮食十分缺乏。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_16_2.mp3')}
|
||||
>
|
||||
国民党顽固派封锁边区,企图“不让一粒粮、一尺布进入边区”,同时千方百计阻止边区食盐出口。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -134,11 +134,14 @@ const ChenyunPlace6 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_18.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_18_1.mp3')}
|
||||
>
|
||||
是的,新中国刚成立时,中国共产党接管的是一个民穷财尽、物价飞涨的烂摊子。面对财政经济上的严重困难,陈云在1949年受命担任新成立的中央财政经济委员会(简称中财委)主任,主持领导全国财政经济工作。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_18_2.mp3')}
|
||||
>
|
||||
陈云挂帅后的首仗就是打击投机、平抑物价。通过“银元之战”“两白一黑”战役等几次严峻斗争,只用不到一年时间,就迅速稳定了金融物价。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -44,7 +44,7 @@ const QUIZ1_TITLE =
|
||||
|
||||
const QUIZ1_ERROR_HINTS: Record<string, string> = {
|
||||
A: '淮海战役于1949年1月胜利结束,发生在这次“银元之战”前,不存在因果关系。请你再想想吧!',
|
||||
C: '1949年6月新中国政权尚未建立。请你再想想吧',
|
||||
C: '1949年6月新中国政权尚未建立。请你再想想吧!',
|
||||
}
|
||||
|
||||
const QUIZ1_FALLBACK_HINT = '再想一想,注意题干中的时间和提示信息哦~'
|
||||
@@ -305,18 +305,24 @@ const ChenyunPlace7 = () => {
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath(`${AUDIO_PREFIX}/mp3_chenyun_19.mp3`)}
|
||||
>
|
||||
那"银元之战"是如何取得胜利的呢?
|
||||
那“银元之战”是如何取得胜利的呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath(`${AUDIO_PREFIX}/mp3_chenyun_20.mp3`)}
|
||||
audioSrc={getPublicPath(`${AUDIO_PREFIX}/mp3_chenyun_20_1.mp3`)}
|
||||
>
|
||||
由于通货膨胀,国民党发行的法币、金圆券变成了一堆废纸。当时坊间流传“大街过三道,物价跳三跳”。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath(`${AUDIO_PREFIX}/mp3_chenyun_20_2.mp3`)}
|
||||
>
|
||||
有人计算,一百元法币,在一九三七年可买两头牛,一九四五年只能买两个鸡蛋,一九四七年只能买一个煤球,到一九四九年五月连一粒米也买不到。可见,当时通货膨胀多么严重。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath(`${AUDIO_PREFIX}/mp3_chenyun_20_3.mp3`)}
|
||||
>
|
||||
打击投机资本,稳定金融物价,陈云代中央起草《中共中央关于打击银元使人民币占领阵地的指示》,提出以强硬手段打击投机势力。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
@@ -364,7 +370,7 @@ const ChenyunPlace7 = () => {
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath(`${AUDIO_PREFIX}/mp3_chenyun_21.mp3`)}
|
||||
>
|
||||
那"两白一黑"战役是如何打赢的呢?
|
||||
那“两白一黑”战役是如何打赢的呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
@@ -395,7 +401,6 @@ const ChenyunPlace7 = () => {
|
||||
title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onClose={handleSuccessDismiss}
|
||||
onContinue={handleSuccessDismiss}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -189,11 +189,14 @@ const ChenyunPlace8 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_26.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_26_1.mp3')}
|
||||
>
|
||||
不错,随着大规模经济建设的展开,全国粮、棉、油等主要农产品严重紧缺,供求矛盾尖锐。陈云经过调查研究,反复比较,权衡利弊得失,终于下决心采用在农村征购,在城市配售的解决粮食问题的办法,也就是后来所说的“统购统销”。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_26_2.mp3')}
|
||||
>
|
||||
陈云说:“只有采用这种办法,才能保证我国人民生活日益增长的需要,才能制止投机活动,保证市场物价的稳定,才能使发展国民经济第一个五年计划得以顺利地进行。”
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
@@ -231,14 +234,20 @@ const ChenyunPlace8 = () => {
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_24.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_24_1.mp3')}
|
||||
>
|
||||
一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_24_2.mp3')}
|
||||
>
|
||||
这次编制工作是在前四次基础上进行的,陈云提出计划编制工作中要注意几个问题:首先是工业投资金额,其次是市场供应和人民生活,第三是农业发展速度,这三个问题对编制计划有指导意义。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_24_3.mp3')}
|
||||
>
|
||||
第一个五年计划纲要草案的重点是优先发展重工业,特别是156项重点工程。当时陈云重点抓重工业建设和粮食保障,通过“统购统销”稳定粮价,为重点工程项目筑牢根基,亲手铺就了我国工业化的起步之路。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -147,14 +147,20 @@ const ChenyunPlace9 = () => {
|
||||
>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_27.mp3')}
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_27_1.mp3')}
|
||||
>
|
||||
我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他“适合老百姓的要求,做了一点事”的初心。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_27_2.mp3')}
|
||||
>
|
||||
接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的“银元之战”和“米棉之战”之中去。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_27_3.mp3')}
|
||||
>
|
||||
在实践决策中体会陈云“多谋善断”“敢闯务实”的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { ClueData } from '../clue/types'
|
||||
// 银元之战视频配置
|
||||
export const YINYUAN_VIDEO_CONFIG = {
|
||||
src: getPublicPath('video/chenyun/chenyun_15.mp4'),
|
||||
narration: `1949年5月,上海解放,此时上海经济秩序混乱,一些资本家甚至说“共产党是军事一百分,政治八十分,财经打零分。“上海经济旧势力利用人们长期以来形成的担心钞票贬值的心理,大肆贩卖银元,掀起投机风潮。上海证券交易所是投机商的大本营,而大街小巷的银元贩子却更多。原来的一块银元值100人民币,到了6月4日已经涨到1100元,而涨势仍在继续。事实上,银元已经是上海市场的主要货币。有些商号只认银元,把人民币排斥到市场之外。上海,这个全国的经济心脏,能否在新政权手中恢复跳动?一场没有硝烟的经济战争即将打响。面对这硝烟弥漫的经济战线,陈云同志亲自参与指导了同旧经济势力间的“银元之战。你将化身陈云同志的特派员,直面这场“银元之战”。"`,
|
||||
narration: `1949年5月,上海解放,此时上海经济秩序混乱,一些资本家甚至说“共产党是军事一百分,政治八十分,财经打零分。“上海经济旧势力利用人们长期以来形成的担心钞票贬值的心理,大肆贩卖银元,掀起投机风潮。上海证券交易所是投机商的大本营,而大街小巷的银元贩子却更多。原来的一块银元值100人民币,到了6月4日已经涨到1100元,而涨势仍在继续。事实上,银元已经是上海市场的主要货币。有些商号只认银元,把人民币排斥到市场之外。上海,这个全国的经济心脏,能否在新政权手中恢复跳动?一场没有硝烟的经济战争即将打响。面对这硝烟弥漫的经济战线,陈云同志亲自参与指导了同旧经济势力间的“银元之战”。”你将化身陈云同志的特派员,直面这场“银元之战”。`,
|
||||
alert: {
|
||||
title: '第一幕:“银元之战”--人民币的阵地争夺战',
|
||||
subtitle: '上海目前出现大肆贩卖银元的情况,请你了解情况!',
|
||||
@@ -36,10 +36,10 @@ export const YINYUAN_CLUE_DATA: Record<
|
||||
title: '线索二',
|
||||
content: (
|
||||
<div className="flex flex-col items-start text-left gap-2 px-14">
|
||||
<div className="mb-8">
|
||||
<div className="mb-8 indent-14">
|
||||
南京路的先施、永安、大新、新新等四大公司率先拒收人民币,甚至用银元标价。
|
||||
</div>
|
||||
<div>其他商户纷纷仿效,人民币在市场交易中地位下降。</div>
|
||||
<div className="indent-14">其他商户纷纷仿效,人民币在市场交易中地位下降。</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 陈云反思页面主入口
|
||||
* 整合银元之战和米棉之战两个模块
|
||||
*/
|
||||
import { Image, StudyGuideModal } from '@/components'
|
||||
import { Image, StudyGuideModal, Video } from '@/components'
|
||||
import ErrorAlert from '@/components/layout/alert/error-alert'
|
||||
import TitleAlert from '@/components/layout/alert/title-alert'
|
||||
import ClueOverlay from '@/pages/chenyun/reflection/clue'
|
||||
@@ -63,10 +63,10 @@ const ChenyunReflection = () => {
|
||||
/** 是否显示线索弹窗 */
|
||||
const [showClueOverlay, setShowClueOverlay] = useState(false)
|
||||
|
||||
const [showOpeningVideo, setShowOpeningVideo] = useState(true)
|
||||
|
||||
/** 欢迎语弹窗(使用 StudyGuideModal 展示入口引导) */
|
||||
const [showWelcomeGuide, setShowWelcomeGuide] = useState(
|
||||
() => !useChenyunStore.getState().isYinyuanSolved
|
||||
)
|
||||
const [showWelcomeGuide, setShowWelcomeGuide] = useState(false)
|
||||
|
||||
/** 第一幕银元之战 - 学习引导弹窗 */
|
||||
const [showYinyuanGuide, setShowYinyuanGuide] = useState(false)
|
||||
@@ -219,6 +219,36 @@ const ChenyunReflection = () => {
|
||||
backgroundImage: getBackgroundImage(`chenyun/reflection/底图`),
|
||||
}}
|
||||
>
|
||||
{showOpeningVideo && (
|
||||
<div className="fixed inset-0 z-10000 w-full h-full bg-black">
|
||||
<Video
|
||||
src={getPublicPath('video/reflection/chenyun_tiwu_00.mp4')}
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipText="跳过"
|
||||
onEnded={() => {
|
||||
setShowOpeningVideo(false)
|
||||
if (!useChenyunStore.getState().isYinyuanSolved) {
|
||||
setShowWelcomeGuide(true)
|
||||
}
|
||||
}}
|
||||
onSkip={() => {
|
||||
setShowOpeningVideo(false)
|
||||
if (!useChenyunStore.getState().isYinyuanSolved) {
|
||||
setShowWelcomeGuide(true)
|
||||
}
|
||||
}}
|
||||
onError={() => {
|
||||
setShowOpeningVideo(false)
|
||||
if (!useChenyunStore.getState().isYinyuanSolved) {
|
||||
setShowWelcomeGuide(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 离开按钮 */}
|
||||
{expandedFile == null && (
|
||||
<Image
|
||||
|
||||
@@ -139,7 +139,7 @@ export const YinyuanBattle = ({
|
||||
// 关闭多选题1,显示旁白文字(图2)
|
||||
state.setShowQuiz1(false)
|
||||
state.setNarration2Text(
|
||||
`六月五日,上海政府抛出十万银元,力图以银元制服银元,使价格回跌。全市举行了"反对银元投机,保障人民生活"的游行和宣传。上海市军委会主任陈毅在全市集会上警告投机奸商"赶快洗手不干,否则勿谓言之不预"。但这两手收效仍微。十万银元被投机商人一吸而空,六月七日银元每元的价格涨到一千八百元人民币。陈云同志判断:"单纯的经济手段已不足以震慑敌人,必须辅以坚定的行政与法律手段。"`
|
||||
`六月五日,上海政府抛出十万银元,力图以银元制服银元,使价格回跌。全市举行了“反对银元投机,保障人民生活”的游行和宣传。上海市军委会主任陈毅在全市集会上警告投机奸商“赶快洗手不干,否则勿谓言之不预”。但这两手收效仍微。十万银元被投机商人一吸而空,六月七日银元每元的价格涨到一千八百元人民币。陈云同志判断:“单纯的经济手段已不足以震慑敌人,必须辅以坚定的行政与法律手段。”`
|
||||
)
|
||||
state.setShowNarration2(true)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const YinyuanGuides = ({
|
||||
<ExhibitOverlay
|
||||
visible={showTrendChartDetail}
|
||||
src="chenyun/reflection/银元价格走势图"
|
||||
description={`以上海为中心,代表"人民币信用"的绿色区域迅速覆盖全市,并向外辐射。`}
|
||||
description={`以上海为中心,代表“人民币信用”的绿色区域迅速覆盖全市,并向外辐射。`}
|
||||
onClick={onTrendChartClick}
|
||||
onClose={onTrendChartDetailClose}
|
||||
/>
|
||||
|
||||