@quicktvui/web-cli 1.0.8 → 2.1.1

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.
@@ -0,0 +1,370 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect x='10' y='20' width='80' height='55' rx='8' fill='%23333' stroke='%23666' stroke-width='3'/><rect x='18' y='28' width='64' height='39' rx='4' fill='%234a90d9'/><rect x='35' y='75' width='30' height='8' rx='2' fill='%23333'/><rect x='25' y='83' width='50' height='5' rx='2' fill='%23333'/><circle cx='50' cy='45' r='12' fill='none' stroke='%23fff' stroke-width='2' opacity='0.5'/><polygon points='46,40 46,50 54,45' fill='%23fff' opacity='0.8'/></svg>">
7
+ <title>QuickTVUI Web Dev</title>
8
+ <script>
9
+ // Webpack public path - 让 web-runtime 的动态加载指向根路径
10
+ (function() {
11
+ window.__webpack_public_path__ = '/';
12
+ console.log('[Dev] __webpack_public_path__: /');
13
+ })();
14
+ </script>
15
+ <script>
16
+ // TV dimensions - MUST be set before anything else
17
+ var TV_WIDTH = 1920;
18
+ var TV_HEIGHT = 1080;
19
+ var _originalInnerWidth = window.innerWidth;
20
+ var _originalInnerHeight = window.innerHeight;
21
+
22
+ Object.defineProperty(window, 'innerWidth', {
23
+ get: function() { return TV_WIDTH; },
24
+ configurable: true
25
+ });
26
+ Object.defineProperty(window, 'innerHeight', {
27
+ get: function() { return TV_HEIGHT; },
28
+ configurable: true
29
+ });
30
+ Object.defineProperty(window, 'devicePixelRatio', {
31
+ get: function() { return 1; },
32
+ configurable: true
33
+ });
34
+
35
+ // Bundle 配置 - 由 DevServer 注入模板变量
36
+ window.__BUNDLE_CONFIG__ = {
37
+ entry: '{{BUNDLE_PATH}}',
38
+ watch: {{WATCH_ENABLED}},
39
+ sseEndpoint: '{{SSE_ENDPOINT}}'
40
+ };
41
+
42
+ console.log('[Dev] TV dimensions:', TV_WIDTH, 'x', TV_HEIGHT);
43
+ console.log('[Dev] Bundle config:', window.__BUNDLE_CONFIG__);
44
+ </script>
45
+ <style>
46
+ * { margin: 0; padding: 0; box-sizing: border-box; }
47
+ html, body {
48
+ width: 100%;
49
+ height: 100%;
50
+ overflow: hidden;
51
+ background-color: #1a1a1a;
52
+ }
53
+ #main-container {
54
+ width: 1920px;
55
+ height: 1080px;
56
+ position: absolute;
57
+ top: 0;
58
+ left: 0;
59
+ background-color: #26292F;
60
+ visibility: hidden;
61
+ }
62
+ #app {
63
+ width: 1920px !important;
64
+ height: 1080px !important;
65
+ background-color: #26292F;
66
+ }
67
+ #app > * {
68
+ width: 1920px !important;
69
+ height: 1080px !important;
70
+ position: relative !important;
71
+ overflow: hidden !important;
72
+ }
73
+ [focusable="true"], [data-focusable="true"] {
74
+ cursor: pointer;
75
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
76
+ }
77
+ [focusable="true"].focused, [data-focusable="true"].focused {
78
+ transform: scale(1.05);
79
+ box-shadow: 0 0 20px rgba(76, 175, 80, 0.5);
80
+ z-index: 100;
81
+ }
82
+ /* 返回按钮 */
83
+ #web-back-btn {
84
+ position: absolute;
85
+ z-index: 99999;
86
+ width: 48px;
87
+ height: 48px;
88
+ background: rgba(255, 255, 255, 0.1);
89
+ border: 2px solid rgba(255, 255, 255, 0.3);
90
+ border-radius: 50%;
91
+ cursor: pointer;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ transition: all 0.2s ease;
96
+ backdrop-filter: blur(10px);
97
+ left: 20px;
98
+ top: 20px;
99
+ }
100
+ #web-back-btn:hover {
101
+ background: rgba(255, 255, 255, 0.2);
102
+ border-color: rgba(255, 255, 255, 0.5);
103
+ transform: scale(1.1);
104
+ }
105
+ #web-back-btn:active {
106
+ transform: scale(0.95);
107
+ }
108
+ #web-back-btn svg {
109
+ width: 24px;
110
+ height: 24px;
111
+ fill: none;
112
+ stroke: rgba(255, 255, 255, 0.8);
113
+ stroke-width: 2;
114
+ stroke-linecap: round;
115
+ stroke-linejoin: round;
116
+ }
117
+ /* 状态提示条 */
118
+ #dev-status-bar {
119
+ position: absolute;
120
+ bottom: 0;
121
+ left: 0;
122
+ right: 0;
123
+ z-index: 99998;
124
+ background: rgba(0, 0, 0, 0.8);
125
+ color: #aaa;
126
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
127
+ font-size: 12px;
128
+ padding: 6px 16px;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: space-between;
132
+ transition: opacity 0.3s ease;
133
+ }
134
+ #dev-status-bar .status-left {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 8px;
138
+ }
139
+ #dev-status-bar .status-dot {
140
+ width: 8px;
141
+ height: 8px;
142
+ border-radius: 50%;
143
+ background: #4CAF50;
144
+ }
145
+ #dev-status-bar .status-dot.building {
146
+ background: #FF9800;
147
+ animation: pulse 1s infinite;
148
+ }
149
+ #dev-status-bar .status-dot.error {
150
+ background: #f44336;
151
+ }
152
+ @keyframes pulse {
153
+ 0%, 100% { opacity: 1; }
154
+ 50% { opacity: 0.4; }
155
+ }
156
+ #dev-status-bar .status-text {
157
+ color: #ccc;
158
+ }
159
+ #dev-status-bar .status-right {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 12px;
163
+ color: #888;
164
+ }
165
+ /* 等待 bundle 的加载界面 */
166
+ #waiting-overlay {
167
+ position: absolute;
168
+ top: 0;
169
+ left: 0;
170
+ right: 0;
171
+ bottom: 0;
172
+ z-index: 99990;
173
+ background: #1a1a1a;
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ flex-direction: column;
178
+ }
179
+ #waiting-overlay.hidden {
180
+ display: none;
181
+ }
182
+ #waiting-overlay .waiting-title {
183
+ color: #4CAF50;
184
+ font-size: 28px;
185
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
186
+ margin-bottom: 20px;
187
+ }
188
+ #waiting-overlay .waiting-info {
189
+ color: #aaa;
190
+ font-size: 16px;
191
+ font-family: 'Monaco', 'Menlo', monospace;
192
+ }
193
+ #waiting-overlay .waiting-spinner {
194
+ width: 40px;
195
+ height: 40px;
196
+ border: 3px solid rgba(76, 175, 80, 0.2);
197
+ border-top: 3px solid #4CAF50;
198
+ border-radius: 50%;
199
+ animation: spin 1s linear infinite;
200
+ margin-bottom: 24px;
201
+ }
202
+ @keyframes spin {
203
+ 0% { transform: rotate(0deg); }
204
+ 100% { transform: rotate(360deg); }
205
+ }
206
+ </style>
207
+ </head>
208
+ <body>
209
+ <div id="main-container">
210
+ <!-- 返回按钮 -->
211
+ <button id="web-back-btn" title="返回">
212
+ <svg viewBox="0 0 24 24">
213
+ <polyline points="15 18 9 12 15 6"></polyline>
214
+ </svg>
215
+ </button>
216
+ <!-- 应用容器 -->
217
+ <div id="app"></div>
218
+ <!-- 状态栏 -->
219
+ <div id="dev-status-bar">
220
+ <div class="status-left">
221
+ <div class="status-dot" id="status-dot"></div>
222
+ <span class="status-text" id="status-text">正在等待构建...</span>
223
+ </div>
224
+ <div class="status-right">
225
+ <span id="bundle-info"></span>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ <!-- 等待构建界面 -->
230
+ <div id="waiting-overlay">
231
+ <div class="waiting-spinner"></div>
232
+ <div class="waiting-title">QuickTVUI Web Dev</div>
233
+ <div class="waiting-info" id="waiting-info">正在等待 dist/dev/index.bundle 构建...</div>
234
+ </div>
235
+ <script>
236
+ // ========== 缩放适配 ==========
237
+ function scaleApp() {
238
+ var container = document.getElementById('main-container');
239
+ if (container) {
240
+ container.style.setProperty('width', TV_WIDTH + 'px');
241
+ container.style.setProperty('height', TV_HEIGHT + 'px');
242
+ var scaleX = _originalInnerWidth / TV_WIDTH;
243
+ var scaleY = _originalInnerHeight / TV_HEIGHT;
244
+ var scale = Math.min(scaleX, scaleY);
245
+ container.style.transformOrigin = 'top left';
246
+ container.style.transform = 'scale(' + scale + ')';
247
+ var offsetX = (_originalInnerWidth - TV_WIDTH * scale) / 2;
248
+ var offsetY = (_originalInnerHeight - TV_HEIGHT * scale) / 2;
249
+ container.style.left = '0';
250
+ container.style.top = '0';
251
+ container.style.marginLeft = offsetX + 'px';
252
+ container.style.marginTop = offsetY + 'px';
253
+ container.style.visibility = 'visible';
254
+ }
255
+ }
256
+
257
+ if (document.readyState === 'loading') {
258
+ document.addEventListener('DOMContentLoaded', scaleApp);
259
+ } else {
260
+ scaleApp();
261
+ }
262
+ window.addEventListener('load', scaleApp);
263
+ window.addEventListener('resize', scaleApp);
264
+
265
+ // ========== 返回按钮 ==========
266
+ document.getElementById('web-back-btn').addEventListener('click', function() {
267
+ var focusManager = window.__TV_FOCUS_MANAGER__;
268
+ if (focusManager && typeof focusManager.dispatchBackPressed === 'function') {
269
+ focusManager.dispatchBackPressed({ key: 'Escape', keyCode: 4 });
270
+ }
271
+ });
272
+
273
+ // ========== 状态管理 ==========
274
+ function updateStatus(state, text) {
275
+ var dot = document.getElementById('status-dot');
276
+ var statusText = document.getElementById('status-text');
277
+ if (dot) {
278
+ dot.className = 'status-dot';
279
+ if (state === 'building') dot.classList.add('building');
280
+ if (state === 'error') dot.classList.add('error');
281
+ }
282
+ if (statusText) statusText.textContent = text;
283
+ }
284
+
285
+ function hideWaitingOverlay() {
286
+ var overlay = document.getElementById('waiting-overlay');
287
+ if (overlay) overlay.classList.add('hidden');
288
+ }
289
+
290
+ // ========== SSE 热更新 ==========
291
+ var sseConnected = false;
292
+ // 页面加载后的冷却期:这段时间内忽略 bundle-update 事件
293
+ // 防止 SSE 重连后收到缓存的旧 bundle-update 导致无限刷新
294
+ var sseCooldownUntil = Date.now() + 3000; // 3 秒冷却期
295
+ if (window.__BUNDLE_CONFIG__ && window.__BUNDLE_CONFIG__.watch) {
296
+ try {
297
+ var sse = new EventSource(window.__BUNDLE_CONFIG__.sseEndpoint);
298
+ sse.onopen = function() {
299
+ sseConnected = true;
300
+ console.log('[Dev] SSE connected');
301
+ updateStatus('ready', '已连接 - 等待变化');
302
+ };
303
+ sse.onmessage = function(e) {
304
+ try {
305
+ var data = JSON.parse(e.data);
306
+ console.log('[Dev] SSE event:', data.type, data);
307
+ if (data.type === 'connected') {
308
+ updateStatus('ready', '开发服务器已连接');
309
+ } else if (data.type === 'bundle-update') {
310
+ // 冷却期内忽略 bundle-update(可能是 SSE 重连后缓存的旧事件)
311
+ if (Date.now() < sseCooldownUntil) {
312
+ console.log('[Dev] Ignoring bundle-update during cooldown');
313
+ updateStatus('ready', '已连接 - 等待变化');
314
+ return;
315
+ }
316
+ updateStatus('building', 'Bundle 更新中...');
317
+ setTimeout(function() { location.reload(); }, 300);
318
+ } else if (data.type === 'full-reload') {
319
+ if (Date.now() < sseCooldownUntil) {
320
+ console.log('[Dev] Ignoring full-reload during cooldown');
321
+ return;
322
+ }
323
+ updateStatus('building', '正在刷新...');
324
+ setTimeout(function() { location.reload(); }, 300);
325
+ } else if (data.type === 'build-status') {
326
+ if (data.status === 'building') {
327
+ updateStatus('building', data.message || '构建中...');
328
+ } else if (data.status === 'ready') {
329
+ updateStatus('ready', data.message || '构建完成');
330
+ } else if (data.status === 'error') {
331
+ updateStatus('error', data.message || '构建失败');
332
+ }
333
+ }
334
+ } catch (ex) {
335
+ console.error('[Dev] SSE parse error:', ex);
336
+ }
337
+ };
338
+ sse.onerror = function() {
339
+ sseConnected = false;
340
+ console.log('[Dev] SSE disconnected, retrying...');
341
+ updateStatus('building', 'SSE 断开,重连中...');
342
+ };
343
+ } catch (e) {
344
+ console.error('[Dev] SSE init error:', e);
345
+ }
346
+ }
347
+
348
+ // ========== 自动添加 bundle 参数(如果 URL 中没有) ==========
349
+ // web-runtime 的 loader.js 通过 URL 参数 ?bundle=xxx 加载
350
+ // 如果当前 URL 没有 bundle 参数,自动添加并跳转
351
+ (function() {
352
+ var urlParams = new URLSearchParams(window.location.search);
353
+ var bundleUrl = urlParams.get('bundle');
354
+ if (!bundleUrl && window.__BUNDLE_CONFIG__ && window.__BUNDLE_CONFIG__.entry) {
355
+ var newUrl = new URL(window.location.href);
356
+ newUrl.searchParams.set('bundle', window.__BUNDLE_CONFIG__.entry);
357
+ window.location.replace(newUrl.toString());
358
+ return; // 跳转后不再执行后续代码
359
+ }
360
+ // 有 bundle 参数时,更新状态
361
+ if (bundleUrl) {
362
+ document.getElementById('bundle-info').textContent = bundleUrl;
363
+ hideWaitingOverlay();
364
+ }
365
+ })();
366
+ </script>
367
+ <!-- web-runtime 的编译产物(loader.js 等),由 DevServer 动态注入 -->
368
+ {{WEB_RUNTIME_SCRIPTS}}
369
+ </body>
370
+ </html>