skill-base 2.0.3 → 2.0.6

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.
Files changed (46) hide show
  1. package/README.md +189 -85
  2. package/bin/skill-base.js +33 -7
  3. package/package.json +3 -1
  4. package/src/cappy.js +416 -0
  5. package/src/database.js +11 -0
  6. package/src/index.js +87 -24
  7. package/src/middleware/auth.js +96 -32
  8. package/src/routes/auth.js +1 -1
  9. package/src/routes/skills.js +10 -5
  10. package/src/utils/zip.js +15 -4
  11. package/static/android-chrome-192x192.png +0 -0
  12. package/static/android-chrome-512x512.png +0 -0
  13. package/static/apple-touch-icon.png +0 -0
  14. package/static/assets/index-BgwubB87.css +1 -0
  15. package/static/assets/index-DBHCo8Mz.js +230 -0
  16. package/static/favicon-16x16.png +0 -0
  17. package/static/favicon-32x32.png +0 -0
  18. package/static/favicon.ico +0 -0
  19. package/static/favicon.svg +14 -0
  20. package/static/index.html +18 -248
  21. package/static/site.webmanifest +1 -0
  22. package/static/admin/users.html +0 -593
  23. package/static/cli-code.html +0 -203
  24. package/static/css/.gitkeep +0 -0
  25. package/static/css/style.css +0 -1567
  26. package/static/diff.html +0 -466
  27. package/static/file.html +0 -443
  28. package/static/js/.gitkeep +0 -0
  29. package/static/js/admin/users.js +0 -346
  30. package/static/js/app.js +0 -508
  31. package/static/js/auth.js +0 -151
  32. package/static/js/cli-code.js +0 -184
  33. package/static/js/collaborators.js +0 -283
  34. package/static/js/diff.js +0 -540
  35. package/static/js/file.js +0 -619
  36. package/static/js/i18n.js +0 -739
  37. package/static/js/index.js +0 -168
  38. package/static/js/publish.js +0 -718
  39. package/static/js/settings.js +0 -124
  40. package/static/js/setup.js +0 -157
  41. package/static/js/skill.js +0 -808
  42. package/static/login.html +0 -82
  43. package/static/publish.html +0 -459
  44. package/static/settings.html +0 -163
  45. package/static/setup.html +0 -101
  46. package/static/skill.html +0 -851
