@macle-community/simulator-web 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -2
- package/bin/macle-web-simulator.js +108 -0
- package/package.json +37 -4
- package/runtime/framework/service/service.js +1 -0
- package/runtime/framework/view/view.js +1 -0
- package/runtime/remoteDebug/adapter.js +1 -0
- package/runtime/remoteDebug/service.html +16 -0
- package/runtime/remoteDebug/sub-packages.js +1 -0
- package/runtime/simulator/config/config.json +40 -0
- package/runtime/simulator/css/common.css +945 -0
- package/runtime/simulator/index.html +95 -0
- package/runtime/simulator/mockApi.json +1 -0
- package/runtime/simulator/native/native.js +1 -0
- package/runtime/simulator/public/default-user-avatar@128w.jpg +0 -0
- package/runtime/simulator/service/app.js +1 -0
- package/runtime/simulator/service/service.html +14 -0
- package/runtime/simulator/service/sync-api.js +1 -0
- package/runtime/simulator/version.json +1 -0
- package/runtime/simulator/view/webview.js +1 -0
- package/src/compiler.js +117 -0
- package/src/mock-backend.js +225 -0
- package/src/project-parser.js +87 -0
- package/src/server.js +334 -0
- package/src/watcher.js +76 -0
- package/web/index.html +234 -0
- package/web/qrcode.js +1145 -0
- package/web/simulator.css +526 -0
- package/web/simulator.js +510 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const url = require('url');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { parseProject } = require('./project-parser');
|
|
7
|
+
const { compileProject, findMacleCompiler } = require('./compiler');
|
|
8
|
+
const { createMockBackend } = require('./mock-backend');
|
|
9
|
+
const { LiveReloadManager } = require('./watcher');
|
|
10
|
+
|
|
11
|
+
const MIME_TYPES = {
|
|
12
|
+
'.html': 'text/html; charset=utf-8',
|
|
13
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
14
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
15
|
+
'.css': 'text/css; charset=utf-8',
|
|
16
|
+
'.json': 'application/json; charset=utf-8',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.jpg': 'image/jpeg',
|
|
19
|
+
'.jpeg': 'image/jpeg',
|
|
20
|
+
'.gif': 'image/gif',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.woff': 'font/woff',
|
|
24
|
+
'.woff2': 'font/woff2',
|
|
25
|
+
'.ttf': 'font/ttf',
|
|
26
|
+
'.otf': 'font/otf'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class MacleWebSimulatorServer {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.port = options.port || 30001;
|
|
32
|
+
this.host = options.host || '0.0.0.0';
|
|
33
|
+
this.sourceDir = options.source ? path.resolve(options.source) : null;
|
|
34
|
+
this.runtimeDir = path.resolve(__dirname, '../runtime');
|
|
35
|
+
this.webDir = path.resolve(__dirname, '../web');
|
|
36
|
+
this.projectInfo = null;
|
|
37
|
+
this.appDir = null;
|
|
38
|
+
this.mockBackend = null;
|
|
39
|
+
this.liveReload = new LiveReloadManager();
|
|
40
|
+
this.server = null;
|
|
41
|
+
this.isWatching = !!options.watch;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async init() {
|
|
45
|
+
// If no source path provided, search for default sample-project or opendoc
|
|
46
|
+
if (!this.sourceDir) {
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.resolve(__dirname, '../../sample-project/dist'),
|
|
49
|
+
path.resolve(__dirname, '../../sample-project'),
|
|
50
|
+
path.resolve(__dirname, '../../opendoc/miniapp-taro/dist'),
|
|
51
|
+
path.resolve(__dirname, '../../opendoc/miniapp')
|
|
52
|
+
];
|
|
53
|
+
this.sourceDir = candidates.find(c => fs.existsSync(c)) || path.resolve('.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse project
|
|
57
|
+
this.projectInfo = parseProject(this.sourceDir);
|
|
58
|
+
const projectName = this.projectInfo.projectName;
|
|
59
|
+
|
|
60
|
+
// Compile if necessary
|
|
61
|
+
const compileRes = await compileProject(this.sourceDir, projectName);
|
|
62
|
+
this.appDir = compileRes.outDir;
|
|
63
|
+
|
|
64
|
+
// Initialize mock backend for this app
|
|
65
|
+
this.mockBackend = createMockBackend(this.appDir);
|
|
66
|
+
|
|
67
|
+
// Setup watcher if requested
|
|
68
|
+
if (this.isWatching) {
|
|
69
|
+
this.liveReload.watchDirectory(this.sourceDir, async () => {
|
|
70
|
+
await compileProject(this.sourceDir, projectName);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
start() {
|
|
78
|
+
this.server = http.createServer((req, res) => {
|
|
79
|
+
this.handleRequest(req, res);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
this.server.listen(this.port, this.host, () => {
|
|
84
|
+
const localUrl = `http://localhost:${this.port}`;
|
|
85
|
+
const networkIp = getNetworkIp();
|
|
86
|
+
const networkUrl = networkIp ? `http://${networkIp}:${this.port}` : null;
|
|
87
|
+
|
|
88
|
+
console.log('\n======================================================');
|
|
89
|
+
console.log(' 🚀 Macle Web Simulator (Standalone Edition)');
|
|
90
|
+
console.log('======================================================');
|
|
91
|
+
console.log(` Project: ${this.projectInfo.projectName}`);
|
|
92
|
+
console.log(` Source: ${this.sourceDir}`);
|
|
93
|
+
console.log(` App Target: ${this.appDir}`);
|
|
94
|
+
console.log(` Local URL: ${localUrl}`);
|
|
95
|
+
if (networkUrl) {
|
|
96
|
+
console.log(` Network URL: ${networkUrl} (Scan on mobile)`);
|
|
97
|
+
}
|
|
98
|
+
console.log(` Workbench: ${localUrl}/`);
|
|
99
|
+
console.log(` Dual-Thread: ${localUrl}/dist/root/debug/simulator/index.html`);
|
|
100
|
+
console.log('======================================================\n');
|
|
101
|
+
|
|
102
|
+
resolve({ localUrl, networkUrl, port: this.port });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this.server.on('error', (err) => {
|
|
106
|
+
if (err.code === 'EADDRINUSE') {
|
|
107
|
+
console.warn(`[Simulator] Port ${this.port} is in use, trying ${this.port + 1}...`);
|
|
108
|
+
this.port++;
|
|
109
|
+
this.server.listen(this.port, this.host);
|
|
110
|
+
} else {
|
|
111
|
+
reject(err);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
handleRequest(req, res) {
|
|
118
|
+
// Add CORS & iFrame permissions headers to all responses
|
|
119
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
120
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
121
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
122
|
+
res.setHeader('X-Frame-Options', 'ALLOWALL');
|
|
123
|
+
|
|
124
|
+
if (req.method === 'OPTIONS') {
|
|
125
|
+
res.writeHead(204);
|
|
126
|
+
return res.end();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parsedUrl = url.parse(req.url, true);
|
|
130
|
+
const reqPath = parsedUrl.pathname;
|
|
131
|
+
|
|
132
|
+
// 1. API: Live Reload SSE
|
|
133
|
+
if (reqPath === '/api/live-reload') {
|
|
134
|
+
return this.liveReload.addClient(res);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. API: Project Info
|
|
138
|
+
if (reqPath === '/api/project-info') {
|
|
139
|
+
const info = {
|
|
140
|
+
projectName: this.projectInfo.projectName,
|
|
141
|
+
sourceDir: this.sourceDir,
|
|
142
|
+
appDir: this.appDir,
|
|
143
|
+
pages: this.projectInfo.pages,
|
|
144
|
+
entryPagePath: this.projectInfo.entryPagePath,
|
|
145
|
+
window: this.projectInfo.window,
|
|
146
|
+
tabBar: this.projectInfo.tabBar,
|
|
147
|
+
simulatorUrl: `/dist/root/debug/simulator/index.html`,
|
|
148
|
+
engineVersion: '26.6.0-LG0001',
|
|
149
|
+
runtimeVersion: '100.4.2'
|
|
150
|
+
};
|
|
151
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
152
|
+
return res.end(JSON.stringify(info));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 3. API: Recompile
|
|
156
|
+
if (reqPath === '/api/recompile' && req.method === 'POST') {
|
|
157
|
+
compileProject(this.sourceDir, this.projectInfo.projectName).then(result => {
|
|
158
|
+
this.liveReload.notifyReload('manual-recompile');
|
|
159
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
160
|
+
res.end(JSON.stringify(result));
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 4. MCSS Native Mock Backend (/mcss/simulate/*)
|
|
166
|
+
if (reqPath.startsWith('/mcss/simulate')) {
|
|
167
|
+
return this.handleMockBackend(req, res, parsedUrl);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 5. Web Simulator Workbench Shell (/ and /__web)
|
|
171
|
+
if (reqPath === '/' || reqPath === '/__web' || reqPath === '/index.html') {
|
|
172
|
+
return this.serveStaticFile(path.join(this.webDir, 'index.html'), res);
|
|
173
|
+
}
|
|
174
|
+
if (reqPath === '/simulator.css') {
|
|
175
|
+
return this.serveStaticFile(path.join(this.webDir, 'simulator.css'), res);
|
|
176
|
+
}
|
|
177
|
+
if (reqPath === '/simulator.js') {
|
|
178
|
+
return this.serveStaticFile(path.join(this.webDir, 'simulator.js'), res);
|
|
179
|
+
}
|
|
180
|
+
if (reqPath === '/qrcode.js') {
|
|
181
|
+
return this.serveStaticFile(path.join(this.webDir, 'qrcode.js'), res);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 6. Dual-thread Macle Runtime Routing:
|
|
185
|
+
// Resolve:
|
|
186
|
+
// - /dist/root/debug/framework/... or /framework/... -> runtimeDir/framework/...
|
|
187
|
+
// - /dist/root/debug/simulator/... or /simulator/... -> runtimeDir/simulator/...
|
|
188
|
+
// - /dist/root/debug/app/... or /app/... -> appDir/...
|
|
189
|
+
let diskPath = this.resolveRuntimePath(reqPath);
|
|
190
|
+
if (diskPath && fs.existsSync(diskPath)) {
|
|
191
|
+
const stat = fs.statSync(diskPath);
|
|
192
|
+
if (stat.isDirectory()) {
|
|
193
|
+
diskPath = path.join(diskPath, 'index.html');
|
|
194
|
+
}
|
|
195
|
+
if (fs.existsSync(diskPath)) {
|
|
196
|
+
return this.serveStaticFile(diskPath, res);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 7. Check if file exists inside appDir directly (e.g. static assets, images)
|
|
201
|
+
const directAppPath = path.join(this.appDir, reqPath.replace(/^\//, ''));
|
|
202
|
+
if (fs.existsSync(directAppPath) && fs.statSync(directAppPath).isFile()) {
|
|
203
|
+
return this.serveStaticFile(directAppPath, res);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 8. 404 Not Found
|
|
207
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
208
|
+
res.end(`404 Not Found: ${reqPath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resolveRuntimePath(reqPath) {
|
|
212
|
+
// Strip project name or /dist prefix if present:
|
|
213
|
+
// e.g. /dist/root/debug/framework/... or /miniapp/root/debug/framework/... -> /root/debug/framework/...
|
|
214
|
+
const normalized = reqPath.replace(/^\/(dist|[a-zA-Z0-9_-]+)\/root\/debug\//, '/');
|
|
215
|
+
|
|
216
|
+
if (normalized.startsWith('/framework/')) {
|
|
217
|
+
const sub = normalized.replace(/^\/framework\//, '');
|
|
218
|
+
return path.join(this.runtimeDir, 'framework', sub);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (normalized.startsWith('/simulator/')) {
|
|
222
|
+
const sub = normalized.replace(/^\/simulator\//, '');
|
|
223
|
+
return path.join(this.runtimeDir, 'simulator', sub);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (normalized.startsWith('/remoteDebug/')) {
|
|
227
|
+
const sub = normalized.replace(/^\/remoteDebug\//, '');
|
|
228
|
+
return path.join(this.runtimeDir, 'remoteDebug', sub);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (normalized.startsWith('/app/')) {
|
|
232
|
+
const sub = normalized.replace(/^\/app\//, '');
|
|
233
|
+
return path.join(this.appDir, sub);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Fallbacks for direct relative references
|
|
237
|
+
if (reqPath.startsWith('/root/debug/framework/')) {
|
|
238
|
+
return path.join(this.runtimeDir, 'framework', reqPath.replace('/root/debug/framework/', ''));
|
|
239
|
+
}
|
|
240
|
+
if (reqPath.startsWith('/root/debug/simulator/')) {
|
|
241
|
+
return path.join(this.runtimeDir, 'simulator', reqPath.replace('/root/debug/simulator/', ''));
|
|
242
|
+
}
|
|
243
|
+
if (reqPath.startsWith('/root/debug/app/')) {
|
|
244
|
+
return path.join(this.appDir, reqPath.replace('/root/debug/app/', ''));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
serveStaticFile(filePath, res) {
|
|
251
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
252
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
253
|
+
|
|
254
|
+
fs.readFile(filePath, (err, data) => {
|
|
255
|
+
if (err) {
|
|
256
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
257
|
+
return res.end('Internal Server Error: ' + err.message);
|
|
258
|
+
}
|
|
259
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
260
|
+
res.end(data);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
handleMockBackend(req, res, parsedUrl) {
|
|
265
|
+
const subRoute = parsedUrl.pathname.replace(/^\/mcss\/simulate/, '');
|
|
266
|
+
|
|
267
|
+
if (req.method === 'POST') {
|
|
268
|
+
let bodyStr = '';
|
|
269
|
+
req.on('data', chunk => bodyStr += chunk);
|
|
270
|
+
req.on('end', () => {
|
|
271
|
+
let body = {};
|
|
272
|
+
try {
|
|
273
|
+
body = JSON.parse(bodyStr);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
body = { data: bodyStr };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (subRoute === '/requestData') {
|
|
279
|
+
return this.mockBackend.handleRequestData(req, res, body);
|
|
280
|
+
}
|
|
281
|
+
if (subRoute === '/saveFile') {
|
|
282
|
+
return this.mockBackend.handleSaveFile(req, res, body);
|
|
283
|
+
}
|
|
284
|
+
if (subRoute === '/removeSaved') {
|
|
285
|
+
return this.mockBackend.handleRemoveSaved(req, res, body);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
289
|
+
res.end(JSON.stringify({ errMsg: `${subRoute.replace('/', '')}:ok` }));
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (req.method === 'GET') {
|
|
295
|
+
if (subRoute === '/fileInfo') {
|
|
296
|
+
return this.mockBackend.handleFileInfo(req, res, parsedUrl.query);
|
|
297
|
+
}
|
|
298
|
+
if (subRoute === '/savedFileList') {
|
|
299
|
+
return this.mockBackend.handleSavedFileList(req, res);
|
|
300
|
+
}
|
|
301
|
+
if (subRoute === '/savedFileInfo') {
|
|
302
|
+
return this.mockBackend.handleSavedFileInfo(req, res, parsedUrl.query);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
306
|
+
res.end(JSON.stringify({ errMsg: `${subRoute.replace('/', '')}:ok` }));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
|
311
|
+
res.end('Method Not Allowed');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
close() {
|
|
315
|
+
this.liveReload.closeAll();
|
|
316
|
+
if (this.server) {
|
|
317
|
+
this.server.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getNetworkIp() {
|
|
323
|
+
const nets = os.networkInterfaces();
|
|
324
|
+
for (const name of Object.keys(nets)) {
|
|
325
|
+
for (const net of nets[name]) {
|
|
326
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
327
|
+
return net.address;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { MacleWebSimulatorServer };
|
package/src/watcher.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
class LiveReloadManager extends EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.clients = new Set();
|
|
9
|
+
this.watchers = [];
|
|
10
|
+
this.debounceTimer = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
addClient(res) {
|
|
14
|
+
res.writeHead(200, {
|
|
15
|
+
'Content-Type': 'text/event-stream',
|
|
16
|
+
'Cache-Control': 'no-cache',
|
|
17
|
+
'Connection': 'keep-alive',
|
|
18
|
+
'Access-Control-Allow-Origin': '*'
|
|
19
|
+
});
|
|
20
|
+
res.write('data: connected\n\n');
|
|
21
|
+
this.clients.add(res);
|
|
22
|
+
|
|
23
|
+
res.on('close', () => {
|
|
24
|
+
this.clients.delete(res);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
notifyReload(reason = 'file-change') {
|
|
29
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
30
|
+
this.debounceTimer = setTimeout(() => {
|
|
31
|
+
console.log(`[Watcher] Triggering simulator reload (reason: ${reason})...`);
|
|
32
|
+
for (const client of this.clients) {
|
|
33
|
+
try {
|
|
34
|
+
client.write('data: reload\n\n');
|
|
35
|
+
} catch (e) {
|
|
36
|
+
this.clients.delete(client);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.emit('reload', reason);
|
|
40
|
+
}, 300);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
watchDirectory(targetDir, onChangeCallback) {
|
|
44
|
+
if (!fs.existsSync(targetDir)) return;
|
|
45
|
+
try {
|
|
46
|
+
const watcher = fs.watch(targetDir, { recursive: true }, (eventType, filename) => {
|
|
47
|
+
if (!filename) return;
|
|
48
|
+
// Ignore dotfiles, temp files, node_modules, and git
|
|
49
|
+
if (filename.startsWith('.') || filename.includes('node_modules') || filename.includes('.git')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (onChangeCallback) {
|
|
53
|
+
onChangeCallback(eventType, filename);
|
|
54
|
+
}
|
|
55
|
+
this.notifyReload(filename);
|
|
56
|
+
});
|
|
57
|
+
this.watchers.push(watcher);
|
|
58
|
+
console.log(`[Watcher] Watching directory for live reload: ${targetDir}`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.warn(`[Watcher] Could not watch ${targetDir}:`, err.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
closeAll() {
|
|
65
|
+
for (const w of this.watchers) {
|
|
66
|
+
try { w.close(); } catch (e) {}
|
|
67
|
+
}
|
|
68
|
+
this.watchers = [];
|
|
69
|
+
for (const c of this.clients) {
|
|
70
|
+
try { c.end(); } catch (e) {}
|
|
71
|
+
}
|
|
72
|
+
this.clients.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { LiveReloadManager };
|
package/web/index.html
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Macle MiniProgram Web Simulator</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📱</text></svg>">
|
|
8
|
+
<link rel="stylesheet" href="simulator.css">
|
|
9
|
+
<script src="qrcode.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
|
|
13
|
+
<!-- Top Toolbar -->
|
|
14
|
+
<header class="toolbar">
|
|
15
|
+
<div class="toolbar-left">
|
|
16
|
+
<div class="logo-badge">
|
|
17
|
+
<div class="logo-icon">M</div>
|
|
18
|
+
<span>Macle Simulator</span>
|
|
19
|
+
<span class="logo-tag">Web Edition</span>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="control-group">
|
|
23
|
+
<span class="control-label">Device</span>
|
|
24
|
+
<select id="device-select" class="control-select"></select>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="control-group">
|
|
28
|
+
<span class="control-label">Page</span>
|
|
29
|
+
<select id="page-select" class="control-select">
|
|
30
|
+
<option value="pages/index/index">pages/index/index</option>
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="control-group">
|
|
35
|
+
<span class="control-label">Zoom</span>
|
|
36
|
+
<select id="scale-select" class="control-select">
|
|
37
|
+
<option value="1.0" selected>100%</option>
|
|
38
|
+
<option value="0.85">85%</option>
|
|
39
|
+
<option value="0.75">75%</option>
|
|
40
|
+
<option value="0.5">50%</option>
|
|
41
|
+
<option value="1.25">125%</option>
|
|
42
|
+
<option value="fit">Fit Screen</option>
|
|
43
|
+
</select>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="toolbar-center">
|
|
48
|
+
<button id="rotate-btn" class="tool-btn" title="Toggle Orientation (Portrait / Landscape)">
|
|
49
|
+
<svg viewBox="0 0 24 24"><path d="M16.48 2.52c3.27 1.55 5.61 4.72 5.97 8.48h1.5C23.44 4.84 18.29 0 12 0l-.66.03 3.81 3.81 1.33-1.32zM7.52 21.48C4.25 19.93 1.91 16.76 1.55 13H.05C.56 19.16 5.71 24 12 24l.66-.03-3.81-3.81-1.33 1.32zM7 7h10v10H7z"/></svg>
|
|
50
|
+
Rotate
|
|
51
|
+
</button>
|
|
52
|
+
|
|
53
|
+
<button id="touch-btn" class="tool-btn" title="Toggle Touch Simulation">
|
|
54
|
+
<svg viewBox="0 0 24 24"><path d="M9 11.24V7.5a2.5 2.5 0 0 1 5 0v3.74c1.21-.81 2-2.18 2-3.74a4.5 4.5 0 0 0-9 0c0 1.56.79 2.93 2 3.74zm9.84 4.63l-4.54-2.26a1.53 1.53 0 0 0-1.8.34l-.87.87 5.48 5.48c.39.39 1.02.39 1.41 0l.32-.32c1.7-1.7 1.7-4.43 0-6.11zM11.5 1a6.5 6.5 0 0 1 6.5 6.5c0 1.63-.6 3.12-1.6 4.26l.72.36a3.5 3.5 0 0 1 1.7 4.54c-.16.39-.39.75-.68 1.04l-.32.32a3.5 3.5 0 0 1-4.95 0L8.2 13.34c-.87-.87-.87-2.27 0-3.14l1.3-.8V7.5A4.5 4.5 0 0 1 14 3a4.5 4.5 0 0 1 4 2.45c.48-.3 1.04-.45 1.62-.45 1.66 0 3 1.34 3 3 0 .73-.26 1.4-.7 1.93.9.7 1.48 1.79 1.48 3.01 0 1.94-1.4 3.55-3.26 3.88l-4.66 2.33a3.5 3.5 0 0 1-3.13 0l-4.73-2.37A6.98 6.98 0 0 1 5 11.5 6.5 6.5 0 0 1 11.5 1z"/></svg>
|
|
55
|
+
Touch
|
|
56
|
+
</button>
|
|
57
|
+
|
|
58
|
+
<div class="control-group">
|
|
59
|
+
<span class="control-label">Network</span>
|
|
60
|
+
<select id="network-select" class="control-select">
|
|
61
|
+
<option value="wifi" selected>WiFi</option>
|
|
62
|
+
<option value="5g">5G</option>
|
|
63
|
+
<option value="4g">4G</option>
|
|
64
|
+
<option value="3g">3G</option>
|
|
65
|
+
<option value="none">Offline</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<button id="refresh-btn" class="tool-btn" title="Refresh MiniProgram">
|
|
70
|
+
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
71
|
+
Refresh
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="toolbar-right">
|
|
76
|
+
<button id="qr-btn" class="tool-btn" title="Preview on Mobile Device via QR Code">
|
|
77
|
+
<svg viewBox="0 0 24 24"><path d="M3 3h8v8H3zm2 2v4h4V5zm8-2h8v8h-8zm2 2v4h4V5zM3 13h8v8H3zm2 2v4h4v-4zm13-2h3v2h-3zm-5 0h2v3h-2zm2 3h3v2h-3zm-2 2h2v3h-2zm3 0h3v3h-3z"/></svg>
|
|
78
|
+
Mobile Preview
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
<button id="devtools-btn" class="tool-btn" title="Toggle Developer Tools Console">
|
|
82
|
+
<svg viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
|
83
|
+
DevTools
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
<!-- Canvas -->
|
|
89
|
+
<main class="canvas">
|
|
90
|
+
<div id="device-wrapper" class="device-wrapper">
|
|
91
|
+
<div id="device-frame" class="device-frame">
|
|
92
|
+
<div id="device-notch" class="device-notch">
|
|
93
|
+
<div class="notch-sensor"></div>
|
|
94
|
+
<div class="notch-camera"></div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="device-screen">
|
|
98
|
+
<iframe id="simulator-iframe" class="simulator-iframe" allow="geolocation; microphone; camera"></iframe>
|
|
99
|
+
<div class="touch-overlay"></div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</main>
|
|
104
|
+
|
|
105
|
+
<!-- Touch Cursor Overlay -->
|
|
106
|
+
<div id="touch-cursor" class="touch-cursor"></div>
|
|
107
|
+
|
|
108
|
+
<!-- Status Bar Footer -->
|
|
109
|
+
<footer class="statusbar">
|
|
110
|
+
<div class="statusbar-left">
|
|
111
|
+
<div class="status-badge">
|
|
112
|
+
<span class="status-dot"></span>
|
|
113
|
+
<span>Connected</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="path-display">
|
|
116
|
+
<span>Page:</span>
|
|
117
|
+
<strong id="page-path">-</strong>
|
|
118
|
+
<button id="copy-path-btn" class="copy-btn" title="Copy Path">
|
|
119
|
+
<svg style="width:12px;height:12px;fill:currentColor" viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="statusbar-right">
|
|
125
|
+
<span>Auto-Reload: <strong style="color:var(--success-color)">Active</strong></span>
|
|
126
|
+
<span>Engine: <strong>Macle 26.6.0-LG0001 (100.4.2)</strong></span>
|
|
127
|
+
</div>
|
|
128
|
+
</footer>
|
|
129
|
+
|
|
130
|
+
<!-- DevTools Drawer -->
|
|
131
|
+
<div id="devtools-drawer" class="devtools-drawer">
|
|
132
|
+
<div class="devtools-header">
|
|
133
|
+
<div class="devtools-tabs">
|
|
134
|
+
<div class="devtools-tab active" data-tab="console">Console</div>
|
|
135
|
+
<div class="devtools-tab" data-tab="storage">Storage</div>
|
|
136
|
+
<div class="devtools-tab" data-tab="network">Network</div>
|
|
137
|
+
<div class="devtools-tab" data-tab="app-info">App Info</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="devtools-actions">
|
|
140
|
+
<input type="text" id="filter-logs" class="filter-input" placeholder="Filter console logs...">
|
|
141
|
+
<button id="clear-logs" class="tool-btn" style="padding:2px 8px;font-size:11px">Clear</button>
|
|
142
|
+
<button id="close-devtools" class="tool-btn" style="padding:2px 8px;font-size:11px">✕</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Console Tab -->
|
|
147
|
+
<div id="tab-console" class="tab-pane active" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
|
|
148
|
+
<div id="logs-container" class="devtools-content">
|
|
149
|
+
<div class="log-entry">
|
|
150
|
+
<span class="log-time">--:--:--</span>
|
|
151
|
+
<span class="log-level info">info</span>
|
|
152
|
+
<span class="log-msg">Macle Web Simulator DevTools initialized. Logs from the miniprogram logic and view threads appear here.</span>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<form id="eval-form" class="devtools-eval">
|
|
157
|
+
<span class="eval-prompt">></span>
|
|
158
|
+
<input type="text" id="eval-input" class="eval-input" placeholder="Execute JavaScript in miniprogram window...">
|
|
159
|
+
</form>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Storage Tab -->
|
|
163
|
+
<div id="tab-storage" class="tab-pane" style="display:none;flex:1;overflow-y:auto;padding:12px;background:#090d16;">
|
|
164
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
|
165
|
+
<span style="font-size:12px;font-weight:600;color:var(--text-secondary)">Local / MiniProgram Storage Keys</span>
|
|
166
|
+
<button id="refresh-storage-btn" class="tool-btn" style="padding:2px 8px;font-size:11px">Refresh</button>
|
|
167
|
+
</div>
|
|
168
|
+
<table id="storage-table" style="width:100%;border-collapse:collapse;font-size:12px;font-family:monospace;">
|
|
169
|
+
<thead>
|
|
170
|
+
<tr style="border-bottom:1px solid var(--border-color);color:var(--text-muted);text-align:left;">
|
|
171
|
+
<th style="padding:6px;">Key</th>
|
|
172
|
+
<th style="padding:6px;">Value</th>
|
|
173
|
+
<th style="padding:6px;width:60px;">Action</th>
|
|
174
|
+
</tr>
|
|
175
|
+
</thead>
|
|
176
|
+
<tbody id="storage-body">
|
|
177
|
+
<tr><td colspan="3" style="padding:10px;text-align:center;color:var(--text-muted);">No storage items found</td></tr>
|
|
178
|
+
</tbody>
|
|
179
|
+
</table>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Network Tab -->
|
|
183
|
+
<div id="tab-network" class="tab-pane" style="display:none;flex:1;overflow-y:auto;padding:12px;background:#090d16;">
|
|
184
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
|
185
|
+
<span style="font-size:12px;font-weight:600;color:var(--text-secondary)">Network Requests (ma.request)</span>
|
|
186
|
+
<button id="clear-network-btn" class="tool-btn" style="padding:2px 8px;font-size:11px">Clear</button>
|
|
187
|
+
</div>
|
|
188
|
+
<table id="network-table" style="width:100%;border-collapse:collapse;font-size:12px;font-family:monospace;">
|
|
189
|
+
<thead>
|
|
190
|
+
<tr style="border-bottom:1px solid var(--border-color);color:var(--text-muted);text-align:left;">
|
|
191
|
+
<th style="padding:6px;width:60px;">Method</th>
|
|
192
|
+
<th style="padding:6px;width:60px;">Status</th>
|
|
193
|
+
<th style="padding:6px;">URL</th>
|
|
194
|
+
<th style="padding:6px;width:80px;">Time</th>
|
|
195
|
+
</tr>
|
|
196
|
+
</thead>
|
|
197
|
+
<tbody id="network-body">
|
|
198
|
+
<tr><td colspan="4" style="padding:10px;text-align:center;color:var(--text-muted);">No network requests recorded</td></tr>
|
|
199
|
+
</tbody>
|
|
200
|
+
</table>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- App Info Tab -->
|
|
204
|
+
<div id="tab-app-info" class="tab-pane" style="display:none;flex:1;overflow-y:auto;padding:16px;background:#090d16;font-size:12px;">
|
|
205
|
+
<div id="app-info-container" style="display:flex;flex-direction:column;gap:12px;">
|
|
206
|
+
<div><strong>Project Name:</strong> <span id="info-project-name">-</span></div>
|
|
207
|
+
<div><strong>Engine Version:</strong> <span id="info-engine-version">26.6.0-LG0001</span></div>
|
|
208
|
+
<div><strong>Runtime Framework:</strong> <span id="info-runtime-version">100.4.2</span></div>
|
|
209
|
+
<div><strong>Entry Route:</strong> <span id="info-entry-page">-</span></div>
|
|
210
|
+
<div><strong>Registered Pages:</strong> <ul id="info-pages-list" style="margin-left:20px;margin-top:4px;"></ul></div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- QR Code Modal -->
|
|
216
|
+
<div id="qr-modal" class="modal-backdrop">
|
|
217
|
+
<div class="modal-dialog">
|
|
218
|
+
<h3 class="modal-title">Mobile Preview</h3>
|
|
219
|
+
<p class="modal-subtitle">Scan this QR code with your mobile phone connected to the same Wi-Fi network.</p>
|
|
220
|
+
|
|
221
|
+
<div id="qrcode-container" class="qrcode-box"></div>
|
|
222
|
+
|
|
223
|
+
<div id="qr-url" class="url-box">http://...</div>
|
|
224
|
+
|
|
225
|
+
<div style="display:flex;gap:10px;width:100%">
|
|
226
|
+
<button id="copy-qr-url-btn" class="tool-btn" style="flex:1;justify-content:center">Copy URL</button>
|
|
227
|
+
<button id="close-qr-modal" class="tool-btn" style="flex:1;justify-content:center">Close</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<script src="simulator.js"></script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>
|