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.
- package/README.md +189 -85
- package/bin/skill-base.js +33 -7
- package/package.json +3 -1
- package/src/cappy.js +416 -0
- package/src/database.js +11 -0
- package/src/index.js +87 -24
- 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-BgwubB87.css +1 -0
- package/static/assets/index-DBHCo8Mz.js +230 -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/diff.js
DELETED
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill Base - Diff 对比页逻辑
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// 全局状态
|
|
6
|
-
let skillId = null;
|
|
7
|
-
let currentSkill = null;
|
|
8
|
-
let versions = [];
|
|
9
|
-
let currentVersionA = null;
|
|
10
|
-
let currentVersionB = null;
|
|
11
|
-
let currentFilePath = null;
|
|
12
|
-
let zipA = null;
|
|
13
|
-
let zipB = null;
|
|
14
|
-
let outputFormat = 'side-by-side'; // 默认左右分栏
|
|
15
|
-
let changedFiles = [];
|
|
16
|
-
|
|
17
|
-
// 二进制文件扩展名
|
|
18
|
-
const BINARY_EXTS = new Set([
|
|
19
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp',
|
|
20
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
21
|
-
'.zip', '.tar', '.gz', '.rar', '.7z',
|
|
22
|
-
'.exe', '.dll', '.so', '.dylib',
|
|
23
|
-
'.mp3', '.mp4', '.wav', '.avi', '.mov',
|
|
24
|
-
'.ttf', '.otf', '.woff', '.woff2',
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
// 页面加载时初始化
|
|
28
|
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
29
|
-
const params = new URLSearchParams(window.location.search);
|
|
30
|
-
skillId = params.get('id');
|
|
31
|
-
currentFilePath = params.get('path') || null;
|
|
32
|
-
currentVersionA = params.get('version_a') || null;
|
|
33
|
-
currentVersionB = params.get('version_b') || null;
|
|
34
|
-
|
|
35
|
-
if (!skillId) {
|
|
36
|
-
showToast(t('diff.missingId'), 'error');
|
|
37
|
-
setTimeout(() => window.location.href = '/', 1500);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 1. 检查登录状态
|
|
42
|
-
const user = await checkAuth();
|
|
43
|
-
if (!user) return;
|
|
44
|
-
|
|
45
|
-
// 2. 渲染导航栏
|
|
46
|
-
renderNavbar(user);
|
|
47
|
-
|
|
48
|
-
// 3. 加载 Skill 信息
|
|
49
|
-
await loadSkillInfo();
|
|
50
|
-
|
|
51
|
-
// 4. 加载版本列表填充下拉框
|
|
52
|
-
await loadVersions();
|
|
53
|
-
|
|
54
|
-
// 5. 如果有 version_a 和 version_b 参数,自动执行 diff
|
|
55
|
-
if (currentVersionA && currentVersionB) {
|
|
56
|
-
// 设置下拉框选中状态
|
|
57
|
-
document.getElementById('version-a').value = currentVersionA;
|
|
58
|
-
document.getElementById('version-b').value = currentVersionB;
|
|
59
|
-
await performDiff();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 6. 更新文件路径显示
|
|
63
|
-
updateFilePathDisplay();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 加载 Skill 信息
|
|
68
|
-
*/
|
|
69
|
-
async function loadSkillInfo() {
|
|
70
|
-
try {
|
|
71
|
-
const skill = await apiGet(`/skills/${skillId}`);
|
|
72
|
-
currentSkill = skill;
|
|
73
|
-
|
|
74
|
-
// 更新页面标题
|
|
75
|
-
document.title = `${t('diff.pageTitle')}${skill.name} - Skill Base`;
|
|
76
|
-
|
|
77
|
-
// 更新面包屑
|
|
78
|
-
const breadcrumbSkill = document.getElementById('breadcrumb-skill');
|
|
79
|
-
breadcrumbSkill.textContent = skill.name;
|
|
80
|
-
breadcrumbSkill.href = `/skill.html?id=${encodeURIComponent(skillId)}`;
|
|
81
|
-
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error('Failed to load Skill info:', error);
|
|
84
|
-
showToast(t('diff.loadInfoFailed'), 'error');
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 加载版本列表
|
|
90
|
-
*/
|
|
91
|
-
async function loadVersions() {
|
|
92
|
-
try {
|
|
93
|
-
const data = await apiGet(`/skills/${skillId}/versions`);
|
|
94
|
-
versions = data.versions || [];
|
|
95
|
-
|
|
96
|
-
// 填充两个版本下拉框
|
|
97
|
-
const selectA = document.getElementById('version-a');
|
|
98
|
-
const selectB = document.getElementById('version-b');
|
|
99
|
-
|
|
100
|
-
const optionsHtml = versions.map((v, index) => {
|
|
101
|
-
const label = index === 0 ? `${v.version} (${t('skill.latestTag').replace(/[()]/g,'')})` : v.version;
|
|
102
|
-
return `<option value="${escapeHtml(v.version)}">${escapeHtml(label)}</option>`;
|
|
103
|
-
}).join('');
|
|
104
|
-
|
|
105
|
-
selectA.innerHTML = `<option value="">${t('diff.selectVersion')}</option>` + optionsHtml;
|
|
106
|
-
selectB.innerHTML = `<option value="">${t('diff.selectVersion')}</option>` + optionsHtml;
|
|
107
|
-
|
|
108
|
-
// 恢复选中状态
|
|
109
|
-
if (currentVersionA) selectA.value = currentVersionA;
|
|
110
|
-
if (currentVersionB) selectB.value = currentVersionB;
|
|
111
|
-
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.error('Failed to load version list:', error);
|
|
114
|
-
showToast(t('diff.loadVersionsFailed'), 'error');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 版本选择变更事件
|
|
120
|
-
*/
|
|
121
|
-
function onVersionChange() {
|
|
122
|
-
const selectA = document.getElementById('version-a');
|
|
123
|
-
const selectB = document.getElementById('version-b');
|
|
124
|
-
currentVersionA = selectA.value;
|
|
125
|
-
currentVersionB = selectB.value;
|
|
126
|
-
|
|
127
|
-
// 更新 URL
|
|
128
|
-
updateUrl();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 更新 URL(不刷新页面)
|
|
133
|
-
*/
|
|
134
|
-
function updateUrl() {
|
|
135
|
-
const url = new URL(window.location);
|
|
136
|
-
if (currentVersionA) {
|
|
137
|
-
url.searchParams.set('version_a', currentVersionA);
|
|
138
|
-
}
|
|
139
|
-
if (currentVersionB) {
|
|
140
|
-
url.searchParams.set('version_b', currentVersionB);
|
|
141
|
-
}
|
|
142
|
-
if (currentFilePath) {
|
|
143
|
-
url.searchParams.set('path', currentFilePath);
|
|
144
|
-
} else {
|
|
145
|
-
url.searchParams.delete('path');
|
|
146
|
-
}
|
|
147
|
-
window.history.replaceState({}, '', url);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 更新文件路径显示
|
|
152
|
-
*/
|
|
153
|
-
function updateFilePathDisplay() {
|
|
154
|
-
const pathDisplay = document.getElementById('current-file-path');
|
|
155
|
-
if (currentFilePath) {
|
|
156
|
-
pathDisplay.textContent = currentFilePath;
|
|
157
|
-
} else {
|
|
158
|
-
pathDisplay.textContent = t('diff.allFiles');
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 执行 Diff
|
|
164
|
-
*/
|
|
165
|
-
async function performDiff() {
|
|
166
|
-
if (!currentVersionA || !currentVersionB) {
|
|
167
|
-
showToast(t('diff.selectBoth'), 'warning');
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (currentVersionA === currentVersionB) {
|
|
172
|
-
showToast(t('diff.selectBoth'), 'warning');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const outputContainer = document.getElementById('diff-output-container');
|
|
177
|
-
const outputDiv = document.getElementById('diff-output');
|
|
178
|
-
|
|
179
|
-
// 显示 loading
|
|
180
|
-
outputDiv.innerHTML = `
|
|
181
|
-
<div class="loading-content">
|
|
182
|
-
<div class="spinner"></div>
|
|
183
|
-
<p style="margin-top: var(--spacing-md); color: var(--text-secondary);">${t('diff.computing')}</p>
|
|
184
|
-
</div>
|
|
185
|
-
`;
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
// 1. 并发下载两个版本的 zip
|
|
189
|
-
const [bufA, bufB] = await Promise.all([
|
|
190
|
-
fetch(`${API_BASE}/skills/${skillId}/versions/${currentVersionA}/download`, { credentials: 'same-origin' }).then(r => {
|
|
191
|
-
if (!r.ok) throw new Error(`Failed to download version ${currentVersionA}`);
|
|
192
|
-
return r.arrayBuffer();
|
|
193
|
-
}),
|
|
194
|
-
fetch(`${API_BASE}/skills/${skillId}/versions/${currentVersionB}/download`, { credentials: 'same-origin' }).then(r => {
|
|
195
|
-
if (!r.ok) throw new Error(`Failed to download version ${currentVersionB}`);
|
|
196
|
-
return r.arrayBuffer();
|
|
197
|
-
})
|
|
198
|
-
]);
|
|
199
|
-
|
|
200
|
-
// 2. JSZip 解压
|
|
201
|
-
[zipA, zipB] = await Promise.all([
|
|
202
|
-
JSZip.loadAsync(bufA),
|
|
203
|
-
JSZip.loadAsync(bufB)
|
|
204
|
-
]);
|
|
205
|
-
|
|
206
|
-
if (currentFilePath) {
|
|
207
|
-
// 单文件 diff
|
|
208
|
-
document.getElementById('changed-files').style.display = 'none';
|
|
209
|
-
await diffSingleFile(currentFilePath);
|
|
210
|
-
} else {
|
|
211
|
-
// 整体对比:列出所有变更的文件
|
|
212
|
-
await diffAllFiles();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
} catch (error) {
|
|
216
|
-
console.error('Diff failed:', error);
|
|
217
|
-
showToast(t('diff.computeFailed') + ': ' + error.message, 'error');
|
|
218
|
-
outputDiv.innerHTML = `
|
|
219
|
-
<div class="empty-state">
|
|
220
|
-
<div class="empty-state-icon">❌</div>
|
|
221
|
-
<p>${t('diff.computeFailed')}: ${escapeHtml(error.message)}</p>
|
|
222
|
-
</div>
|
|
223
|
-
`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 单文件 Diff
|
|
229
|
-
* @param {string} filePath - 文件路径
|
|
230
|
-
*/
|
|
231
|
-
async function diffSingleFile(filePath) {
|
|
232
|
-
const outputDiv = document.getElementById('diff-output');
|
|
233
|
-
const statsDiv = document.getElementById('diff-stats');
|
|
234
|
-
|
|
235
|
-
// 检查是否为二进制文件
|
|
236
|
-
if (isBinaryExt(filePath)) {
|
|
237
|
-
outputDiv.innerHTML = `
|
|
238
|
-
<div class="binary-diff-notice">
|
|
239
|
-
<div class="empty-state-icon">📦</div>
|
|
240
|
-
<p>${t('diff.binaryDiff')}</p>
|
|
241
|
-
<p class="text-muted mt-1">${escapeHtml(filePath)}</p>
|
|
242
|
-
</div>
|
|
243
|
-
`;
|
|
244
|
-
statsDiv.style.display = 'none';
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const fileA = zipA.file(filePath);
|
|
250
|
-
const fileB = zipB.file(filePath);
|
|
251
|
-
|
|
252
|
-
const contentA = fileA ? await fileA.async('string') : '';
|
|
253
|
-
const contentB = fileB ? await fileB.async('string') : '';
|
|
254
|
-
|
|
255
|
-
// 如果内容完全相同
|
|
256
|
-
if (contentA === contentB) {
|
|
257
|
-
outputDiv.innerHTML = `
|
|
258
|
-
<div class="empty-state">
|
|
259
|
-
<div class="empty-state-icon">✅</div>
|
|
260
|
-
<p>Files are identical, no differences</p>
|
|
261
|
-
</div>
|
|
262
|
-
`;
|
|
263
|
-
statsDiv.style.display = 'none';
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 使用 Diff.createPatch 生成 unified diff
|
|
268
|
-
const patch = Diff.createPatch(
|
|
269
|
-
filePath,
|
|
270
|
-
contentA,
|
|
271
|
-
contentB,
|
|
272
|
-
`Version ${currentVersionA}`,
|
|
273
|
-
`Version ${currentVersionB}`
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// 使用 diff2html 渲染
|
|
277
|
-
renderDiff(patch);
|
|
278
|
-
|
|
279
|
-
// 更新统计信息
|
|
280
|
-
updateDiffStats(patch, 1);
|
|
281
|
-
statsDiv.style.display = 'flex';
|
|
282
|
-
|
|
283
|
-
} catch (error) {
|
|
284
|
-
console.error('单文件 Diff 失败:', error);
|
|
285
|
-
outputDiv.innerHTML = `
|
|
286
|
-
<div class="empty-state">
|
|
287
|
-
<div class="empty-state-icon">❌</div>
|
|
288
|
-
<p>对比失败: ${escapeHtml(error.message)}</p>
|
|
289
|
-
</div>
|
|
290
|
-
`;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* 全文件 Diff(列出所有变更文件)
|
|
296
|
-
*/
|
|
297
|
-
async function diffAllFiles() {
|
|
298
|
-
const outputDiv = document.getElementById('diff-output');
|
|
299
|
-
const statsDiv = document.getElementById('diff-stats');
|
|
300
|
-
const changedFilesDiv = document.getElementById('changed-files');
|
|
301
|
-
const changedFilesListDiv = document.getElementById('changed-files-list');
|
|
302
|
-
|
|
303
|
-
// 收集所有文件
|
|
304
|
-
const filesA = new Set();
|
|
305
|
-
const filesB = new Set();
|
|
306
|
-
|
|
307
|
-
zipA.forEach((path, entry) => {
|
|
308
|
-
if (!entry.dir) filesA.add(path);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
zipB.forEach((path, entry) => {
|
|
312
|
-
if (!entry.dir) filesB.add(path);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
const allFiles = new Set([...filesA, ...filesB]);
|
|
316
|
-
changedFiles = [];
|
|
317
|
-
|
|
318
|
-
// 检查每个文件的变更状态
|
|
319
|
-
for (const file of allFiles) {
|
|
320
|
-
const inA = filesA.has(file);
|
|
321
|
-
const inB = filesB.has(file);
|
|
322
|
-
|
|
323
|
-
let status;
|
|
324
|
-
if (!inA && inB) {
|
|
325
|
-
status = 'added';
|
|
326
|
-
} else if (inA && !inB) {
|
|
327
|
-
status = 'deleted';
|
|
328
|
-
} else {
|
|
329
|
-
// 比较内容
|
|
330
|
-
const contentA = await zipA.file(file)?.async('string') ?? '';
|
|
331
|
-
const contentB = await zipB.file(file)?.async('string') ?? '';
|
|
332
|
-
if (contentA !== contentB) {
|
|
333
|
-
status = 'modified';
|
|
334
|
-
} else {
|
|
335
|
-
continue; // 内容相同,跳过
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
changedFiles.push({ file, status });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// 排序:按状态和文件名
|
|
343
|
-
changedFiles.sort((a, b) => {
|
|
344
|
-
const statusOrder = { added: 0, deleted: 1, modified: 2 };
|
|
345
|
-
if (statusOrder[a.status] !== statusOrder[b.status]) {
|
|
346
|
-
return statusOrder[a.status] - statusOrder[b.status];
|
|
347
|
-
}
|
|
348
|
-
return a.file.localeCompare(b.file);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// 如果没有变更
|
|
352
|
-
if (changedFiles.length === 0) {
|
|
353
|
-
outputDiv.innerHTML = `
|
|
354
|
-
<div class="empty-state">
|
|
355
|
-
<div class="empty-state-icon">✅</div>
|
|
356
|
-
<p>The two versions are identical, no differences</p>
|
|
357
|
-
</div>
|
|
358
|
-
`;
|
|
359
|
-
statsDiv.style.display = 'none';
|
|
360
|
-
changedFilesDiv.style.display = 'none';
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// 渲染变更文件列表
|
|
365
|
-
renderChangedFileList(changedFiles);
|
|
366
|
-
changedFilesDiv.style.display = 'block';
|
|
367
|
-
|
|
368
|
-
// 生成所有文件的合并 diff
|
|
369
|
-
let combinedPatch = '';
|
|
370
|
-
for (const change of changedFiles) {
|
|
371
|
-
if (isBinaryExt(change.file)) {
|
|
372
|
-
combinedPatch += `diff --git a/${change.file} b/${change.file}\n`;
|
|
373
|
-
combinedPatch += `Binary files differ\n`;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const contentA = await zipA.file(change.file)?.async('string') ?? '';
|
|
378
|
-
const contentB = await zipB.file(change.file)?.async('string') ?? '';
|
|
379
|
-
|
|
380
|
-
const patch = Diff.createPatch(
|
|
381
|
-
change.file,
|
|
382
|
-
contentA,
|
|
383
|
-
contentB,
|
|
384
|
-
`Version ${currentVersionA}`,
|
|
385
|
-
`Version ${currentVersionB}`
|
|
386
|
-
);
|
|
387
|
-
combinedPatch += patch;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// 渲染 diff
|
|
391
|
-
renderDiff(combinedPatch);
|
|
392
|
-
|
|
393
|
-
// 更新统计信息
|
|
394
|
-
updateDiffStats(combinedPatch, changedFiles.length);
|
|
395
|
-
statsDiv.style.display = 'flex';
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* 渲染变更文件列表
|
|
400
|
-
* @param {array} changes - 变更文件数组
|
|
401
|
-
*/
|
|
402
|
-
function renderChangedFileList(changes) {
|
|
403
|
-
const listDiv = document.getElementById('changed-files-list');
|
|
404
|
-
|
|
405
|
-
const statusLabels = {
|
|
406
|
-
added: t('diff.added'),
|
|
407
|
-
deleted: t('diff.deleted'),
|
|
408
|
-
modified: t('diff.modified')
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
listDiv.innerHTML = changes.map((change, index) => `
|
|
412
|
-
<div class="changed-file-item" data-file="${escapeHtml(change.file)}" onclick="selectChangedFile('${escapeHtml(change.file)}', ${index})">
|
|
413
|
-
<span class="changed-file-status ${change.status}">${statusLabels[change.status]}</span>
|
|
414
|
-
<span class="changed-file-path">${escapeHtml(change.file)}</span>
|
|
415
|
-
</div>
|
|
416
|
-
`).join('');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* 选择变更文件进行单独 diff
|
|
421
|
-
* @param {string} filePath - 文件路径
|
|
422
|
-
* @param {number} index - 索引
|
|
423
|
-
*/
|
|
424
|
-
async function selectChangedFile(filePath, index) {
|
|
425
|
-
// 更新选中状态
|
|
426
|
-
document.querySelectorAll('.changed-file-item').forEach((el, i) => {
|
|
427
|
-
el.classList.toggle('active', i === index);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
currentFilePath = filePath;
|
|
431
|
-
updateFilePathDisplay();
|
|
432
|
-
updateUrl();
|
|
433
|
-
|
|
434
|
-
await diffSingleFile(filePath);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 渲染 Diff(使用 diff2html)
|
|
439
|
-
* @param {string} patch - unified diff 字符串
|
|
440
|
-
*/
|
|
441
|
-
function renderDiff(patch) {
|
|
442
|
-
const outputDiv = document.getElementById('diff-output');
|
|
443
|
-
|
|
444
|
-
// 使用 Diff2HtmlUI API
|
|
445
|
-
const targetElement = document.getElementById('diff-output');
|
|
446
|
-
const configuration = {
|
|
447
|
-
drawFileList: false,
|
|
448
|
-
matching: 'lines',
|
|
449
|
-
outputFormat: outputFormat, // 'side-by-side' 或 'line-by-line'
|
|
450
|
-
renderNothingWhenEmpty: false
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
const diff2htmlUi = new Diff2HtmlUI(targetElement, patch, configuration);
|
|
454
|
-
diff2htmlUi.draw();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* 更新 Diff 统计
|
|
459
|
-
* @param {string} patch - unified diff 字符串
|
|
460
|
-
* @param {number} fileCount - 文件数量
|
|
461
|
-
*/
|
|
462
|
-
function updateDiffStats(patch, fileCount) {
|
|
463
|
-
let added = 0;
|
|
464
|
-
let removed = 0;
|
|
465
|
-
|
|
466
|
-
patch.split('\n').forEach(line => {
|
|
467
|
-
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
468
|
-
added++;
|
|
469
|
-
}
|
|
470
|
-
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
471
|
-
removed++;
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
document.getElementById('stats-added').textContent = `+${added} lines`;
|
|
476
|
-
document.getElementById('stats-removed').textContent = `-${removed} lines`;
|
|
477
|
-
document.getElementById('stats-files').textContent = `${fileCount} ${t('diff.filesChanged')}`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* 切换视图格式
|
|
482
|
-
* @param {string} format - 'side-by-side' 或 'line-by-line'
|
|
483
|
-
*/
|
|
484
|
-
function toggleOutputFormat(format) {
|
|
485
|
-
outputFormat = format;
|
|
486
|
-
|
|
487
|
-
// 更新按钮状态
|
|
488
|
-
document.getElementById('btn-side-by-side').classList.toggle('active', format === 'side-by-side');
|
|
489
|
-
document.getElementById('btn-line-by-line').classList.toggle('active', format === 'line-by-line');
|
|
490
|
-
|
|
491
|
-
// 如果已经有 diff 结果,重新渲染
|
|
492
|
-
if (zipA && zipB) {
|
|
493
|
-
if (currentFilePath) {
|
|
494
|
-
diffSingleFile(currentFilePath);
|
|
495
|
-
} else if (changedFiles.length > 0) {
|
|
496
|
-
// 重新生成合并 diff
|
|
497
|
-
performDiff();
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* 判断是否为二进制文件扩展名
|
|
504
|
-
* @param {string} filePath - 文件路径
|
|
505
|
-
* @returns {boolean}
|
|
506
|
-
*/
|
|
507
|
-
function isBinaryExt(filePath) {
|
|
508
|
-
const ext = '.' + filePath.split('.').pop()?.toLowerCase();
|
|
509
|
-
return BINARY_EXTS.has(ext);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* 返回 Skill 详情页
|
|
514
|
-
*/
|
|
515
|
-
function goToSkillDetail() {
|
|
516
|
-
if (skillId) {
|
|
517
|
-
window.location.href = `/skill.html?id=${encodeURIComponent(skillId)}`;
|
|
518
|
-
} else {
|
|
519
|
-
window.location.href = '/';
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* 清除文件选择,显示全部变更
|
|
525
|
-
*/
|
|
526
|
-
function clearFileSelection() {
|
|
527
|
-
currentFilePath = null;
|
|
528
|
-
updateFilePathDisplay();
|
|
529
|
-
updateUrl();
|
|
530
|
-
|
|
531
|
-
// 清除选中状态
|
|
532
|
-
document.querySelectorAll('.changed-file-item').forEach(el => {
|
|
533
|
-
el.classList.remove('active');
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// 重新执行全文件 diff
|
|
537
|
-
if (zipA && zipB) {
|
|
538
|
-
diffAllFiles();
|
|
539
|
-
}
|
|
540
|
-
}
|