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