package/static/js/app.js DELETED
@@ -1,508 +0,0 @@
1
- /**
2
- * Skill Base - 共享 API 调用封装
3
- */
4
-
5
- // API 基础路径
6
- const API_BASE = '/api/v1';
7
-
8
- /**
9
- * 封装 fetch 请求
10
- * @param {string} path - API 路径
11
- * @param {object} options - fetch 选项
12
- * @returns {Promise<any>} - 响应数据
13
- */
14
- async function api(path, options = {}) {
15
- const url = path.startsWith('/') ? `${API_BASE}${path}` : `${API_BASE}/${path}`;
16
-
17
- // 默认 headers
18
- const headers = {
19
- ...options.headers,
20
- };
21
-
22
- // 如果不是 FormData,添加 JSON content-type
23
- if (options.body && !(options.body instanceof FormData)) {
24
- headers['Content-Type'] = 'application/json';
25
- }
26
-
27
- const response = await fetch(url, {
28
- ...options,
29
- headers,
30
- credentials: 'same-origin', // 包含 cookies
31
- });
32
-
33
- // 处理 401 未授权,跳转登录页
34
- if (response.status === 401) {
35
- // 如果当前不在登录页,跳转到登录页
36
- if (!window.location.pathname.includes('/login')) {
37
- window.location.href = '/login.html';
38
- }
39
- throw new Error(typeof t === 'function' ? t('login.unauthorized') : 'Unauthorized, please sign in again');
40
- }
41
-
42
- // 处理 204 No Content
43
- if (response.status === 204) {
44
- return null;
45
- }
46
-
47
- // 解析 JSON 响应
48
- const contentType = response.headers.get('content-type');
49
- let data;
50
-
51
- if (contentType && contentType.includes('application/json')) {
52
- data = await response.json();
53
- } else {
54
- data = await response.text();
55
- }
56
-
57
- // 处理错误响应
58
- if (!response.ok) {
59
- const errorMessage = data?.detail || data?.error || data?.message || `Request failed (${response.status})`;
60
- throw new Error(errorMessage);
61
- }
62
-
63
- return data;
64
- }
65
-
66
- /**
67
- * 检查系统是否已初始化,未初始化则跳转到 setup 页面
68
- * @returns {Promise<boolean>} - 是否已初始化
69
- */
70
- async function checkSystemInit() {
71
- // setup 页面不需要检查
72
- if (window.location.pathname === '/setup' || window.location.pathname === '/setup.html') {
73
- return true;
74
- }
75
-
76
- try {
77
- const res = await fetch(`${API_BASE}/init/status`);
78
- const data = await res.json();
79
-
80
- if (!data.initialized) {
81
- // 未初始化,跳转到 setup 页面
82
- window.location.href = '/setup';
83
- return false;
84
- }
85
- return true;
86
- } catch (err) {
87
- console.error('Failed to check system init status:', err);
88
- return true; // 出错时假定已初始化,避免循环
89
- }
90
- }
91
-
92
- /**
93
- * GET 请求
94
- * @param {string} path - API 路径
95
- * @returns {Promise<any>}
96
- */
97
- async function apiGet(path) {
98
- return api(path, {
99
- method: 'GET',
100
- });
101
- }
102
-
103
- /**
104
- * POST JSON 请求
105
- * @param {string} path - API 路径
106
- * @param {object} data - 请求数据
107
- * @returns {Promise<any>}
108
- */
109
- async function apiPost(path, data) {
110
- return api(path, {
111
- method: 'POST',
112
- body: JSON.stringify(data),
113
- });
114
- }
115
-
116
- /**
117
- * PUT JSON 请求
118
- * @param {string} path - API 路径
119
- * @param {object} data - 请求数据
120
- * @returns {Promise<any>}
121
- */
122
- async function apiPut(path, data) {
123
- return api(path, {
124
- method: 'PUT',
125
- body: JSON.stringify(data),
126
- });
127
- }
128
-
129
- /**
130
- * DELETE 请求
131
- * @param {string} path - API 路径
132
- * @returns {Promise<any>}
133
- */
134
- async function apiDelete(path) {
135
- return api(path, {
136
- method: 'DELETE',
137
- });
138
- }
139
-
140
- /**
141
- * POST FormData 请求(用于文件上传)
142
- * @param {string} path - API 路径
143
- * @param {FormData} formData - FormData 对象
144
- * @returns {Promise<any>}
145
- */
146
- async function apiUpload(path, formData) {
147
- return api(path, {
148
- method: 'POST',
149
- body: formData,
150
- // 不设置 Content-Type,让浏览器自动设置 multipart/form-data
151
- });
152
- }
153
-
154
- /**
155
- * 检查登录状态
156
- * @returns {Promise<object|null>} - 用户信息或 null
157
- */
158
- async function checkAuth() {
159
- try {
160
- const user = await apiGet('/auth/me');
161
- return user;
162
- } catch (error) {
163
- // 请求失败,跳转登录页
164
- if (!window.location.pathname.includes('/login')) {
165
- window.location.href = '/login.html';
166
- }
167
- return null;
168
- }
169
- }
170
-
171
- // 当前用户信息缓存
172
- let currentUser = null;
173
-
174
- /**
175
- * 获取当前用户信息(带缓存)
176
- * @param {boolean} forceRefresh - 是否强制刷新
177
- * @returns {Promise<object|null>}
178
- */
179
- async function getCurrentUser(forceRefresh = false) {
180
- if (currentUser && !forceRefresh) {
181
- return currentUser;
182
- }
183
-
184
- try {
185
- currentUser = await apiGet('/auth/me');
186
- return currentUser;
187
- } catch (error) {
188
- currentUser = null;
189
- return null;
190
- }
191
- }
192
-
193
- /**
194
- * 登出
195
- * @returns {Promise<void>}
196
- */
197
- async function logout() {
198
- try {
199
- await apiPost('/auth/logout', {});
200
- } catch (error) {
201
- // 忽略登出错误
202
- } finally {
203
- currentUser = null;
204
- window.location.href = '/login.html';
205
- }
206
- }
207
-
208
- // ========================================
209
- // Toast 通知
210
- // ========================================
211
-
212
- // Toast 图标 SVG
213
- const TOAST_ICONS = {
214
- success: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
215
- error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
216
- warning: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
217
- info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
218
- };
219
-
220
- /**
221
- * 获取或创建 Toast 容器
222
- * @returns {HTMLElement}
223
- */
224
- function getToastContainer() {
225
- let container = document.querySelector('.toast-container');
226
- if (!container) {
227
- container = document.createElement('div');
228
- container.className = 'toast-container';
229
- document.body.appendChild(container);
230
- }
231
- return container;
232
- }
233
-
234
- /**
235
- * 显示 Toast 通知
236
- * @param {string} message - 消息内容
237
- * @param {string} type - 类型:success, error, warning, info
238
- * @param {number} duration - 显示时长(毫秒),默认 3000
239
- */
240
- function showToast(message, type = 'info', duration = 3000) {
241
- const container = getToastContainer();
242
-
243
- const toast = document.createElement('div');
244
- toast.className = `toast ${type}`;
245
- toast.innerHTML = `
246
- <span class="toast-icon">${TOAST_ICONS[type] || TOAST_ICONS.info}</span>
247
- <span class="toast-message">${escapeHtml(message)}</span>
248
- `;
249
-
250
- container.appendChild(toast);
251
-
252
- // 自动消失
253
- setTimeout(() => {
254
- toast.classList.add('toast-out');
255
- setTimeout(() => {
256
- toast.remove();
257
- }, 300);
258
- }, duration);
259
- }
260
-
261
- // ========================================
262
- // 工具函数
263
- // ========================================
264
-
265
- /**
266
- * HTML 转义
267
- * @param {string} text - 原始文本
268
- * @returns {string} - 转义后的文本
269
- */
270
- function escapeHtml(text) {
271
- const div = document.createElement('div');
272
- div.textContent = text;
273
- return div.innerHTML;
274
- }
275
-
276
- /**
277
- * 格式化日期
278
- * @param {string|Date} dateStr - 日期字符串或 Date 对象
279
- * @returns {string} - 格式化后的日期
280
- */
281
- function formatDate(dateStr) {
282
- if (!dateStr) return '-';
283
-
284
- const date = new Date(dateStr);
285
- const now = new Date();
286
- const diff = now - date;
287
-
288
- const _t = typeof t === 'function' ? t : (k) => k;
289
-
290
- // 1分钟内
291
- if (diff < 60 * 1000) {
292
- return _t('time.justNow');
293
- }
294
-
295
- // 1小时内
296
- if (diff < 60 * 60 * 1000) {
297
- const minutes = Math.floor(diff / (60 * 1000));
298
- return window.I18N_LANG === 'zh' ? `${minutes}${_t('time.minutesAgo')}` : `${minutes}${_t('time.minutesAgo')}`;
299
- }
300
-
301
- // 24小时内
302
- if (diff < 24 * 60 * 60 * 1000) {
303
- const hours = Math.floor(diff / (60 * 60 * 1000));
304
- return window.I18N_LANG === 'zh' ? `${hours}${_t('time.hoursAgo')}` : `${hours}${_t('time.hoursAgo')}`;
305
- }
306
-
307
- // 7天内
308
- if (diff < 7 * 24 * 60 * 60 * 1000) {
309
- const days = Math.floor(diff / (24 * 60 * 60 * 1000));
310
- return window.I18N_LANG === 'zh' ? `${days}${_t('time.daysAgo')}` : `${days}${_t('time.daysAgo')}`;
311
- }
312
-
313
- // 超过7天,显示具体日期
314
- const year = date.getFullYear();
315
- const month = String(date.getMonth() + 1).padStart(2, '0');
316
- const day = String(date.getDate()).padStart(2, '0');
317
- const hour = String(date.getHours()).padStart(2, '0');
318
- const minute = String(date.getMinutes()).padStart(2, '0');
319
-
320
- // 同一年不显示年份
321
- if (year === now.getFullYear()) {
322
- return `${month}-${day} ${hour}:${minute}`;
323
- }
324
-
325
- return `${year}-${month}-${day} ${hour}:${minute}`;
326
- }
327
-
328
- /**
329
- * 格式化文件大小
330
- * @param {number} bytes - 字节数
331
- * @returns {string} - 格式化后的大小
332
- */
333
- function formatFileSize(bytes) {
334
- if (bytes === 0) return '0 B';
335
-
336
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
337
- const k = 1024;
338
- const i = Math.floor(Math.log(bytes) / Math.log(k));
339
-
340
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + units[i];
341
- }
342
-
343
- /**
344
- * 防抖函数
345
- * @param {Function} func - 要防抖的函数
346
- * @param {number} wait - 等待时间(毫秒)
347
- * @returns {Function}
348
- */
349
- function debounce(func, wait) {
350
- let timeout;
351
- return function executedFunction(...args) {
352
- const later = () => {
353
- clearTimeout(timeout);
354
- func(...args);
355
- };
356
- clearTimeout(timeout);
357
- timeout = setTimeout(later, wait);
358
- };
359
- }
360
-
361
- // ========================================
362
- // 导航栏渲染
363
- // ========================================
364
-
365
- /**
366
- * 渲染导航栏
367
- * @param {object} user - 用户信息
368
- */
369
- function renderNavbar(user) {
370
- const navbar = document.querySelector('.navbar');
371
- if (!navbar) return;
372
-
373
- // 查找或创建用户区域
374
- let userArea = navbar.querySelector('.navbar-user');
375
- if (!userArea) {
376
- userArea = document.createElement('div');
377
- userArea.className = 'navbar-user';
378
- navbar.querySelector('.container').appendChild(userArea);
379
- }
380
-
381
- const _t = typeof t === 'function' ? t : (k) => k;
382
- const _lang = window.I18N_LANG || 'en';
383
- const _langLabel = _lang === 'zh' ? '中文' : 'English';
384
- const _langBtnHtml = `
385
- <div class="lang-switcher">
386
- <button class="lang-switcher-trigger">
387
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
388
- <circle cx="12" cy="12" r="10"/>
389
- <line x1="2" y1="12" x2="22" y2="12"/>
390
- <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
391
- </svg>
392
- <span>${_langLabel}</span>
393
- <svg class="lang-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
394
- <polyline points="6 9 12 15 18 9"/>
395
- </svg>
396
- </button>
397
- <div class="lang-switcher-menu">
398
- <button class="lang-switcher-option${_lang === 'zh' ? ' active' : ''}" onclick="setLang('zh')">中文</button>
399
- <button class="lang-switcher-option${_lang === 'en' ? ' active' : ''}" onclick="setLang('en')">English</button>
400
- </div>
401
- </div>
402
- `;
403
-
404
- if (user) {
405
- // 已登录:显示用户名和下拉菜单
406
- userArea.innerHTML = `
407
- ${_langBtnHtml}
408
- <div class="navbar-user-dropdown">
409
- <button class="navbar-user-btn">
410
- <span class="username">${escapeHtml(user.username)}</span>
411
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
412
- <polyline points="6 9 12 15 18 9"/>
413
- </svg>
414
- </button>
415
- <div class="navbar-user-menu">
416
- <a href="/settings.html" class="navbar-user-menu-item">
417
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
418
- <circle cx="12" cy="12" r="3"/>
419
- <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
420
- </svg>
421
- ${_t('nav.settings')}
422
- </a>
423
- ${user.role === 'admin' ? `
424
- <a href="/admin/users" class="navbar-user-menu-item">
425
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
426
- <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
427
- <circle cx="9" cy="7" r="4"/>
428
- <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
429
- <path d="M16 3.13a4 4 0 0 1 0 7.75"/>
430
- </svg>
431
- ${_t('nav.admin')}
432
- </a>
433
- ` : ''}
434
- <div class="navbar-user-menu-divider"></div>
435
- <button onclick="logout()" class="navbar-user-menu-item navbar-user-logout">
436
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
437
- <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
438
- <polyline points="16 17 21 12 16 7"/>
439
- <line x1="21" y1="12" x2="9" y2="12"/>
440
- </svg>
441
- ${_t('nav.logout')}
442
- </button>
443
- </div>
444
- </div>
445
- `;
446
-
447
- // 绑定下拉菜单的点击事件
448
- const dropdown = userArea.querySelector('.navbar-user-dropdown');
449
- const button = dropdown.querySelector('.navbar-user-btn');
450
- button.addEventListener('click', (e) => {
451
- e.stopPropagation();
452
- dropdown.classList.toggle('active');
453
- });
454
-
455
- // 绑定语言切换下拉
456
- const langSwitcher = userArea.querySelector('.lang-switcher');
457
- const langTrigger = userArea.querySelector('.lang-switcher-trigger');
458
- if (langTrigger) {
459
- langTrigger.addEventListener('click', (e) => {
460
- e.stopPropagation();
461
- langSwitcher.classList.toggle('active');
462
- });
463
- }
464
-
465
- // 点击其他地方关闭所有菜单
466
- document.addEventListener('click', () => {
467
- dropdown.classList.remove('active');
468
- if (langSwitcher) langSwitcher.classList.remove('active');
469
- });
470
- } else {
471
- // 未登录:显示登录按钮
472
- userArea.innerHTML = `
473
- ${_langBtnHtml}
474
- <a href="/login.html" class="btn btn-primary btn-sm">${_t('nav.login')}</a>
475
- `;
476
-
477
- // 绑定语言切换下拉
478
- const langSwitcher = userArea.querySelector('.lang-switcher');
479
- const langTrigger = userArea.querySelector('.lang-switcher-trigger');
480
- if (langTrigger) {
481
- langTrigger.addEventListener('click', (e) => {
482
- e.stopPropagation();
483
- langSwitcher.classList.toggle('active');
484
- });
485
- document.addEventListener('click', () => {
486
- langSwitcher.classList.remove('active');
487
- });
488
- }
489
- }
490
- }
491
-
492
- /**
493
- * 初始化页面(检查系统初始化状态、登录状态并渲染导航栏)
494
- * 在需要登录的页面调用此函数
495
- */
496
- async function initPage() {
497
- // 先检查系统是否已初始化
498
- const initialized = await checkSystemInit();
499
- if (!initialized) {
500
- return null; // 已跳转到 setup 页面
501
- }
502
-
503
- const user = await checkAuth();
504
- if (user) {
505
- renderNavbar(user);
506
- }
507
- return user;
508
- }
package/static/js/auth.js DELETED
@@ -1,151 +0,0 @@
1
- /**
2
- * Skill Base - 登录逻辑
3
- */
4
-
5
- (function() {
6
- 'use strict';
7
-
8
- // DOM 元素
9
- const loginForm = document.getElementById('loginForm');
10
- const usernameInput = document.getElementById('username');
11
- const passwordInput = document.getElementById('password');
12
- const loginButton = document.getElementById('loginButton');
13
- const errorBox = document.getElementById('loginError');
14
-
15
- /**
16
- * 显示错误信息
17
- * @param {string} message - 错误消息
18
- */
19
- function showError(message) {
20
- if (errorBox) {
21
- errorBox.textContent = message;
22
- errorBox.classList.add('visible');
23
- }
24
- }
25
-
26
- /**
27
- * 隐藏错误信息
28
- */
29
- function hideError() {
30
- if (errorBox) {
31
- errorBox.classList.remove('visible');
32
- }
33
- }
34
-
35
- /**
36
- * 设置按钮加载状态
37
- * @param {boolean} loading - 是否加载中
38
- */
39
- const loginButtonIdleHtml =
40
- '<svg class="btn-devtools-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 3"/></svg> ' + (typeof t === 'function' ? t('login.submit') : '执行登录');
41
-
42
- function setLoading(loading) {
43
- if (loginButton) {
44
- loginButton.disabled = loading;
45
- loginButton.innerHTML = loading
46
- ? '<span class="spinner spinner-sm"></span> ' + (typeof t === 'function' ? t('login.loading') : '登录中...')
47
- : loginButtonIdleHtml;
48
- }
49
- }
50
-
51
- /**
52
- * 处理登录表单提交
53
- * @param {Event} e - 提交事件
54
- */
55
- async function handleLogin(e) {
56
- e.preventDefault();
57
-
58
- // 隐藏之前的错误
59
- hideError();
60
-
61
- // 获取表单值
62
- const username = usernameInput?.value?.trim();
63
- const password = passwordInput?.value;
64
-
65
- // 前端验证
66
- if (!username) {
67
- showError(typeof t === 'function' ? t('login.errUsername') : '请输入用户名');
68
- usernameInput?.focus();
69
- return;
70
- }
71
-
72
- if (!password) {
73
- showError(typeof t === 'function' ? t('login.errPassword') : '请输入密码');
74
- passwordInput?.focus();
75
- return;
76
- }
77
-
78
- // 设置加载状态
79
- setLoading(true);
80
-
81
- try {
82
- // 调用登录 API
83
- await apiPost('/auth/login', {
84
- username,
85
- password,
86
- });
87
-
88
- // 登录成功,检查是否来自 CLI
89
- const urlParams = new URLSearchParams(window.location.search);
90
- const fromCli = urlParams.get('from') === 'cli';
91
-
92
- if (fromCli) {
93
- // 跳转到 CLI 验证码页面
94
- window.location.href = '/cli-code.html?from=cli';
95
- } else {
96
- // 跳转到首页
97
- window.location.href = '/';
98
- }
99
-
100
- } catch (error) {
101
- // 显示错误信息
102
- showError(error.message || (typeof t === 'function' ? t('login.errFailed') : '登录失败,请重试'));
103
-
104
- // 清空密码
105
- if (passwordInput) {
106
- passwordInput.value = '';
107
- passwordInput.focus();
108
- }
109
- } finally {
110
- setLoading(false);
111
- }
112
- }
113
-
114
- /**
115
- * 初始化登录页面
116
- */
117
- async function init() {
118
- // 先检查系统是否已初始化
119
- if (typeof checkSystemInit === 'function') {
120
- const initialized = await checkSystemInit();
121
- if (!initialized) {
122
- return; // 已跳转到 setup 页面
123
- }
124
- }
125
-
126
- // 监听表单提交
127
- if (loginForm) {
128
- loginForm.addEventListener('submit', handleLogin);
129
- }
130
-
131
- // 输入时隐藏错误
132
- if (usernameInput) {
133
- usernameInput.addEventListener('input', hideError);
134
- }
135
- if (passwordInput) {
136
- passwordInput.addEventListener('input', hideError);
137
- }
138
-
139
- // 自动聚焦用户名输入框
140
- if (usernameInput) {
141
- usernameInput.focus();
142
- }
143
- }
144
-
145
- // 页面加载完成后初始化
146
- if (document.readyState === 'loading') {
147
- document.addEventListener('DOMContentLoaded', init);
148
- } else {
149
- init();
150
- }
151
- })();