skill-base 2.0.4 → 2.0.7
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 +177 -115
- package/bin/skill-base.js +29 -3
- package/package.json +4 -1
- package/src/cappy.js +416 -0
- package/src/database.js +11 -0
- package/src/index.js +125 -25
- package/src/middleware/auth.js +96 -32
- package/src/routes/auth.js +1 -1
- package/src/routes/skills.js +10 -5
- package/src/utils/zip.js +15 -4
- package/static/android-chrome-192x192.png +0 -0
- package/static/android-chrome-512x512.png +0 -0
- package/static/apple-touch-icon.png +0 -0
- package/static/assets/index-BkwByEEp.css +1 -0
- package/static/assets/index-CB4Diul3.js +209 -0
- package/static/favicon-16x16.png +0 -0
- package/static/favicon-32x32.png +0 -0
- package/static/favicon.ico +0 -0
- package/static/favicon.svg +14 -0
- package/static/index.html +18 -248
- package/static/site.webmanifest +1 -0
- package/static/admin/users.html +0 -593
- package/static/cli-code.html +0 -203
- package/static/css/.gitkeep +0 -0
- package/static/css/style.css +0 -1567
- package/static/diff.html +0 -466
- package/static/file.html +0 -443
- package/static/js/.gitkeep +0 -0
- package/static/js/admin/users.js +0 -346
- package/static/js/app.js +0 -508
- package/static/js/auth.js +0 -151
- package/static/js/cli-code.js +0 -184
- package/static/js/collaborators.js +0 -283
- package/static/js/diff.js +0 -540
- package/static/js/file.js +0 -619
- package/static/js/i18n.js +0 -739
- package/static/js/index.js +0 -168
- package/static/js/publish.js +0 -718
- package/static/js/settings.js +0 -124
- package/static/js/setup.js +0 -157
- package/static/js/skill.js +0 -808
- package/static/login.html +0 -82
- package/static/publish.html +0 -459
- package/static/settings.html +0 -163
- package/static/setup.html +0 -101
- 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
|
-
})();
|