Compare commits
8 Commits
37e0413576
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb78d8bf5c | ||
|
|
c4641aee19 | ||
|
|
782f76170f | ||
|
|
d83654837a | ||
|
|
899adbc9d4 | ||
|
|
4499d7fdef | ||
|
|
11d7496b54 | ||
|
|
1012af6039 |
BIN
scratch-gui/competitio_login/assets/gongan-beian.png
Normal file
BIN
scratch-gui/competitio_login/assets/gongan-beian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -384,7 +384,7 @@
|
||||
<footer>
|
||||
<div class="footer-line" role="presentation"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="assets/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"test:lint": "eslint . --ext .js,.jsx",
|
||||
"test:unit": "jest test[\\\\/]unit[\\\\/]addons",
|
||||
"test:smoke": "jest --runInBand test[\\\\/]smoke",
|
||||
"test:cpp": "node scripts/test-cpp-regressions.cjs",
|
||||
"test:devtool": "node scripts/test-devtool-control.cjs",
|
||||
"watch": "webpack --colors --watch"
|
||||
},
|
||||
"config": {
|
||||
|
||||
71
scratch-gui/scripts/diagnose-cpp.cjs
Normal file
71
scratch-gui/scripts/diagnose-cpp.cjs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Read-only diagnostic: exercise the current editor interpreter with mocked game APIs.
|
||||
// Run from scratch-gui: node scripts/diagnose-cpp.cjs
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const babel = require('@babel/core');
|
||||
const filename = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
|
||||
const transformed = babel.transformSync(fs.readFileSync(filename, 'utf8'), {
|
||||
filename, configFile: false, babelrc: false,
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs']
|
||||
}).code;
|
||||
const context = {
|
||||
exports: {}, require, setTimeout,
|
||||
console: {log() {}, warn() {}, error() {}}, window: {}
|
||||
};
|
||||
vm.runInNewContext(transformed, context, {filename});
|
||||
const {CppCodeExecutor, validateCppSyntax} = context.exports;
|
||||
const main = body => `int main() {\n${body}\n}`;
|
||||
const customer = `void test() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
turnLeft(-1);
|
||||
for (int i = 0; i < 1; i++) {
|
||||
test();
|
||||
playerMove(i);
|
||||
}
|
||||
}`;
|
||||
const cases = [
|
||||
['customer_local_i', customer, {moves: [0]}],
|
||||
['customer_renamed_inner_j', customer.replace('int i = 0; i < 2; i++', 'int j = 0; j < 2; j++'), {moves: [0]}],
|
||||
['cout_literal', main('cout << "Hello" << endl;'), {output: ['Hello\n']}],
|
||||
['cout_variable', main('int i = 7;\ncout << "i=" << i << endl;'), {output: ['i=7\n']}],
|
||||
['cout_string_assignment_shape', main('cout << "value=42" << endl;'), {output: ['value=42\n']}],
|
||||
['cout_increment_expression', main('int i = 0;\ncout << i++ << endl;\nplayerMove(i);'), {output: ['0\n'], moves: [1]}],
|
||||
['cout_increment_literal', main('cout << "C++" << endl;'), {output: ['C++\n']}],
|
||||
['cout_std_qualified', main('std::cout << "Hello" << std::endl;'), {output: ['Hello\n']}],
|
||||
['main_return_zero', main('return 0;'), {error: null}],
|
||||
['legal_space_before_call', main('playerMove (1);'), {valid: true, moves: [1]}],
|
||||
['nested_loop_shadowing', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
|
||||
['negative_integer_division', main('int a = -3;\na /= 2;\nplayerMove(a);'), {moves: [-1]}],
|
||||
['function_return_value', 'int answer() {\nreturn 7;\n}\n' + main('int n = answer();\nplayerMove(n);'), {moves: [7]}],
|
||||
['nested_if_break', main('for (int i = 0; i < 3; i++) {\nif (i == 1) {\nbreak;\n}\nplayerMove(i);\n}'), {moves: [0]}],
|
||||
['floating_point', main('double x = 1.5;\ncout << x << endl;'), {output: ['1.5\n']}],
|
||||
['array_index', main('int a[2] = {3, 7};\nplayerMove(a[1]);'), {moves: [7]}],
|
||||
['unknown_variable', main('playerMove(missing);'), {valid: false}],
|
||||
['early_return_zero', 'int stop() {\nreturn 0;\nplayerMove(9);\n}\n' + main('stop();'), {moves: []}]
|
||||
];
|
||||
(async () => {
|
||||
const results = [];
|
||||
for (const [name, code, expected] of cases) {
|
||||
const actual = {valid: false, moves: [], turns: [], output: [], error: null};
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {
|
||||
playerMove: async n => actual.moves.push(n),
|
||||
turnLeft: async n => actual.turns.push(n),
|
||||
cmdPrint: text => actual.output.push(text)
|
||||
}};
|
||||
const validation = await validateCppSyntax(code);
|
||||
actual.valid = validation.isValid;
|
||||
if (!actual.valid) actual.validationErrors = validation.errors;
|
||||
const executor = new CppCodeExecutor(code, null);
|
||||
if (actual.valid) {
|
||||
try { await executor.run(); } catch (error) {actual.error = error.message;}
|
||||
}
|
||||
const matchesExpected = Object.entries(expected).every(([key, value]) =>
|
||||
JSON.stringify(actual[key]) === JSON.stringify(value));
|
||||
results.push({name, expected, actual, matchesExpected});
|
||||
}
|
||||
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
@@ -21,7 +21,7 @@ function getConfigForEnv(env) {
|
||||
case 'test':
|
||||
return { url: 'https://test-service.001code.cn', disabledev: false };
|
||||
case 'prod':
|
||||
return { url: 'https://service.001code.cn', disabledev: true };
|
||||
return { url: 'https://service.001code.com', disabledev: true };
|
||||
case 'start':
|
||||
default:
|
||||
return { url: 'http://127.0.0.1:8000', disabledev: false };
|
||||
|
||||
139
scratch-gui/scripts/test-cpp-regressions.cjs
Normal file
139
scratch-gui/scripts/test-cpp-regressions.cjs
Normal file
@@ -0,0 +1,139 @@
|
||||
// Regression tests for variable scopes and return control flow in the editor interpreter.
|
||||
// Run: node scripts/test-cpp-regressions.cjs
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const babel = require('@babel/core');
|
||||
const parserPath = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
|
||||
const source = fs.readFileSync(parserPath, 'utf8');
|
||||
const compiled = babel.transformSync(source, {
|
||||
filename: parserPath, configFile: false, babelrc: false,
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs'],
|
||||
presets: process.argv.includes('--transpiled') ? ['@babel/preset-env'] : []
|
||||
}).code;
|
||||
const silentConsole = {log() {}, warn() {}, error() {}};
|
||||
const context = {exports: {}, require, setTimeout, console: silentConsole, window: {}};
|
||||
vm.runInNewContext(compiled, context, {filename: parserPath});
|
||||
const {CppCodeExecutor, validateCppSyntax, createCppExecutor} = context.exports;
|
||||
const main = body => `int main() {\n${body}\n}`;
|
||||
// Complete program from the first email screenshot; the second repeats this issue.
|
||||
const customerEmailCode = `void test() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 在这里编写你的C++代码来控制角色
|
||||
turnLeft(-1);
|
||||
for (int i = 0; i < 1; i++) {
|
||||
test();
|
||||
playerMove(i);
|
||||
}
|
||||
return 0;
|
||||
}`;
|
||||
async function run(code, expected, configure) {
|
||||
const moves = [];
|
||||
const turns = [];
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {
|
||||
playerMove: async n => moves.push(n),
|
||||
turnLeft: async n => turns.push(n),
|
||||
cmdPrint: () => {}
|
||||
}};
|
||||
const executor = new CppCodeExecutor(code, null);
|
||||
if (configure) configure(executor, context.window);
|
||||
const validation = await validateCppSyntax(code);
|
||||
assert.strictEqual(validation.isValid, true, JSON.stringify(validation.errors));
|
||||
const exitCode = await executor.run();
|
||||
if (Object.prototype.hasOwnProperty.call(expected, 'exitCode')) assert.strictEqual(exitCode, expected.exitCode);
|
||||
if (expected.moves) assert.deepStrictEqual(moves, expected.moves);
|
||||
if (expected.turns) assert.deepStrictEqual(turns, expected.turns);
|
||||
assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution');
|
||||
assert.strictEqual(executor.callStack.length, 0);
|
||||
assert.strictEqual(executor.currentExecutionContext, null);
|
||||
assert.strictEqual(executor.isRunning, false);
|
||||
return executor;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
['screenshot: main return 1 is a normal exit', main('playerMove(4);\nreturn 1;'), {moves: [4], exitCode: 1}],
|
||||
['return 1 skips remaining main statements', main('return 1;\nplayerMove(9);'), {moves: [], exitCode: 1}],
|
||||
['return 0 also stops main early', main('return 0;\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
||||
['negative exit code', main('return -1;'), {moves: [], exitCode: -1}],
|
||||
['parenthesized return expression', main('int status = 2;\nreturn (status + 1);'), {moves: [], exitCode: 3}],
|
||||
['main fallthrough returns zero', main('playerMove(1);'), {moves: [1], exitCode: 0}],
|
||||
['return from for loop exits main', main('for (int i = 0; i < 3; i++) {\nplayerMove(i);\nreturn 1;\n}\nplayerMove(9);'), {moves: [0], exitCode: 1}],
|
||||
['return zero inside loop is not ignored', main('for (int i = 0; i < 3; i++) {\nreturn 0;\n}\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
||||
['return through while and if exits main', main('while (true) {\nif (true) {\nreturn 2;\n}\nplayerMove(9);\n}\nplayerMove(8);'), {moves: [], exitCode: 2}],
|
||||
['function return 0 skips only its own remaining statements', 'int f() {\nreturn 0;\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);\nreturn 1;'), {moves: [1], exitCode: 1}],
|
||||
['function return inside loop resumes caller', 'int f() {\nfor (int i = 0; i < 3; i++) {\nreturn 1;\n}\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);'), {moves: [1], exitCode: 0}],
|
||||
['identifier starting with return is not a return statement', main('int returnValue = 1;\nreturnValue = 2;\nplayerMove(returnValue);\nreturn 0;'), {moves: [2], exitCode: 0}],
|
||||
['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}],
|
||||
['nested loops', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
|
||||
['loop initializer restores shadowed value', main('int i = 9;\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [9]}],
|
||||
['loop assignment updates existing value', main('int i = 9;\nfor (i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [2]}],
|
||||
['block declaration versus assignment', main('int n = 1;\nif (true) {\nint n = 7;\nplayerMove(n);\n}\nif (true) {\nn = 3;\n}\nplayerMove(n);'), {moves: [7, 3]}],
|
||||
['for body scope per iteration', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nplayerMove(x);\nint x = 2;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
|
||||
['while body scope per iteration', main('int x = 9;\nint i = 0;\nwhile (i < 2) {\nplayerMove(x);\nint x = 2;\ni++;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
|
||||
['function locals', 'void f() {\nint n = 7;\nplayerMove(n);\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [7, 2]}],
|
||||
['all arguments evaluated before binding', 'void f(int a, int b) {\nplayerMove(a);\nplayerMove(b);\n}\n' + main('int a = 2;\nf(7, a);\nplayerMove(a);'), {moves: [7, 2, 2]}],
|
||||
['recursive locals and parameters', 'void f(int n) {\nint saved = n;\nif (n > 0) {\nf(n - 1);\n}\nplayerMove(saved);\n}\n' + main('f(2);'), {moves: [0, 1, 2]}],
|
||||
['early void return unwinds scope', 'void f() {\nint n = 8;\nreturn;\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [2]}],
|
||||
['continue unwinds body scope', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nint x = 2;\ncontinue;\n}\nplayerMove(x);'), {moves: [9]}],
|
||||
['break does not increment', main('int i = 0;\nfor (i = 0; i < 2; i++) {\nbreak;\n}\nplayerMove(i);'), {moves: [0]}],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
for (const [name, code, expected] of cases) {
|
||||
try { await run(code, expected); } catch (error) {error.message = `${name}: ${error.message}`; throw error;}
|
||||
console.log(`PASS ${name}`);
|
||||
}
|
||||
const strings = new CppCodeExecutor('', null);
|
||||
await strings.executeSimpleStatement('string msg = "outer";');
|
||||
await strings.withVariableScope(async () => {
|
||||
await strings.executeSimpleStatement('string msg = "inner";');
|
||||
assert.strictEqual(strings.variables.get('msg'), 'inner');
|
||||
});
|
||||
assert.strictEqual(strings.variables.get('msg'), 'outer');
|
||||
console.log('PASS string block shadowing');
|
||||
|
||||
// Seed the interpreter's global environment to verify lexical lookup and writes.
|
||||
const globalExecutor = await run('void f() {\ng += 1;\nplayerMove(g);\n}\n' + main('int g = 9;\nf();\nplayerMove(g);'), {moves: [2, 9]}, executor => executor.variables.set('g', 1));
|
||||
assert.strictEqual(globalExecutor.variables.get('g'), 2);
|
||||
console.log('PASS global writes survive function calls; caller locals are not visible');
|
||||
|
||||
for (const stop of [false, true]) {
|
||||
const executor = new CppCodeExecutor('void f(int n) {\nfor (int i = 0; i < 2; i++) {\nplayerMove(i);\n}\n}\n' + main('f(2);'), null);
|
||||
executor.variables.set('n', 9);
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {playerMove: async () => {
|
||||
if (stop) context.window.isScriptRunOver = true;
|
||||
else throw new Error('game failure');
|
||||
}}};
|
||||
if (stop) await executor.run();
|
||||
else await assert.rejects(executor.run(), /game failure/);
|
||||
assert.strictEqual(executor.variables, executor.globalVariables);
|
||||
assert.strictEqual(executor.variables.get('n'), 9);
|
||||
assert.strictEqual(executor.variables.has('i'), false);
|
||||
assert.strictEqual(executor.callStack.length, 0);
|
||||
assert.strictEqual(executor.currentExecutionContext, null);
|
||||
console.log(`PASS scope cleanup on ${stop ? 'stop' : 'error'}`);
|
||||
}
|
||||
|
||||
const highlighted = [];
|
||||
context.window = {isScriptRunOver: false, setHighlightedLine: line => highlighted.push(line)};
|
||||
const manager = createCppExecutor();
|
||||
for (const status of [1, 0]) {
|
||||
assert.strictEqual(await manager.execute(main(`return ${status};`)), status);
|
||||
assert.strictEqual(manager.currentExecutor, null);
|
||||
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
||||
}
|
||||
console.log('PASS editor executor manager resolves nonzero exit and can run again');
|
||||
|
||||
context.window.cppGameAPI = {playerMove: async () => {throw new Error('RETURN_INTERRUPT');}};
|
||||
await assert.rejects(manager.execute('void f() {\nplayerMove(1);\n}\n' + main('f();')), /RETURN_INTERRUPT/);
|
||||
assert.strictEqual(manager.currentExecutor, null);
|
||||
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
||||
console.log('PASS real game errors are not mistaken for a return signal');
|
||||
|
||||
console.log(`${cases.length + 6} regression checks passed`);
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
166
scratch-gui/scripts/test-devtool-control.cjs
Normal file
166
scratch-gui/scripts/test-devtool-control.cjs
Normal file
@@ -0,0 +1,166 @@
|
||||
// Run: npm run test:devtool
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const babel = require('@babel/core');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const controlPath = 'src/playground/devtool-control.js';
|
||||
const compile = file => babel.transformFileSync(path.join(root, file), {
|
||||
configFile: false, babelrc: false, presets: ['@babel/preset-env', '@babel/preset-react']
|
||||
}).code;
|
||||
const controlCode = compile(controlPath);
|
||||
const homeCode = compile('src/playground/home.js');
|
||||
let checks = 0;
|
||||
function test(name, action) {
|
||||
action();
|
||||
checks++;
|
||||
console.log(`PASS ${name}`);
|
||||
}
|
||||
|
||||
function page(storage = new Map(), {enabled = true, brokenStorage = false, immediate = false} = {}) {
|
||||
const starts = [];
|
||||
const effects = [];
|
||||
function detector(options) {
|
||||
starts.push(options);
|
||||
// Simulate a library probe firing during initialization with F12 already open.
|
||||
if (immediate) options.ondevtoolopen('already-open');
|
||||
}
|
||||
detector.isSuspend = false;
|
||||
const window = {
|
||||
location: {href: '/editor.html'}, onbeforeunload: () => {},
|
||||
cppCodeStopRun: () => effects.push('stop C++'),
|
||||
loadCppProject: () => effects.push('clear C++'),
|
||||
pythonCodeStopRun: () => effects.push('stop Python'),
|
||||
loadPythonProject: () => effects.push('clear Python'),
|
||||
unityInstance: {Quit: () => effects.push('quit engine')}
|
||||
};
|
||||
const sessionStorage = {
|
||||
getItem(key) {if (brokenStorage) throw new Error('storage denied'); return storage.get(key) || null;},
|
||||
setItem(key, value) {if (brokenStorage) throw new Error('storage denied'); storage.set(key, value);}
|
||||
};
|
||||
const sandbox = {
|
||||
exports: {}, window, sessionStorage,
|
||||
localStorage: {getItem: () => '/index.html'},
|
||||
vm: {
|
||||
stopAll: () => effects.push('stop Scratch'),
|
||||
editingTarget: {blocks: {deleteAllBlocks: () => effects.push('clear Scratch')}},
|
||||
emitWorkspaceUpdate: () => effects.push('update workspace')
|
||||
},
|
||||
require(name) {
|
||||
if (name === 'disable-devtool') return detector;
|
||||
if (name === './config.js') return {disabledev: enabled};
|
||||
if (name === './devtool-control.js') return api;
|
||||
throw new Error(`Unexpected import: ${name}`);
|
||||
}
|
||||
};
|
||||
const context = vm.createContext(sandbox);
|
||||
vm.runInContext(controlCode, context, {filename: controlPath});
|
||||
const api = sandbox.exports;
|
||||
return {api, starts, effects, detector, window, context};
|
||||
}
|
||||
|
||||
// Use the actual options and destructive callback from each editor mode.
|
||||
function stageOptions(file) {
|
||||
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const ast = babel.parseSync(source, {configFile: false, babelrc: false, parserOpts: {plugins: ['jsx']}});
|
||||
let options;
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
assert.notStrictEqual(node.source.value, 'disable-devtool', `${file} bypasses shared control`);
|
||||
}
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'initDisableDevtool') options = node.arguments[0];
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit);
|
||||
else if (value && typeof value === 'object') visit(value);
|
||||
}
|
||||
}
|
||||
visit(ast);
|
||||
assert(options, `${file} must initialize through shared control`);
|
||||
// Also verify the entire JSX file remains compatible with project Babel presets.
|
||||
compile(file);
|
||||
return source.slice(options.start, options.end);
|
||||
}
|
||||
|
||||
const storage = new Map();
|
||||
test('home exposes stopDev and persists it for navigation', () => {
|
||||
const home = page(storage);
|
||||
vm.runInContext(homeCode, home.context);
|
||||
home.window.stopDev();
|
||||
assert.strictEqual(storage.get('stopDev'), '1');
|
||||
assert.strictEqual(home.detector.isSuspend, true);
|
||||
});
|
||||
|
||||
for (const mode of ['stage-unity', 'stage-python-unity', 'stage-cpp-unity']) {
|
||||
const optionSource = stageOptions(`src/components/${mode}/stage-unity.jsx`);
|
||||
test(`${mode}: home -> editor with F12 open does not start detection or clear code`, () => {
|
||||
const editor = page(storage, {immediate: true});
|
||||
const options = vm.runInContext(`(${optionSource})`, editor.context);
|
||||
editor.api.initDisableDevtool(options);
|
||||
assert.strictEqual(editor.starts.length, 0);
|
||||
assert.strictEqual(editor.detector.isSuspend, true);
|
||||
assert.strictEqual(editor.window.location.href, '/editor.html');
|
||||
assert.deepStrictEqual(editor.effects, []);
|
||||
// A refresh or another mode initialization in this tab must stay stopped.
|
||||
editor.api.initDisableDevtool(options);
|
||||
assert.strictEqual(editor.starts.length, 0);
|
||||
});
|
||||
test(`${mode}: stopDev blocks an already queued destructive callback`, () => {
|
||||
const editor = page();
|
||||
const options = vm.runInContext(`(${optionSource})`, editor.context);
|
||||
editor.api.initDisableDevtool(options);
|
||||
assert.strictEqual(editor.starts.length, 1);
|
||||
const callback = editor.starts[0];
|
||||
editor.window.stopDev();
|
||||
assert.strictEqual(callback.ignore(), true);
|
||||
callback.ondevtoolopen('queued');
|
||||
assert.strictEqual(editor.window.location.href, '/editor.html');
|
||||
assert.deepStrictEqual(editor.effects, []);
|
||||
});
|
||||
}
|
||||
|
||||
test('normal session retains default detection and redirect', () => {
|
||||
const normal = page();
|
||||
normal.api.initDisableDevtool();
|
||||
assert.strictEqual(normal.starts.length, 1);
|
||||
assert.strictEqual(normal.starts[0].ignore(), false);
|
||||
normal.starts[0].ondevtoolopen();
|
||||
assert.strictEqual(normal.window.location.href, '/index.html');
|
||||
});
|
||||
test('custom options, ignore and callback arguments remain intact', () => {
|
||||
const normal = page();
|
||||
const seen = [];
|
||||
const next = () => {};
|
||||
normal.api.initDisableDevtool({interval: 123, ignore: () => true, ondevtoolopen: (...args) => seen.push(args)});
|
||||
assert.strictEqual(normal.starts[0].interval, 123);
|
||||
assert.strictEqual(normal.starts[0].ignore(), true);
|
||||
normal.starts[0].ondevtoolopen(7, next);
|
||||
assert.deepStrictEqual(seen, [[7, next]]);
|
||||
});
|
||||
test('disabled configuration does not initialize; stopDev remains available', () => {
|
||||
const development = page(new Map(), {enabled: false});
|
||||
development.api.initDisableDevtool();
|
||||
assert.strictEqual(development.starts.length, 0);
|
||||
assert.strictEqual(typeof development.window.stopDev, 'function');
|
||||
});
|
||||
test('blocked storage still permits immediate stop on current page', () => {
|
||||
const broken = page(new Map(), {brokenStorage: true});
|
||||
broken.api.initDisableDevtool();
|
||||
broken.window.stopDev();
|
||||
assert.strictEqual(broken.detector.isSuspend, true);
|
||||
assert.strictEqual(broken.starts[0].ignore(), true);
|
||||
broken.starts[0].ondevtoolopen();
|
||||
broken.api.initDisableDevtool();
|
||||
assert.strictEqual(broken.starts.length, 1);
|
||||
assert.strictEqual(broken.window.location.href, '/editor.html');
|
||||
});
|
||||
for (const file of ['src/playground/competition.js', 'src/playground/competition_sum.js']) {
|
||||
const optionsSource = stageOptions(file);
|
||||
test(`${file}: same stopDev state is honored`, () => {
|
||||
const competition = page(storage, {immediate: true});
|
||||
competition.api.initDisableDevtool(vm.runInContext(`(${optionsSource})`, competition.context));
|
||||
assert.strictEqual(competition.starts.length, 0);
|
||||
});
|
||||
}
|
||||
console.log(`${checks} devtool regression checks passed`);
|
||||
@@ -852,7 +852,7 @@ class MenuBar extends React.Component {
|
||||
let realLvId_tmp = '';
|
||||
// 发送消息给 Unity 实例,切换关卡
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv;
|
||||
realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv;
|
||||
} else {
|
||||
realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
}
|
||||
@@ -902,7 +902,8 @@ class MenuBar extends React.Component {
|
||||
window.gamelvdataJson.gamelv,
|
||||
window.gamelvdataJson.gameid,
|
||||
window.gamelvdataJson.ismatch,
|
||||
window.gamelvdataJson.gameNumber
|
||||
window.gamelvdataJson.gameNumber,
|
||||
window.gamelvdataJson.gamemode
|
||||
) + (window.gamelvdata ? window.gamelvdata.currentlv : '');
|
||||
} else {
|
||||
realLvId_tmp = CONFIG.getRealLvId(
|
||||
|
||||
@@ -148,7 +148,7 @@ const CurrentLevelChoose = ({ onCancel }) => {
|
||||
console.warn('window.loadCppProject 未定义或不是函数');
|
||||
}
|
||||
}
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode);
|
||||
const realLvId = realLvId_tmp + cellIndex
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
|
||||
@@ -11,7 +11,6 @@ import RankBadgeGenerator from '../../playground/rank-badge-generator.js';
|
||||
// const friendToggle = new FriendToggle();
|
||||
window.RankBadgeGenerator = RankBadgeGenerator;
|
||||
window.CONFIG = CONFIG;
|
||||
import disableDevtool from 'disable-devtool';
|
||||
import { initDisableDevtool } from '../../playground/devtool-control.js';
|
||||
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
|
||||
import { debug } from 'scratch-vm/src/util/log.js';
|
||||
@@ -561,7 +560,7 @@ const UnityComponent = function (props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (CONFIG.disabledev) {
|
||||
disableDevtool({
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
@@ -1906,4 +1905,4 @@ UnityComponent.propTypes = {
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
export default UnityComponent;
|
||||
export default UnityComponent;
|
||||
|
||||
@@ -510,10 +510,57 @@ export class CppSyntaxValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// 声明写入当前作用域,赋值更新最近的已有变量;函数不继承调用者的局部变量。
|
||||
class CppVariableScope extends Map {
|
||||
constructor(parent = null) {
|
||||
super();
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
declare(name, value) {
|
||||
return super.set(name, value);
|
||||
}
|
||||
|
||||
has(name) {
|
||||
return super.has(name) || Boolean(this.parent && this.parent.has(name));
|
||||
}
|
||||
|
||||
get(name) {
|
||||
return super.has(name) ? super.get(name) : this.parent && this.parent.get(name);
|
||||
}
|
||||
|
||||
set(name, value) {
|
||||
if (!super.has(name) && this.parent && this.parent.has(name)) {
|
||||
this.parent.set(name, value);
|
||||
return this;
|
||||
}
|
||||
return super.set(name, value);
|
||||
}
|
||||
|
||||
*entries() {
|
||||
const visible = new Map(this.parent ? this.parent.entries() : []);
|
||||
for (const [name, value] of super.entries()) visible.set(name, value);
|
||||
yield* visible.entries();
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
|
||||
// return 是跨越循环/条件块的控制信号,只在所属函数边界消费。
|
||||
// 使用独立类型,避免吞掉恰好带有相同消息的真实运行错误。
|
||||
class CppReturnSignal {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class CppCodeExecutor {
|
||||
constructor(code, setHighlightedLine) {
|
||||
this.code = code;
|
||||
this.variables = new Map();
|
||||
this.globalVariables = new CppVariableScope();
|
||||
this.variables = this.globalVariables;
|
||||
this.functions = new Map(); // 存储自定义函数
|
||||
this.currentLine = 0;
|
||||
this.lines = [];
|
||||
@@ -527,7 +574,7 @@ export class CppCodeExecutor {
|
||||
try {
|
||||
this.isRunning = true;
|
||||
this.parseCode();
|
||||
await this.executeMain();
|
||||
return await this.withVariableScope(() => this.executeMain(), this.globalVariables);
|
||||
} catch (error) {
|
||||
console.error('C++执行错误:', error);
|
||||
throw error;
|
||||
@@ -544,6 +591,16 @@ export class CppCodeExecutor {
|
||||
return !this.isRunning || window.isScriptRunOver;
|
||||
}
|
||||
|
||||
async withVariableScope(action, parent = this.variables) {
|
||||
const previous = this.variables;
|
||||
this.variables = new CppVariableScope(parent);
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
this.variables = previous;
|
||||
}
|
||||
}
|
||||
|
||||
parseCode() {
|
||||
debugLog('🔍 开始解析C++代码,支持自定义函数');
|
||||
|
||||
@@ -765,12 +822,26 @@ export class CppCodeExecutor {
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i].trim();
|
||||
if (!line || line.startsWith('//') || line === '{' || line === '}' || line === 'return 0;' ||
|
||||
if (!line || line.startsWith('//') || line === '{' || line === '}' ||
|
||||
line.match(/^\s*}\s*else\s*\{\s*$/) || line.startsWith('else')) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// return 不能标记成 continue,否则循环会直接跳过它。
|
||||
if (/^return\b/.test(line)) {
|
||||
statements.push({
|
||||
type: 'control',
|
||||
subtype: 'return',
|
||||
content: line,
|
||||
lineNumber: isMainFunction ? this.getActualLineNumber(i) : i + 1,
|
||||
originalContent: line,
|
||||
contextPath: isMainFunction ? 'main' : 'function'
|
||||
});
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否是自定义函数调用(包括带括号的实参,如 f((n + 1));)
|
||||
const customCall = this.parseCustomFunctionCall(line);
|
||||
if (customCall) {
|
||||
@@ -799,8 +870,8 @@ export class CppCodeExecutor {
|
||||
statements.push(whileLoop);
|
||||
i = whileLoop.endIndex;
|
||||
} else {
|
||||
// 检查是否是break或continue或return语句
|
||||
if (line === 'break;' || line === 'continue;' || line.startsWith('return')) {
|
||||
// 检查是否是break或continue语句
|
||||
if (line === 'break;' || line === 'continue;') {
|
||||
statements.push({
|
||||
type: 'control',
|
||||
subtype: line === 'break;' ? 'break' : 'continue',
|
||||
@@ -1278,10 +1349,17 @@ export class CppCodeExecutor {
|
||||
}
|
||||
|
||||
async executeMain() {
|
||||
for (const statement of this.statements) {
|
||||
if (this.shouldStop()) break;
|
||||
await this.executeStatement(statement);
|
||||
try {
|
||||
for (const statement of this.statements) {
|
||||
if (this.shouldStop()) break;
|
||||
await this.executeStatement(statement);
|
||||
}
|
||||
} catch (signal) {
|
||||
if (!(signal instanceof CppReturnSignal)) throw signal;
|
||||
return signal.value;
|
||||
}
|
||||
// main 正常执行到末尾,相当于 return 0。
|
||||
return 0;
|
||||
}
|
||||
|
||||
async executeStatement(statement) {
|
||||
@@ -1300,10 +1378,10 @@ export class CppCodeExecutor {
|
||||
await this.executeSimpleStatement(statement.content);
|
||||
break;
|
||||
case 'for':
|
||||
await this.executeForLoop(statement);
|
||||
await this.withVariableScope(() => this.executeForLoop(statement));
|
||||
break;
|
||||
case 'if':
|
||||
await this.executeIfStatement(statement);
|
||||
await this.withVariableScope(() => this.executeIfStatement(statement));
|
||||
break;
|
||||
case 'while':
|
||||
await this.executeWhileLoop(statement);
|
||||
@@ -1312,8 +1390,11 @@ export class CppCodeExecutor {
|
||||
await this.executeFunctionCall(statement);
|
||||
break;
|
||||
case 'control':
|
||||
if (statement.content.startsWith('return')) {
|
||||
throw new Error('RETURN_INTERRUPT');
|
||||
if (statement.subtype === 'return') {
|
||||
const match = statement.content.match(/^return\b([\s\S]*?);(?:\s*\/\/.*)?$/);
|
||||
if (!match) throw new Error(`无法解析return语句: ${statement.content}`);
|
||||
const expression = match[1].trim();
|
||||
throw new CppReturnSignal(expression ? this.evaluateExpression(expression) : undefined);
|
||||
}
|
||||
// break和continue在各自的循环中处理,这里只记录
|
||||
debugLog(`🔄 控制语句: ${statement.subtype}`);
|
||||
@@ -1323,7 +1404,15 @@ export class CppCodeExecutor {
|
||||
|
||||
async executeSimpleStatement(content) {
|
||||
debugLog(`🔍 执行简单语句: ${content}`);
|
||||
|
||||
|
||||
// 声明必须留在当前作用域,不能按普通赋值覆盖外层同名变量。
|
||||
const declaration = content.match(/^\s*(int|bool|Position|string)\s+(\w+)\s*=\s*(.+);\s*$/);
|
||||
if (declaration) {
|
||||
const [, , name, value] = declaration;
|
||||
this.variables.declare(name, this.evaluateExpression(value));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是自定义函数调用(包括带括号的实参,如 f((n + 1));)
|
||||
const customCall = this.parseCustomFunctionCall(content);
|
||||
if (customCall && this.functions.has(customCall.functionName)) {
|
||||
@@ -1355,40 +1444,6 @@ export class CppCodeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// 变量声明
|
||||
if (content.includes('int ') && content.includes('=')) {
|
||||
const match = content.match(/int\s+(\w+)\s*=\s*(.+?);/);
|
||||
if (match) {
|
||||
const [, varName, value] = match;
|
||||
this.variables.set(varName, this.evaluateExpression(value));
|
||||
debugLog('声明int变量 ' + varName + ' = ' + this.variables.get(varName));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// bool类型变量声明
|
||||
if (content.includes('bool ') && content.includes('=')) {
|
||||
const match = content.match(/bool\s+(\w+)\s*=\s*(.+?);/);
|
||||
if (match) {
|
||||
const [, varName, value] = match;
|
||||
this.variables.set(varName, this.evaluateExpression(value));
|
||||
debugLog('声明bool变量 ' + varName + ' = ' + this.variables.get(varName));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Position类型变量声明
|
||||
if (content.includes('Position ') && content.includes('=')) {
|
||||
const match = content.match(/Position\s+(\w+)\s*=\s*(.+?);/);
|
||||
if (match) {
|
||||
const [, varName, value] = match;
|
||||
const position = this.evaluateExpression(value);
|
||||
this.variables.set(varName, position);
|
||||
debugLog(`声明Position变量 ${varName}:`, position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 复合赋值运算符 (+=, -=, *=, /=)
|
||||
if (content.includes('+=')) {
|
||||
const match = content.match(/(\w+)\s*\+=\s*(.+?);/);
|
||||
@@ -1553,41 +1608,46 @@ export class CppCodeExecutor {
|
||||
|
||||
let shouldContinue = false;
|
||||
|
||||
for (const bodyStatement of forStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出for循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
try {
|
||||
await this.withVariableScope(async () => {
|
||||
for (const bodyStatement of forStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出for循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
}
|
||||
|
||||
});
|
||||
} finally {
|
||||
this.currentExecutionContext = oldContext;
|
||||
}
|
||||
|
||||
this.currentExecutionContext = oldContext;
|
||||
|
||||
if (this.shouldStop()) break;
|
||||
if (this.shouldStop() || shouldBreak) break;
|
||||
|
||||
// 如果遇到continue,跳过执行增量,直接进入下次循环
|
||||
if (shouldContinue) {
|
||||
@@ -1730,43 +1790,48 @@ export class CppCodeExecutor {
|
||||
|
||||
let shouldContinue = false;
|
||||
|
||||
for (const bodyStatement of whileStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出while循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
|
||||
// 在每个语句执行后重新检查循环条件(特别重要!)
|
||||
debugLog(`🔍 语句执行后重新检查条件: ${whileStatement.condition} = ${this.evaluateCondition(whileStatement.condition)}`);
|
||||
try {
|
||||
await this.withVariableScope(async () => {
|
||||
for (const bodyStatement of whileStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出while循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
|
||||
// 在每个语句执行后重新检查循环条件(特别重要!)
|
||||
debugLog(`🔍 语句执行后重新检查条件: ${whileStatement.condition} = ${this.evaluateCondition(whileStatement.condition)}`);
|
||||
}
|
||||
|
||||
});
|
||||
} finally {
|
||||
this.currentExecutionContext = oldContext;
|
||||
}
|
||||
|
||||
this.currentExecutionContext = oldContext;
|
||||
|
||||
if (shouldContinue) {
|
||||
continue; // 跳过本次循环的剩余部分,重新开始
|
||||
}
|
||||
@@ -1804,6 +1869,11 @@ export class CppCodeExecutor {
|
||||
throw new Error(`函数调用栈溢出: ${functionName} (可能存在无限递归)`);
|
||||
}
|
||||
|
||||
// 所有实参先在调用者作用域求值,避免前一个形参覆盖后一个实参。
|
||||
const argumentValues = callArguments.map(arg => this.evaluateExpression(arg));
|
||||
const callerVariables = this.variables;
|
||||
this.variables = new CppVariableScope(this.globalVariables);
|
||||
|
||||
// 将函数调用信息压入调用栈
|
||||
this.callStack.push({
|
||||
functionName: functionName,
|
||||
@@ -1812,21 +1882,14 @@ export class CppCodeExecutor {
|
||||
});
|
||||
|
||||
try {
|
||||
// 最小改动:防递归数据污染,只备份会被覆盖的同名参数变量
|
||||
const savedParams = new Map();
|
||||
for (let i = 0; i < functionInfo.parameters.length; i++) {
|
||||
const pName = functionInfo.parameters[i].name;
|
||||
if (this.variables.has(pName)) savedParams.set(pName, this.variables.get(pName));
|
||||
}
|
||||
|
||||
// 设置函数参数变量
|
||||
for (let i = 0; i < functionInfo.parameters.length; i++) {
|
||||
const param = functionInfo.parameters[i];
|
||||
const argValue = this.evaluateExpression(callArguments[i]);
|
||||
const argValue = argumentValues[i];
|
||||
|
||||
// 类型检查和转换
|
||||
const convertedValue = this.convertArgumentToType(argValue, param.type);
|
||||
this.variables.set(param.name, convertedValue);
|
||||
this.variables.declare(param.name, convertedValue);
|
||||
|
||||
console.log(`📋 设置参数: ${param.name} (${param.type}) = ${convertedValue}`);
|
||||
}
|
||||
@@ -1856,22 +1919,14 @@ export class CppCodeExecutor {
|
||||
await this.executeStatement(adjustedStatement);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message !== 'RETURN_INTERRUPT') throw e;
|
||||
}
|
||||
|
||||
// 最小改动:仅恢复递归调用前的参数状态,保留全局变量的修改
|
||||
for (let i = 0; i < functionInfo.parameters.length; i++) {
|
||||
const pName = functionInfo.parameters[i].name;
|
||||
if (savedParams.has(pName)) {
|
||||
this.variables.set(pName, savedParams.get(pName));
|
||||
} else {
|
||||
this.variables.delete(pName);
|
||||
}
|
||||
if (!(e instanceof CppReturnSignal)) throw e;
|
||||
return e.value;
|
||||
}
|
||||
|
||||
console.log(`✅ 函数${functionName}执行完成`);
|
||||
|
||||
} finally {
|
||||
this.variables = callerVariables;
|
||||
// 从调用栈中弹出当前函数
|
||||
const callInfo = this.callStack.pop();
|
||||
const executionTime = Date.now() - callInfo.startTime;
|
||||
@@ -2386,202 +2441,162 @@ export class CppCodeExecutor {
|
||||
return position;
|
||||
}
|
||||
|
||||
// 字符串表达式
|
||||
if (expr.includes('"') || expr.includes("'")) {
|
||||
return expr.replace(/['"]/g, '');
|
||||
// 整个表达式是字符串字面量时才当字符串;含引号的 API 调用走后面的算术解析
|
||||
if ((expr.startsWith('"') && expr.endsWith('"')) ||
|
||||
(expr.startsWith("'") && expr.endsWith("'"))) {
|
||||
return expr.slice(1, -1);
|
||||
}
|
||||
|
||||
// 使用改进的表达式解析器处理复杂表达式
|
||||
return this.parseComplexExpression(expr);
|
||||
}
|
||||
|
||||
// 新增:改进的表达式解析器,支持括号嵌套和运算优先级
|
||||
// 递归下降:一元 +/- ,然后 * / % 优先于 + -
|
||||
parseComplexExpression(expr) {
|
||||
try {
|
||||
// 移除所有空格
|
||||
expr = expr.replace(/\s+/g, '');
|
||||
|
||||
// 处理括号
|
||||
while (expr.includes('(')) {
|
||||
const lastOpenParen = expr.lastIndexOf('(');
|
||||
const closeParen = expr.indexOf(')', lastOpenParen);
|
||||
|
||||
if (closeParen === -1) {
|
||||
debugLog(`⚠️ 括号不匹配: ${expr}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const innerExpr = expr.substring(lastOpenParen + 1, closeParen);
|
||||
const innerResult = this.parseComplexExpression(innerExpr);
|
||||
|
||||
expr = expr.substring(0, lastOpenParen) + innerResult + expr.substring(closeParen + 1);
|
||||
}
|
||||
|
||||
// 处理一元运算符(负号)
|
||||
expr = this.handleUnaryOperators(expr);
|
||||
|
||||
// 按正确的运算优先级解析:
|
||||
// 加法和减法是最低优先级,因此在此处最先切分。
|
||||
// 切分后的每一项将交给 parseAdditionSubtraction,在其中处理更高级别的乘除法。
|
||||
return this.parseAdditionSubtraction(expr);
|
||||
|
||||
this._exprTokens = this.lexArithmetic(expr);
|
||||
this._exprPos = 0;
|
||||
if (this._exprTokens.length === 0) return 0;
|
||||
const result = this.parseAddExpr();
|
||||
return typeof result === 'number' && !Number.isNaN(result) ? result : 0;
|
||||
} catch (error) {
|
||||
debugLog(`⚠️ 表达式解析错误: ${expr}, 错误: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理一元运算符
|
||||
handleUnaryOperators(expr) {
|
||||
// 先处理开头的负号
|
||||
if (expr.startsWith('-')) {
|
||||
const restExpr = expr.substring(1);
|
||||
if (/^\d+/.test(restExpr)) {
|
||||
// 负数
|
||||
const match = restExpr.match(/^\d+/);
|
||||
const number = match[0];
|
||||
const remaining = restExpr.substring(number.length);
|
||||
return (-parseInt(number)) + remaining;
|
||||
} else if (/^[a-zA-Z_]\w*/.test(restExpr)) {
|
||||
// 负变量
|
||||
const match = restExpr.match(/^[a-zA-Z_]\w*/);
|
||||
const varName = match[0];
|
||||
const remaining = restExpr.substring(varName.length);
|
||||
if (this.variables.has(varName)) {
|
||||
return (-this.variables.get(varName)) + remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
// 解析乘法、除法和模运算(高优先级)
|
||||
parseMultiplicationDivision(expr) {
|
||||
const tokens = this.tokenizeExpression(expr, ['*', '/', '%']);
|
||||
let result = this.evaluateToken(tokens[0]);
|
||||
|
||||
for (let i = 1; i < tokens.length; i += 2) {
|
||||
const operator = tokens[i];
|
||||
const operand = this.evaluateToken(tokens[i + 1]);
|
||||
|
||||
if (operator === '*') {
|
||||
result *= operand;
|
||||
} else if (operator === '/') {
|
||||
result = Math.floor(result / operand); // C++整数除法
|
||||
} else if (operator === '%') {
|
||||
result = result % operand; // 模运算
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 解析加法和减法(低优先级)
|
||||
parseAdditionSubtraction(expr) {
|
||||
const tokens = this.tokenizeExpressionSmart(expr, ['+', '-']);
|
||||
let result = this.evaluateToken(tokens[0]);
|
||||
|
||||
for (let i = 1; i < tokens.length; i += 2) {
|
||||
const operator = tokens[i];
|
||||
const operand = this.evaluateToken(tokens[i + 1]);
|
||||
|
||||
if (operator === '+') {
|
||||
result += operand;
|
||||
} else if (operator === '-') {
|
||||
result -= operand;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 智能分词器,考虑到负号可能是一元运算符
|
||||
tokenizeExpressionSmart(expr, operators) {
|
||||
lexArithmetic(expr) {
|
||||
const tokens = [];
|
||||
let current = '';
|
||||
let i = 0;
|
||||
|
||||
const isIdChar = (ch) => /[a-zA-Z0-9_.]/.test(ch);
|
||||
|
||||
while (i < expr.length) {
|
||||
const char = expr[i];
|
||||
|
||||
if (operators.includes(char)) {
|
||||
// 检查是否是一元负号
|
||||
if (char === '-' && (i === 0 || operators.includes(expr[i-1]) || expr[i-1] === '(')) {
|
||||
// 这是一元负号,继续收集
|
||||
current += char;
|
||||
} else {
|
||||
// 这是二元运算符
|
||||
if (current.trim()) {
|
||||
tokens.push(current.trim());
|
||||
current = '';
|
||||
}
|
||||
tokens.push(char);
|
||||
const ch = expr[i];
|
||||
if (/\s/.test(ch)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if ('+-*/%()'.includes(ch)) {
|
||||
tokens.push({ type: ch });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (/\d/.test(ch)) {
|
||||
let num = '';
|
||||
while (i < expr.length && /\d/.test(expr[i])) {
|
||||
num += expr[i++];
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
tokens.push({ type: 'num', value: parseInt(num, 10) });
|
||||
continue;
|
||||
}
|
||||
if (/[a-zA-Z_]/.test(ch)) {
|
||||
let id = '';
|
||||
while (i < expr.length && isIdChar(expr[i])) {
|
||||
id += expr[i++];
|
||||
}
|
||||
if (expr[i] === '(') {
|
||||
let depth = 0;
|
||||
let call = id;
|
||||
while (i < expr.length) {
|
||||
const c = expr[i++];
|
||||
call += c;
|
||||
if (c === '(') depth++;
|
||||
else if (c === ')') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
tokens.push({ type: 'call', value: call });
|
||||
} else {
|
||||
tokens.push({ type: 'id', value: id });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (current.trim()) {
|
||||
tokens.push(current.trim());
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// 将表达式分解为标记
|
||||
tokenizeExpression(expr, operators) {
|
||||
const tokens = [];
|
||||
let current = '';
|
||||
|
||||
for (let i = 0; i < expr.length; i++) {
|
||||
const char = expr[i];
|
||||
|
||||
if (operators.includes(char)) {
|
||||
if (current.trim()) {
|
||||
tokens.push(current.trim());
|
||||
current = '';
|
||||
}
|
||||
tokens.push(char);
|
||||
peekExprToken() {
|
||||
return this._exprTokens[this._exprPos] || { type: 'eof' };
|
||||
}
|
||||
|
||||
consumeExprToken() {
|
||||
return this._exprTokens[this._exprPos++] || { type: 'eof' };
|
||||
}
|
||||
|
||||
parseAddExpr() {
|
||||
let left = this.parseMulExpr();
|
||||
while (this.peekExprToken().type === '+' || this.peekExprToken().type === '-') {
|
||||
const op = this.consumeExprToken().type;
|
||||
const right = this.parseMulExpr();
|
||||
left = op === '+' ? left + right : left - right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
parseMulExpr() {
|
||||
let left = this.parseUnaryExpr();
|
||||
while (['*', '/', '%'].includes(this.peekExprToken().type)) {
|
||||
const op = this.consumeExprToken().type;
|
||||
const right = this.parseUnaryExpr();
|
||||
if (op === '*') {
|
||||
left *= right;
|
||||
} else if (op === '/') {
|
||||
left = right === 0 ? 0 : Math.trunc(left / right);
|
||||
} else {
|
||||
current += char;
|
||||
left %= right;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.trim()) {
|
||||
tokens.push(current.trim());
|
||||
}
|
||||
|
||||
return tokens;
|
||||
return left;
|
||||
}
|
||||
|
||||
// 计算单个标记的值
|
||||
evaluateToken(token) {
|
||||
token = token.trim();
|
||||
if (!token) return 0;
|
||||
|
||||
// 如果包含乘除法或模运算,交给乘除法解析器处理
|
||||
if (token.includes('*') || token.includes('/') || token.includes('%')) {
|
||||
return this.parseMultiplicationDivision(token);
|
||||
parseUnaryExpr() {
|
||||
const type = this.peekExprToken().type;
|
||||
if (type === '-') {
|
||||
this.consumeExprToken();
|
||||
return -this.parseUnaryExpr();
|
||||
}
|
||||
|
||||
// 布尔常量
|
||||
if (type === '+') {
|
||||
this.consumeExprToken();
|
||||
return this.parseUnaryExpr();
|
||||
}
|
||||
return this.parsePrimaryExpr();
|
||||
}
|
||||
|
||||
parsePrimaryExpr() {
|
||||
const token = this.peekExprToken();
|
||||
if (token.type === 'num') {
|
||||
this.consumeExprToken();
|
||||
return token.value;
|
||||
}
|
||||
if (token.type === 'id') {
|
||||
this.consumeExprToken();
|
||||
return this.evaluateAtom(token.value);
|
||||
}
|
||||
if (token.type === 'call') {
|
||||
this.consumeExprToken();
|
||||
return this.evaluateAtom(token.value);
|
||||
}
|
||||
if (token.type === '(') {
|
||||
this.consumeExprToken();
|
||||
const value = this.parseAddExpr();
|
||||
if (this.peekExprToken().type === ')') {
|
||||
this.consumeExprToken();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
debugLog(`⚠️ 无法解析标记: ${token.type}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
evaluateAtom(token) {
|
||||
if (token === 'true') return 1;
|
||||
if (token === 'false') return 0;
|
||||
|
||||
// 数字
|
||||
if (/^-?\d+$/.test(token)) {
|
||||
return parseInt(token);
|
||||
}
|
||||
|
||||
// 变量查找
|
||||
|
||||
if (this.variables.has(token)) {
|
||||
return this.variables.get(token);
|
||||
}
|
||||
|
||||
// 对象属性访问 (如 pos.x, pos.y, pos.z)
|
||||
|
||||
if (token.includes('.')) {
|
||||
const [objName, propName] = token.split('.');
|
||||
if (this.variables.has(objName)) {
|
||||
@@ -2591,8 +2606,7 @@ export class CppCodeExecutor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏API调用
|
||||
|
||||
if (token.includes('player_data(')) {
|
||||
const match = token.match(/player_data\s*\(\s*"(.+?)"\s*\)/);
|
||||
if (match) return window.cppGameAPI.player_data(match[1]);
|
||||
@@ -2604,7 +2618,7 @@ export class CppCodeExecutor {
|
||||
if (token.includes('vehicle_position()')) {
|
||||
return window.cppGameAPI.vehicle_position();
|
||||
}
|
||||
|
||||
|
||||
debugLog(`⚠️ 无法解析标记: ${token}`);
|
||||
return 0;
|
||||
}
|
||||
@@ -2738,7 +2752,7 @@ export const createCppExecutor = () => {
|
||||
const executor = new CppCodeExecutor(code, window.setHighlightedLine);
|
||||
this.currentExecutor = executor;
|
||||
try {
|
||||
await executor.run();
|
||||
return await executor.run();
|
||||
} finally {
|
||||
this.currentExecutor = null;
|
||||
}
|
||||
@@ -3761,4 +3775,4 @@ export const testCompleteIfElseIfElse = async () => {
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -654,7 +654,7 @@ const StageWrapperCppClang = () => {
|
||||
if (window.unityInstance) {
|
||||
window.unityInstance.SendMessage("UIMain", "SetText", window.cmd_print_text);
|
||||
}
|
||||
|
||||
|
||||
setCompilationOutput(prev => prev + textStr + "\n");
|
||||
},
|
||||
|
||||
@@ -1353,4 +1353,4 @@ const StageWrapperCppClang = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default StageWrapperCppClang;
|
||||
export default StageWrapperCppClang;
|
||||
|
||||
@@ -144,7 +144,7 @@ const CurrentLevelChoose = ({ onCancel }) => {
|
||||
// 清除所有代码
|
||||
window.loadPythonProject(`` + `\n`.repeat(50));
|
||||
}
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode);
|
||||
const realLvId = realLvId_tmp + cellIndex
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
|
||||
@@ -11,7 +11,6 @@ import RankBadgeGenerator from '../../playground/rank-badge-generator.js';
|
||||
// const friendToggle = new FriendToggle();
|
||||
window.RankBadgeGenerator = RankBadgeGenerator;
|
||||
window.CONFIG = CONFIG;
|
||||
import disableDevtool from 'disable-devtool';
|
||||
import { initDisableDevtool } from '../../playground/devtool-control.js';
|
||||
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
|
||||
import { debug } from 'scratch-vm/src/util/log.js';
|
||||
@@ -561,7 +560,7 @@ const UnityComponent = function (props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (CONFIG.disabledev) {
|
||||
disableDevtool({
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
@@ -1892,4 +1891,4 @@ UnityComponent.propTypes = {
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
export default UnityComponent;
|
||||
export default UnityComponent;
|
||||
|
||||
@@ -11,7 +11,6 @@ import RankBadgeGenerator from '../../playground/rank-badge-generator.js';
|
||||
// const friendToggle = new FriendToggle();
|
||||
window.RankBadgeGenerator = RankBadgeGenerator;
|
||||
window.CONFIG = CONFIG;
|
||||
import disableDevtool from 'disable-devtool';
|
||||
import { initDisableDevtool } from '../../playground/devtool-control.js';
|
||||
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
|
||||
import { debug } from 'scratch-vm/src/util/log.js';
|
||||
@@ -755,7 +754,7 @@ const UnityComponent = function (props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (CONFIG.disabledev) {
|
||||
disableDevtool({
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
@@ -2162,4 +2161,4 @@ UnityComponent.propTypes = {
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
export default UnityComponent;
|
||||
export default UnityComponent;
|
||||
|
||||
@@ -1325,7 +1325,7 @@
|
||||
<h2 class="cta-title">灵丌编程(001CODE)城市合伙人招募中</h2>
|
||||
</div>
|
||||
<div class="cta-grid">
|
||||
<a href="mailto:contact@001Code.com" class="contact-card">
|
||||
<a href="mailto:support@shxshf.com" class="contact-card">
|
||||
<div class="icon-box">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2"></rect>
|
||||
@@ -1334,11 +1334,11 @@
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<span class="card-label">邮箱地址</span>
|
||||
<span class="card-value">wangshenxing@shxshf.com</span>
|
||||
<span class="card-value">support@shxshf.com</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button class="contact-card" onclick="navigator.clipboard.writeText('17380126742')">
|
||||
<button class="contact-card" onclick="navigator.clipboard.writeText('400-659-1659')">
|
||||
<div class="icon-box">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@@ -1348,7 +1348,7 @@
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<span class="card-label">咨询热线</span>
|
||||
<span class="card-value">135-2066-2111</span>
|
||||
<span class="card-value">400-659-1659</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1358,7 +1358,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -613,18 +613,18 @@
|
||||
|
||||
<div class="section-card" id="ttsVoiceCard">
|
||||
<div class="section-title" style="margin-bottom:4px;">选择音色</div>
|
||||
<div class="section-sub" style="margin-bottom:10px;font-size:12px;">想用自己的声音?先在下方「自定义音色」录一段 5~10 秒</div>
|
||||
<div class="section-sub" style="margin-bottom:10px;font-size:12px;">选「我的声音」前,请先在下方录制或上传 5~10 秒参考人声</div>
|
||||
<div class="voice-grid" id="ttsVoiceGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" id="ttsMineVoiceCard">
|
||||
<div class="section-title">自定义音色(录一段你的声音)</div>
|
||||
<div class="section-sub">录一段 10 秒以内的话,AI 就能模仿你的声音说话啦!建议 5~10 秒,清晰无背景音(必填)</div>
|
||||
<div class="section-sub">仅选择「我的声音」时需要。录一段 5~10 秒清晰人声,AI 就能模仿你说话</div>
|
||||
<div class="record-box">
|
||||
<div class="record-box__icon"><img src="../images/aigc/icon-voice-mic.png" alt="" /></div>
|
||||
<div class="record-box__body">
|
||||
<div class="record-box__title">录一段你的声音</div>
|
||||
<div class="record-box__sub">请录制 5~10 秒清晰人声,完成后即可选中「我的声音」</div>
|
||||
<div class="record-box__sub">完成后即可选中「我的声音」;选其他音色无需录音</div>
|
||||
<div class="record-box__status" id="recordStatus"></div>
|
||||
</div>
|
||||
<div class="record-box__actions">
|
||||
@@ -1331,19 +1331,21 @@
|
||||
if (!selectedTtsVoiceId || !voiceOpt) {
|
||||
return fail('请选择音色', document.getElementById('ttsVoiceCard'));
|
||||
}
|
||||
// 无论选哪种音色,都必须先录制/上传参考音频
|
||||
if (!voiceBlob) {
|
||||
return fail('请先录制或上传自定义音色', mineCard);
|
||||
}
|
||||
if (!isVoiceDurationValid(voiceDurationSec)) {
|
||||
return fail(
|
||||
'自定义音色需 ' + MIN_VOICE_SECONDS + '~' + MAX_VOICE_SECONDS + ' 秒,当前约 ' +
|
||||
Math.max(0, Math.floor(Number(voiceDurationSec) || 0)) + ' 秒,请重新录制或上传',
|
||||
mineCard
|
||||
);
|
||||
}
|
||||
if (voiceBlob.size > 10 * 1024 * 1024) {
|
||||
return fail('参考音频不能超过 10MB', mineCard);
|
||||
// 只有选择「我的声音」时才必须录制/上传参考音频
|
||||
if (voiceOpt.mine) {
|
||||
if (!voiceBlob) {
|
||||
return fail('请先录制或上传自定义音色', mineCard);
|
||||
}
|
||||
if (!isVoiceDurationValid(voiceDurationSec)) {
|
||||
return fail(
|
||||
'自定义音色需 ' + MIN_VOICE_SECONDS + '~' + MAX_VOICE_SECONDS + ' 秒,当前约 ' +
|
||||
Math.max(0, Math.floor(Number(voiceDurationSec) || 0)) + ' 秒,请重新录制或上传',
|
||||
mineCard
|
||||
);
|
||||
}
|
||||
if (voiceBlob.size > 10 * 1024 * 1024) {
|
||||
return fail('参考音频不能超过 10MB', mineCard);
|
||||
}
|
||||
}
|
||||
if (!speedEl || !isFinite(speedIdx) || speedIdx < 0 || speedIdx > 7 || !speedLabels[speedIdx]) {
|
||||
return fail('请设置语速', document.getElementById('ttsSliderCard') || speedEl);
|
||||
@@ -2009,12 +2011,12 @@
|
||||
return voiceId;
|
||||
}
|
||||
|
||||
async function generateTtsAudio(text, voiceId) {
|
||||
if (!voiceId) throw new Error('缺少复刻音色,请先完成 voice-clone');
|
||||
async function generateTtsAudio(text, voiceId, modelName) {
|
||||
if (!voiceId) throw new Error('缺少音色');
|
||||
var payload = {
|
||||
text: text,
|
||||
voice: voiceId,
|
||||
model_name: TTS_MODEL,
|
||||
model_name: modelName || TTS_MODEL,
|
||||
instruction: buildTtsInstruction()
|
||||
};
|
||||
|
||||
@@ -2136,14 +2138,21 @@
|
||||
setVoicePreviewLoading('正在准备合成…');
|
||||
|
||||
try {
|
||||
// 无论选哪种音色:先 voice-clone,再 tts/generate
|
||||
btn.textContent = '复刻中…';
|
||||
setVoicePreviewLoading('正在上传参考音频并复刻音色…');
|
||||
var voiceId = await cloneVoiceFromFile(voiceBlob, voiceFileName);
|
||||
if (!voiceId) throw new Error('音色复刻失败,未返回 voice_id');
|
||||
var voiceId;
|
||||
var modelName = TTS_MODEL;
|
||||
if (voiceOpt.mine) {
|
||||
btn.textContent = '复刻中…';
|
||||
setVoicePreviewLoading('正在上传参考音频并复刻音色…');
|
||||
voiceId = await cloneVoiceFromFile(voiceBlob, voiceFileName);
|
||||
if (!voiceId) throw new Error('音色复刻失败,未返回 voice_id');
|
||||
} else {
|
||||
// 预设音色:与「声音变声」同一套 model / voice
|
||||
voiceId = CHANGE_VOICE;
|
||||
modelName = CHANGE_MODEL;
|
||||
}
|
||||
btn.textContent = '合成中…';
|
||||
setVoicePreviewLoading('正在用「' + voiceOpt.name + '」合成语音…');
|
||||
var gen = await generateTtsAudio(text, voiceId);
|
||||
var gen = await generateTtsAudio(text, voiceId, modelName);
|
||||
var meta = '语速 ' + speedLabels[params.speedIdx] +
|
||||
' · 语调 ' + pitchLabels[params.pitchIdx] +
|
||||
' · 情绪 ' + emotionLabels[params.emotionIdx];
|
||||
|
||||
@@ -1302,6 +1302,12 @@
|
||||
<span class="nav__pillAIText">灵丌AI</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nav-link-group">
|
||||
<a class="nav-link" href="/competition_list.html">赛事详情</a>
|
||||
</div>
|
||||
<div class="nav-link-group">
|
||||
<a class="nav-link" href="/aboutus.html">关于我们</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<a class="navbar-link" id="open-personal-info" href="javascript:void(0);">个人信息</a>
|
||||
@@ -1531,7 +1537,7 @@
|
||||
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
@@ -1792,6 +1798,17 @@
|
||||
return !!localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
function getExplorerRole() {
|
||||
var user = window.aiExplorerUserInfo || {};
|
||||
return String(user.role || '').trim();
|
||||
}
|
||||
|
||||
function isStudentRole() {
|
||||
var role = getExplorerRole();
|
||||
var lower = role.toLowerCase();
|
||||
return lower === 'student' || role === '学生';
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
var headers = { 'Content-Type': 'application/json' };
|
||||
var token = localStorage.getItem('access_token');
|
||||
@@ -1894,13 +1911,16 @@
|
||||
var goBtnHtml = showGoBtn
|
||||
? '<button type="button" class="classes-modal__go-btn" data-class-id="' + id + '">' + escapeHtml(actionLabel) + '</button>'
|
||||
: '';
|
||||
var countHtml = isStudentRole()
|
||||
? ''
|
||||
: '<span class="classes-modal__count">' + count + ' 人</span>';
|
||||
return '<div class="classes-modal__item" data-class-id="' + id + '">' +
|
||||
'<div class="classes-modal__main">' +
|
||||
'<div class="classes-modal__name">' + name + '</div>' +
|
||||
'<div class="classes-modal__meta">年级:' + grade + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="classes-modal__aside">' +
|
||||
'<span class="classes-modal__count">' + count + ' 人</span>' +
|
||||
countHtml +
|
||||
goBtnHtml +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<footer>
|
||||
<div class="footer-line"></div>
|
||||
<p class="footer-text">ICP备案号:<a style="color:#478ac9 ;" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a style="color:#478ac9 ;" href="https://beian.mps.gov.cn/" target="_blank">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 导入配置信息
|
||||
import CONFIG from './config.js';
|
||||
import disableDevtool from 'disable-devtool';
|
||||
import { initDisableDevtool } from './devtool-control.js';
|
||||
|
||||
|
||||
// 获取 URL 参数的函数
|
||||
@@ -166,7 +166,7 @@ function showPlatformGuide() {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (CONFIG.disabledev) {
|
||||
disableDevtool({
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
// window.location.href = '/index.html';
|
||||
// window.location.href = localStorage.getItem('returnUrl');
|
||||
@@ -3449,4 +3449,4 @@ function getLanguageName(t) {
|
||||
}
|
||||
|
||||
// 导出函数,以便其他模块可以使用
|
||||
export { getUrlParameter, fetchCompetitionInfo };
|
||||
export { getUrlParameter, fetchCompetitionInfo };
|
||||
|
||||
@@ -917,7 +917,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
<footer>
|
||||
<div class="footer-line"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import CONFIG from './config.js';
|
||||
import disableDevtool from 'disable-devtool';
|
||||
import { initDisableDevtool } from './devtool-control.js';
|
||||
let gamelvdata = {};
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (CONFIG.disabledev) {
|
||||
disableDevtool({
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
},
|
||||
interval: 500,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// config.js
|
||||
|
||||
// const CONFIG = {
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// version : "2025010301",
|
||||
// unitycdndir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826',
|
||||
// unitycdnbuilddir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826/Build',
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
// // 可以根据需要添加其他配置
|
||||
|
||||
// disabledev: false,
|
||||
// disabledev: true,
|
||||
// usecdn :true, //资源走CDN
|
||||
// lvlock :false, //关卡5关解锁,下一关加锁
|
||||
// lvlock_normal_learn :true, //常规关起锁
|
||||
@@ -78,12 +78,12 @@
|
||||
|
||||
|
||||
// const CONFIG = {
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// version : "2025011001",
|
||||
// unitycdndir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826',
|
||||
// unitycdnbuilddir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826/Build',
|
||||
@@ -91,8 +91,8 @@
|
||||
|
||||
// // 可以根据需要添加其他配置
|
||||
|
||||
// // disabledev: false,
|
||||
// disabledev: false,
|
||||
// // disabledev: true,
|
||||
// disabledev: true,
|
||||
// usecdn :true, //资源走CDN
|
||||
// lvlock :false, //关卡5关解锁,下一关加锁
|
||||
// lvlock_normal_learn : !(typeof localStorage !== 'undefined' &&
|
||||
@@ -157,12 +157,12 @@
|
||||
|
||||
|
||||
const CONFIG = {
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
DataServerBaseUrl: "https://service.001code.com",
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
|
||||
version: "2026071701",
|
||||
/** OSS 真实地址(生产环境浏览器直连) */
|
||||
@@ -183,7 +183,7 @@ const CONFIG = {
|
||||
|
||||
|
||||
|
||||
disabledev: false,
|
||||
disabledev: true,
|
||||
usecdn: false,
|
||||
lvlock: false,
|
||||
lvlock_normal_learn: !(typeof localStorage !== 'undefined' &&
|
||||
|
||||
@@ -286,10 +286,20 @@
|
||||
}
|
||||
.toc-item__title {
|
||||
font-size: 13px; font-weight: 600; color: #0b1a4a; line-height: 1.35;
|
||||
display: flex; align-items: flex-start; gap: 6px;
|
||||
}
|
||||
.toc-item__num {
|
||||
flex-shrink: 0; min-width: 1.15em;
|
||||
font-variant-numeric: tabular-nums; font-weight: 800; color: #0003FF;
|
||||
}
|
||||
.toc-item__name {
|
||||
flex: 1; min-width: 0;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.toc-item.is-active .toc-item__title { color: #0003FF; }
|
||||
.toc-item.is-active .toc-item__num { color: #0003FF; }
|
||||
.toc-item.is-locked .toc-item__title { color: #64748b; font-weight: 500; }
|
||||
.toc-item.is-locked .toc-item__num { color: #94a3b8; font-weight: 700; }
|
||||
.toc-item__meta {
|
||||
margin-top: 2px; font-size: 11px; color: #94a3b8;
|
||||
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
||||
@@ -960,9 +970,13 @@
|
||||
function mapCourseware(cw, source, groupTitle) {
|
||||
// 预览与去上课都尊重接口 is_locked;进度条仅在有 class_id 时展示,互不影响
|
||||
var locked = cw.is_locked === true;
|
||||
var sortOrder = Number(cw.sort_order);
|
||||
if (!isFinite(sortOrder)) sortOrder = 0;
|
||||
return {
|
||||
id: cw.id,
|
||||
title: cw.name || '未命名课件',
|
||||
displayIndex: sortOrder + 1,
|
||||
sortOrder: sortOrder,
|
||||
type: String(cw.format || 'html').toLowerCase(),
|
||||
typeLabel: cw.format_display || cw.format || '课件',
|
||||
duration: cw.duration_minutes ? (cw.duration_minutes + '分钟') : '',
|
||||
@@ -980,62 +994,91 @@
|
||||
return (topics || []).filter(function(t) { return !t.isLocked; }).length;
|
||||
}
|
||||
|
||||
function buildChapters(category, source) {
|
||||
var coursewares = Array.isArray(category.coursewares) ? category.coursewares.slice() : [];
|
||||
coursewares.sort(function(a, b) {
|
||||
function sortCoursewares(coursewares) {
|
||||
return (coursewares || []).slice().sort(function(a, b) {
|
||||
return (Number(a.sort_order) || 0) - (Number(b.sort_order) || 0);
|
||||
});
|
||||
var groups = Array.isArray(category.groups) ? category.groups.slice() : [];
|
||||
var chapters = [];
|
||||
}
|
||||
|
||||
if (groups.length) {
|
||||
chapters = groups.map(function(g, idx) {
|
||||
var gName = g.group_name || ('分组' + (idx + 1));
|
||||
var topics = coursewares.filter(function(cw) {
|
||||
return String(cw.group_name || '') === String(g.group_name || '');
|
||||
}).map(function(cw) { return mapCourseware(cw, source, gName); });
|
||||
// 始终按课件实际锁定状态统计,避免 groups.unlocked_count 与课件列表不一致
|
||||
return {
|
||||
id: idx + 1,
|
||||
title: gName,
|
||||
count: topics.length || Number(g.course_count) || 0,
|
||||
unlocked: countUnlockedTopics(topics),
|
||||
topics: topics
|
||||
};
|
||||
function collectCoursewares(container) {
|
||||
if (!container) return [];
|
||||
if (Array.isArray(container.coursewares) && container.coursewares.length) {
|
||||
return container.coursewares.slice();
|
||||
}
|
||||
var list = [];
|
||||
if (Array.isArray(container.groups)) {
|
||||
container.groups.forEach(function(g) {
|
||||
if (Array.isArray(g.coursewares)) {
|
||||
g.coursewares.forEach(function(cw) { list.push(cw); });
|
||||
}
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
var groupedNames = {};
|
||||
groups.forEach(function(g) { groupedNames[String(g.group_name || '')] = true; });
|
||||
var orphans = coursewares.filter(function(cw) {
|
||||
var gn = String(cw.group_name || '');
|
||||
return !Object.prototype.hasOwnProperty.call(groupedNames, gn);
|
||||
function chapterFromCoursewares(coursewares, opts) {
|
||||
var title = opts.title || '课件列表';
|
||||
var topics = sortCoursewares(coursewares).map(function(cw) {
|
||||
return mapCourseware(cw, opts.source, title);
|
||||
});
|
||||
return {
|
||||
id: opts.id,
|
||||
indexLabel: opts.indexLabel != null ? opts.indexLabel : opts.id,
|
||||
title: title,
|
||||
intro: opts.intro || '',
|
||||
count: topics.length,
|
||||
unlocked: countUnlockedTopics(topics),
|
||||
topics: topics
|
||||
};
|
||||
}
|
||||
|
||||
function buildChapters(category, source) {
|
||||
var semesters = Array.isArray(category.semesters) ? category.semesters.slice() : [];
|
||||
if (semesters.length) {
|
||||
semesters.sort(function(a, b) {
|
||||
return (Number(a.sort_order) || 0) - (Number(b.sort_order) || 0);
|
||||
});
|
||||
if (orphans.length) {
|
||||
var orphanTopics = orphans.map(function(cw) {
|
||||
return mapCourseware(cw, source, '其他课件');
|
||||
return semesters.map(function(sem, idx) {
|
||||
var title = String(sem.name || '').trim() || ('第' + (idx + 1) + '学期');
|
||||
return chapterFromCoursewares(collectCoursewares(sem), {
|
||||
source: source,
|
||||
id: sem.id != null ? sem.id : (idx + 1),
|
||||
indexLabel: Number(sem.sort_order) || (idx + 1),
|
||||
title: title,
|
||||
intro: sem.intro || ''
|
||||
});
|
||||
chapters.push({
|
||||
id: chapters.length + 1,
|
||||
title: '其他课件',
|
||||
count: orphanTopics.length,
|
||||
unlocked: countUnlockedTopics(orphanTopics),
|
||||
topics: orphanTopics
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var topics = coursewares.map(function(cw) {
|
||||
return mapCourseware(cw, source, category.name || courseName);
|
||||
});
|
||||
chapters = [{
|
||||
id: 1,
|
||||
title: category.name || courseName || '课件列表',
|
||||
count: topics.length,
|
||||
unlocked: countUnlockedTopics(topics),
|
||||
topics: topics
|
||||
}];
|
||||
}
|
||||
|
||||
return chapters;
|
||||
var coursewares = collectCoursewares(category);
|
||||
var groups = Array.isArray(category.groups) ? category.groups.slice() : [];
|
||||
var namedGroups = groups.filter(function(g) {
|
||||
return String(g.group_name || '').trim();
|
||||
});
|
||||
|
||||
if (namedGroups.length) {
|
||||
return namedGroups.map(function(g, idx) {
|
||||
var gName = String(g.group_name || '').trim();
|
||||
var list = Array.isArray(g.coursewares) && g.coursewares.length
|
||||
? g.coursewares
|
||||
: coursewares.filter(function(cw) {
|
||||
return String(cw.group_name || '') === String(g.group_name || '');
|
||||
});
|
||||
return chapterFromCoursewares(list, {
|
||||
source: source,
|
||||
id: idx + 1,
|
||||
indexLabel: idx + 1,
|
||||
title: gName
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return [chapterFromCoursewares(coursewares, {
|
||||
source: source,
|
||||
id: 1,
|
||||
indexLabel: 1,
|
||||
title: category.name || courseName || '课件列表'
|
||||
})];
|
||||
}
|
||||
|
||||
function flattenLessons(chapters) {
|
||||
@@ -1093,7 +1136,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
stageTitle.textContent = lesson.title;
|
||||
stageTitle.textContent = (lesson.displayIndex != null ? (lesson.displayIndex + '. ') : '') + lesson.title;
|
||||
var metaParts = [];
|
||||
if (lesson.groupTitle) metaParts.push(lesson.groupTitle);
|
||||
if (lesson.typeLabel) metaParts.push(lesson.typeLabel);
|
||||
@@ -1143,6 +1186,12 @@
|
||||
document.getElementById('stageFrame').removeAttribute('src');
|
||||
}
|
||||
|
||||
function resetLockControl(btn) {
|
||||
if (!btn) return;
|
||||
btn.classList.remove('is-loading');
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
function syncLockedPanel(lesson) {
|
||||
var desc = document.getElementById('stageLockedDesc');
|
||||
var unlockBtn = document.getElementById('stageUnlockBtn');
|
||||
@@ -1153,6 +1202,8 @@
|
||||
: '请联系老师解锁相关内容。';
|
||||
}
|
||||
if (!unlockBtn) return;
|
||||
// 大按钮是常驻 DOM,上次解锁会留下 disabled,切到下一课未解锁课件时必须复位
|
||||
resetLockControl(unlockBtn);
|
||||
if (teacherCanUnlock) {
|
||||
unlockBtn.classList.add('is-visible');
|
||||
unlockBtn.setAttribute('data-id', String(lesson.id));
|
||||
@@ -1328,15 +1379,16 @@
|
||||
var keepLessonId = opts.keepLessonId != null ? opts.keepLessonId : currentLessonId;
|
||||
|
||||
listEl.innerHTML = chaptersCache.map(function(ch, idx) {
|
||||
var allUnlocked = ch.topics.length > 0 && ch.topics.every(function(t) { return !t.isLocked; });
|
||||
var topics = ch.topics || [];
|
||||
var allUnlocked = topics.length > 0 && topics.every(function(t) { return !t.isLocked; });
|
||||
var isOpen = opts.preserveOpen
|
||||
? openGroupIds.indexOf(String(ch.id)) !== -1
|
||||
: idx === 0;
|
||||
if (!opts.preserveOpen && keepLessonId) {
|
||||
var hasKeep = ch.topics.some(function(t) { return String(t.id) === String(keepLessonId); });
|
||||
var hasKeep = topics.some(function(t) { return String(t.id) === String(keepLessonId); });
|
||||
if (hasKeep) isOpen = true;
|
||||
}
|
||||
var items = ch.topics.map(function(t) {
|
||||
var items = topics.map(function(t) {
|
||||
var tagClass = 'toc-item__tag--' + (t.status || 'doing');
|
||||
var meta = escapeHtml(t.typeLabel || t.type || '课件');
|
||||
if (t.duration) meta += ' · ' + escapeHtml(t.duration);
|
||||
@@ -1354,7 +1406,10 @@
|
||||
return '<div class="' + itemClass + '" data-id="' + t.id + '" role="button" tabindex="0"' + itemTitle + '>' +
|
||||
'<div class="toc-item__icon"><img src="../images/aigc/icon_book.png" alt="" /></div>' +
|
||||
'<div class="toc-item__body">' +
|
||||
'<div class="toc-item__title">' + escapeHtml(t.title) + '</div>' +
|
||||
'<div class="toc-item__title">' +
|
||||
'<span class="toc-item__num">' + escapeHtml(String(t.displayIndex != null ? t.displayIndex : '')) + '</span>' +
|
||||
'<span class="toc-item__name">' + escapeHtml(t.title) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="toc-item__meta">' +
|
||||
'<span>' + meta + '</span>' +
|
||||
'<span class="toc-item__tag ' + tagClass + '">' + escapeHtml(statusLabels[t.status] || '') + '</span>' +
|
||||
@@ -1370,7 +1425,7 @@
|
||||
: '';
|
||||
return '<div class="toc-group' + (isOpen ? ' is-open' : '') + '" data-group-id="' + ch.id + '">' +
|
||||
'<button type="button" class="toc-group__head" data-toggle-group="' + ch.id + '">' +
|
||||
'<span class="toc-group__idx' + (allUnlocked ? ' is-done' : '') + '">' + (allUnlocked ? '✓' : ch.id) + '</span>' +
|
||||
'<span class="toc-group__idx' + (allUnlocked ? ' is-done' : '') + '">' + (allUnlocked ? '✓' : escapeHtml(String(ch.indexLabel || ch.id))) + '</span>' +
|
||||
'<span class="toc-group__meta">' +
|
||||
'<div class="toc-group__name">' + escapeHtml(ch.title) + '</div>' +
|
||||
unlockCountHtml +
|
||||
@@ -1471,10 +1526,8 @@
|
||||
} catch (error) {
|
||||
console.error('[course-detail] toggle lock failed', error);
|
||||
showToast((error && error.message) || '锁定状态切换失败', 'error');
|
||||
if (btn) {
|
||||
btn.classList.remove('is-loading');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} finally {
|
||||
resetLockControl(btn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1500,7 +1553,10 @@
|
||||
if (!response.ok || (result && result.success === false)) {
|
||||
throw new Error((result && result.message) || ('HTTP ' + response.status));
|
||||
}
|
||||
var data = (result && result.data) || {};
|
||||
var data = result || {};
|
||||
if (data.data && (Array.isArray(data.data.categories) || data.data.source)) {
|
||||
data = data.data;
|
||||
}
|
||||
courseSource = data.source || (classId ? 'class' : 'all');
|
||||
var categories = Array.isArray(data.categories) ? data.categories : [];
|
||||
var category = categories[0] || null;
|
||||
|
||||
@@ -2,8 +2,10 @@ import disableDevtool from 'disable-devtool';
|
||||
import CONFIG from './config.js';
|
||||
|
||||
const STOP_DEV_KEY = 'stopDev';
|
||||
let stoppedForPage = false;
|
||||
|
||||
function isStopped() {
|
||||
if (stoppedForPage) return true;
|
||||
try {
|
||||
return sessionStorage.getItem(STOP_DEV_KEY) === '1';
|
||||
} catch (e) {
|
||||
@@ -16,6 +18,8 @@ function isStopped() {
|
||||
* 可在控制台直接执行:stopDev()
|
||||
*/
|
||||
export function stopDev() {
|
||||
// 即使浏览器拒绝写入 sessionStorage,当前页面仍应立即停止检测。
|
||||
stoppedForPage = true;
|
||||
try {
|
||||
sessionStorage.setItem(STOP_DEV_KEY, '1');
|
||||
} catch (e) {
|
||||
@@ -29,6 +33,8 @@ export function initDisableDevtool(options = {}) {
|
||||
|
||||
if (isStopped()) {
|
||||
disableDevtool.isSuspend = true;
|
||||
// F12 已打开时,库可能在初始化期间立即触发检测,不能继续初始化。
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CONFIG.disabledev) {
|
||||
@@ -36,14 +42,19 @@ export function initDisableDevtool(options = {}) {
|
||||
}
|
||||
|
||||
const userIgnore = options.ignore;
|
||||
const onDevtoolOpen = options.ondevtoolopen || (() => {
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
});
|
||||
disableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
},
|
||||
interval: 500,
|
||||
clearLog: true,
|
||||
disableMenu: true,
|
||||
...options,
|
||||
ondevtoolopen: (...args) => {
|
||||
// stopDev() 之后,已排队的检测回调也不能清空代码或跳转。
|
||||
if (isStopped()) return;
|
||||
return onDevtoolOpen(...args);
|
||||
},
|
||||
ignore: () => {
|
||||
if (isStopped()) {
|
||||
return true;
|
||||
|
||||
@@ -1493,13 +1493,6 @@ top: 2032px; */
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
// 首页默认清除登录态,避免访客页残留 token
|
||||
try {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
} catch (e) {}
|
||||
</script>
|
||||
<header class="nav">
|
||||
<div class="nav__inner">
|
||||
<img class="nav__logo" src="../images/logo.svg" alt="001CODE" />
|
||||
@@ -1766,7 +1759,7 @@ top: 2032px; */
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<footer class="site-footer">
|
||||
<div class="footer-divider"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p></p>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -94,8 +94,7 @@
|
||||
<div class="footer-inner">
|
||||
<div class="footer-text" ">
|
||||
ICP备案号:<a style=" color: #387cbd;" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">沪ICP备2026042325号</a>
|
||||
<!-- <span> 公安备案号:<a style=" color: #387cbd;" href="https://beian.mps.gov.cn/"
|
||||
target="_blank">川公网安备51011202000980号</a></span> -->
|
||||
<span>公安备案号:<a style="color:#387cbd;" href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a></span>
|
||||
<span>© 灵丌编程(001CODE.COM)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,8 +108,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const levelInfo = {
|
||||
1: { title: "Level A <span style='font-size: 0.7em'>丝路青年</span>", sub: "编程基础,循环入门", cover: 1 },
|
||||
2: { title: "Level B <span style='font-size: 0.7em'>丝路青年</span>", sub: "变量入门,循环进阶,变量进阶", cover: 1 },
|
||||
3: { title: "Level C <span style='font-size: 0.7em'>红色之旅</span>", sub: "变量进阶,单条件分支入门,双条件分支入门,复杂路线规划", cover: 2 },
|
||||
4: { title: "Level D <span style='font-size: 0.7em'>红色之旅</span>", sub: "条件分支进阶,循环嵌套入门,循环嵌套进阶,变量、循环与条件综合应用", cover: 2 },
|
||||
3: { title: "Level C <span style='font-size: 0.7em'>国宝秘境</span>", sub: "变量进阶,单条件分支入门,双条件分支入门,复杂路线规划", cover: 4 },
|
||||
4: { title: "Level D <span style='font-size: 0.7em'>国宝秘境</span>", sub: "条件分支进阶,循环嵌套入门,循环嵌套进阶,变量、循环与条件综合应用", cover: 4 },
|
||||
5: { title: "Level E <span style='font-size: 0.7em'>数智时代</span>", sub: "循环嵌套进阶,无参数函数入门,单参数函数入门", cover: 3 },
|
||||
6: { title: "Level F <span style='font-size: 0.7em'>数智时代</span>", sub: "综合运用与项目实战", cover: 3 }
|
||||
};
|
||||
|
||||
@@ -361,7 +361,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
<div class="footer-inner">
|
||||
<div class="footer-text">
|
||||
ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank">川公网安备51011202000980号</a> --> © 灵丌编程(001CODE.COM)
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a> © 灵丌编程(001CODE.COM)
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
BIN
scratch-gui/static/images/gongan-beian.png
Normal file
BIN
scratch-gui/static/images/gongan-beian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user